diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17230d4..d63e9cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,9 @@ on: - 'v*' workflow_dispatch: +permissions: + contents: read + jobs: build-windows: runs-on: windows-latest @@ -19,7 +22,7 @@ jobs: node-version: '22' - name: Install dependencies - run: npm install + run: npm ci - name: Run dist build (Windows) run: npm run dist-windows @@ -46,7 +49,7 @@ jobs: node-version: '22' - name: Install dependencies - run: npm install + run: npm ci - name: Run dist build (Linux) run: npm run dist-linux @@ -75,7 +78,7 @@ jobs: node-version: '22' - name: Install dependencies - run: npm install + run: npm ci - name: Run dist build (macOS) run: npm run dist-mac @@ -93,6 +96,8 @@ jobs: release: needs: [build-windows, build-linux, build-macos] runs-on: ubuntu-latest + permissions: + contents: write # creates the GitHub release steps: - name: Checkout code (for git log) uses: actions/checkout@v4 @@ -150,12 +155,17 @@ jobs: found { print } ' CHANGELOG.md) # Write to multiline output - echo "notes<> $GITHUB_OUTPUT + # Random delimiter: a CHANGELOG line reading exactly "EOF" would + # otherwise close the block early and inject further step outputs. + DELIM="notes-$(openssl rand -hex 16)" + echo "notes<<$DELIM" >> $GITHUB_OUTPUT echo "$NOTES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + echo "$DELIM" >> $GITHUB_OUTPUT - name: Create Release - uses: softprops/action-gh-release@v1 + # Pinned to a commit: a mutable tag can be repointed at any time, + # and this action holds a token that can write releases. + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 with: tag_name: ${{ steps.tag.outputs.tag }} body: ${{ steps.changelog.outputs.notes }} @@ -176,9 +186,11 @@ jobs: needs: [release, deploy-demo] if: always() runs-on: ubuntu-latest + permissions: + actions: write # deletes the intermediate build artifacts steps: - name: Delete intermediate artifacts - uses: geekyeggo/delete-artifact@v5 + uses: geekyeggo/delete-artifact@f275313e70c08f6120db482d7a6b98377786765b # v5 with: name: | windows-dist diff --git a/.gitignore b/.gitignore index 072e8c8..90a20cb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ src/gll.bundle.js.map # Perf benchmark artifacts (regenerate via npm run perf:fixture / npm run perf) scripts/fixtures/ +# Release audit worksheets (/audit-sweep output, working docs — not release artifacts) +AUDIT-*.md + # Compiled Java class files *.class diff --git a/API.md b/API.md index 37fb376..14db8ce 100644 --- a/API.md +++ b/API.md @@ -372,7 +372,7 @@ To group by data instead of by id, use `groupOneProps` — e.g. (handy with §6 categorical values). Useful `bubbleSetStyle.` fields: `fill`, `fillOpacity`, `stroke`, -`strokeOpacity`, `virtualEdges` (boolean), `label` (boolean), `labelText`, +`strokeOpacity`, `padding`, `corridor`, `avoidance`, `label` (boolean), `labelText`, `labelFill`, `labelFontSize`, `labelBackground`, `labelBackgroundFill`, `labelBackgroundRadius`, `labelPlacement`. @@ -488,7 +488,9 @@ an `active` flag plus slider thresholds or selected categories. > `LOWER THAN OR GREATER THAN ` (always two-sided — there is **no** > standalone `LOWER THAN`), `IN [value1, value2]`, or the unary `IS MISSING` > (true when the property is absent/empty or belongs to the other element type; -> emitted automatically by the filter panel's non-strict AND join) — with +> emitted automatically by the filter panel's non-strict AND join) or +> `IS FOREIGN` (true only when the property belongs to the other element type; +> emitted by the AND join's "complete cases only" mode) — with > clauses combined by `AND` / `OR` / `NOT` and grouped in parentheses. There are > no `<`, `>`, or `=` operators. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1b85cad..92b8196 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -24,7 +24,7 @@ Sigma — which presents a stable facade over the `graphology` graph held in `src/graph/graph_model.js`. Managers talk to the adapter and the model, never to Sigma directly. -### Graph layer (`src/graph/`, 27 files) +### Graph layer (`src/graph/`) The rendering and graph-operation core. Key modules: @@ -33,46 +33,113 @@ The rendering and graph-operation core. Key modules: - `core.js` — `GraphCoreManager`: render/draw cycles, event locks, behaviour wiring - `interactions.js` — drag, zoom, lasso, click, hover handlers - `style.js` — per-element style resolution with defaults, per-layout persistence -- `layout.js` / `layout_algorithms.js` — workspace management; headless layouts +- `layout.js` / `layout_algorithms.js` / `workspace_dialog.js` — workspace + management, the "Create New Workspace" dialog, and headless layouts (force via graphology forceAtlas2; circular/grid geometric; radial/concentric/mds via `@antv/layout` v2) - `filter.js` — range-slider + dropdown filtering - `selection.js` — selection state with undo/redo memory - `bubble_sets.js` / `bubble_layer.js` / `bubble_geometry.js` — bubble-set grouping - drawn on an owned canvas layer beneath the nodes + drawn on an owned canvas layer beneath the nodes. A workspace holds ANY number + of groups: the list is `Object.keys(layout.bubbleSetStyle)`, not a constant, so + every caller reads it through `traverseBubbleSets()` — which describes the + SELECTED workspace, and code touching a different layout (`io.parseLayouts`, + `layout.createDefaultLayout`) must key off that layout's own map instead. + Membership has two sources unioned by `getEffectiveGroupMembers`: + `${group}Props` (filters, resolved live) and `${group}ManualMembers` (node IDs, + a snapshot) +- `bubble_tuning.js` / `bubble_smoothing.js` — the layout-aware initial group + settings behind ✨ Re-tune, and the Catmull-Rom ring painter/resampler both the + canvas layer and the SVG export draw through +- `annotation_layer.js` / `annotation_geometry.js` — text notes: a DOM overlay + over the stage (place/drag/edit/style popover) plus the node-safe box metrics + the layer, the PNG export and the SVG export all measure with - `heatmap_layer.js` / `heatmap_geometry.js` — node-density heatmap overlay (off by - default; toggled from the workspace toolbar) + default; one row of the inspector's Overlays layer stack, which owns both its + switch and its parameters — see `UIManager.OVERLAYS`) - `edge_programs.js` / `edge_flow_programs.js` / `edge_flow_glsl.js` / `flow_animator.js` — custom WebGL edge programs and the animated source→target flow overlay (dash/pulse/comet/chevron) +- `overlay_frame.js` — the one rAF/signature/teardown coalescer the three + owned-canvas overlays (bubbles, heatmap, notes) render through +- `overlay_keys.js` — the "has the cached fit gone stale?" keys those layers + compare (member-id key, position checksum, style key) +- `shortest_path.js` — shortest-path selection between two picked nodes - `label_renderers.js`, `pie_slices.js`, `shape_textures.js`, `minimap.js`, `visible_graph.js`, `lasso_geometry.js`, `communities.js`, `export_svg.js`, - `webgl_support.js` + `webgl_support.js`, `dpr_watch.js` -### Functional managers (`src/managers/`, 7 files + `assistant/`) +### Functional managers (`src/managers/`, plus `assistant/`) Business logic and UI: - `io.js` — `IOManager`: Excel/JSON loading, export (JSON / PNG / SVG), data - preprocessing, Excel template generation -- `ui.js` — loading overlays, UI enable/disable, notifications -- `query.js` — query DSL with an AST (AND/OR/NOT, BETWEEN, IN, IS MISSING, comparisons) -- `ui_components.js` — filter UI (dropdown checklists, invertible range sliders), tooltips -- `ui_style_div.js` — the styling panel (node/edge styles, badges, edge flow, density heatmap) -- `metrics.js` — `NetworkMetrics`: degree/betweenness/closeness/eigenvector centrality, PageRank + preprocessing; the Excel column schema lives in `excel_schema.js` and the + downloadable workbook in `excel_template.js` +- `ui.js` — loading overlays, UI enable/disable, notifications, presentation mode, + and the inspector's overlay layer stack (`OVERLAYS`: one table row per thing + drawn over the graph, each layer answering `visible`/`setVisible`) +- `rail.js` — the 52 px top rail and its dropdown menus (app, workspace, layout, + select, export) +- `inspector.js` — the single right-hand panel's context router + (Filters / Overlays / Selection). `CONTEXTS` is the whole contract: the tab + roles, roving tabindex and arrow-key nav are generic over it, and panel and + pill ids are derived from the context name +- `workbench.js` — the bottom surface over the stage; four non-destructive tabs + (Data, Query, Metrics, Assistant) with a remembered height each +- `filter_search.js` — the property search over the inspector's filter list; the + box itself is built into `#filterContainer` by `ui.buildFilterUI`, so one + delegated listener here survives every rebuild +- `command_palette.js` — `⌘K` / `Ctrl+K`: one search over every control, node and + edge. The index is scraped from the live DOM on each open, so it cannot drift + from the UI; rows print the control's breadcrumb and accelerator +- `history.js` — global undo/redo (`Ctrl+Z` / `Ctrl+Y`). Snapshots the current + workspace's whole view state and restores it through `lm.changeLayout()`, so a + feature that stores something new on the layout is undoable for free +- `query.js` — query DSL with an AST (AND/OR/NOT, BETWEEN, IN, IS MISSING, IS FOREIGN, + comparisons) +- `ui_components.js` — filter UI (dropdown checklists, invertible range sliders), + the per-row group chip, tooltips +- `group_menu.js` — the one group checklist, opened by both assign sites (a filter + row assigns a property, the Selection panel assigns nodes); built on `RailMenu` +- `community_menu.js` — the 🧩 Auto-detect configurator (Louvain weighting, + resolution, how many groups); also a `RailMenu`, so Escape, `aria-expanded`, + close-on-scroll and focus restore come for free +- `group_list.js` — the Groups list under Overlays › Groups (rows, the ⋯ menu, the + +/- selection buttons). DOM only: it is handed the bubble-set MANAGER rather + than reaching for it through the cache, and every state change routes back + through it. `bubble_sets` keeps `renderGroupList`/`syncGroupRows` delegators so + callers still have one entry point +- `ui_style_div.js` — builds every config card (node/edge styles, badges, edge flow, + bubble sets, density heatmap, select/act/arrange); `ui.js` `CARD_MOUNTS` then + re-parents each card to its single home in the rail or the inspector. The Groups + card is a list (painted by `bubble_sets.renderGroupList`) over ONE settings pane + rebuilt for the selected row by `buildGroupStylePanel` — a pane per group would + be N × ~20 rows of DOM for a surface showing one group at a time +- `metrics.js` — `NetworkMetrics`: degree/betweenness/closeness/eigenvector centrality, PageRank. + Computed lazily — only while its workbench tab is visible (`setWorkbenchVisible`) - `api_client.js` — client for the standalone ingest service - `assistant/` — the natural-language Graph Assistant (intent parsing, query - generation, settings, budget UI) + generation, settings, budget UI); rendered as a workbench tab, not a dock -### Utilities (`src/utilities/`, 12 files) +### Utilities (`src/utilities/`) - `static.js` — validation, colour math, deep-merge helpers -- `popup.js` / `popover_position.js` — modal and popover positioning +- `popup.js` / `popover_position.js` / `checklist_popup.js` / `fetch_popup.js` — + modal, popover positioning (anchor clamping plus the dropdown flip-up maths), + checklist dialogs, and the shared lifecycle of a "fetch into the graph" dialog + (abort on dismiss, spinner, inline-or-toast errors, submit locked across the + gates) that both Neo4j popups run on +- `ui_tooltip.js` — the delegated tooltip layer: strips native `title`s and + renders them itself, and owns `splitShortcut`, the one parser for the trailing + "(F)" accelerator the command palette also reads - `data_editor.js` — spreadsheet-like data editor (`DataTable`), incl. Excel export - `demo_loader.js` — STRING DB protein-interaction demo data +- `neo4j_loader.js` — Neo4j connector (HTTP transactional Cypher API, no driver dependency) +- `neo4j_session.js` — Neo4j session extensions: expand selected nodes, merge additional queries - `tour.js` — guided tour with a sample dataset - `color_scale_picker.js` / `numeric_scale_picker.js` / `pie_chart_picker.js` — styling pickers -- `selection_hud.js`, `theme.js`, `export_scale.js` +- `excel_merge.js`, `theme.js`, `export_scale.js` ### Key files @@ -99,7 +166,10 @@ mapping bidirectionally between properties and node/edge IDs. Maps on `cache` for O(1) lookups. **Workspaces** — each layout independently stores node positions, styles, -filters, bubble groups, and queries. +filters, bubble groups, and queries. A workspace's groups live in three keyed +places — `bubbleSetStyle[group]`, `${group}Props`, `${group}ManualMembers` — and +deleting a group must clear all three, or `io.savedLayoutGroupKeys` infers the +group back from the orphan on the next load. ## Build & Run @@ -126,9 +196,9 @@ src/ ├── graph_lens_lite.html # SPA page ├── style.css # all CSS ├── lib/ # vendored libs (sigma, graphology, exceljs, marked, purify) -├── graph/ # rendering + graph operations (27 files) -├── managers/ # business-logic managers (7 files + assistant/) -├── utilities/ # helpers (12 files) +├── graph/ # rendering + graph operations +├── managers/ # business-logic managers + assistant/ +├── utilities/ # helpers └── package/ # Electron main process + build scripts server/ # standalone ingest service + live viewer templates/ # Excel input templates diff --git a/CHANGELOG.md b/CHANGELOG.md index d37bdc8..b373bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,107 @@ # Changelog +## 1.17.0 — 2026-08-07 + +Saved graph files, workspaces, filters, styles and bubble groups all load unchanged, including files written before this release — but **the interface is rearranged**, so it is worth reading the first section below before looking for a control where it used to be. The short version: the filter sidebar, styling sidebar, selection HUD and workspace bar are now a **rail** across the top, one **inspector** on the right with Filters / Overlays / Selection contexts, and a **workbench** of tabs (Data, Query, Metrics, Assistant) at the foot of the stage. `⌘K` / `Ctrl+K` finds any control by name and tells you where it lives, which is the fastest way to relearn the layout. + +(The Neo4j connector and its follow-ups were originally released as 1.16.0–1.16.3 on the `feat/neo4j-connector` branch; they are renumbered into this release because `main` published its own 1.16.x line in the meantime.) + +### Features + +* **The interface is now a rail, a stage and one inspector.** The four panels that used to compete for the screen — filter sidebar, styling sidebar, selection HUD, workspace bar — are gone, replaced by three surfaces with one job each. Saved graphs, workspaces, filters and styles are unaffected; only where you reach for things changed. + * **The rail** (top, 52 px, always there) carries the session-level verbs: the **◆ app menu** (open, template, STRING, Neo4j, tour, start over), the **workspace chip** with live shown/total counts and switch/new/rename/delete, **⊡ Fit**, **↻ Layout** — labelled with the workspace's current algorithm, so it is no longer invisible until you open a dialog — **➰ Lasso**, **✨ Hover**, **◈ Select**, **✎ Note**, the **selection chip** (counts, focus, clear, undo/redo, plus a warning when filters hide part of your selection), the Data/Query/Metrics/Assist tabs, **⤓ Export**, **⛶ presentation mode**, the theme toggle and **?**. + * **The inspector** (right, resizable) is the app's single panel, with three contexts. **Filters** holds one filter per property; **Overlays** is a layer stack, one row per thing drawn over the graph; **Selection** holds group membership, act-on-selection, arrange and appearance. **No context ever opens itself.** When the selection changes while you are somewhere else — *Add to selection* on a filter row, a click on the canvas — the **Selection** pill flashes and waits; it never takes the panel out from under the row you are working in. + * **Nothing hides behind a toggle any more.** The **⚙ Details** switch is gone: exact min/max inputs and the per-row group/selection buttons are always rendered. The floating selection HUD and its **Tools ▾** drawer are gone: its contents are permanent inspector sections. Fold a section with its **▾** chevron when a list gets long. + * **◈ Select** promotes the app's biggest hidden surface — select all/no nodes and edges, or by node/edge ID or label with an include/exclude switch — to one click on the rail. + * **Overlays is a layer stack.** One row each for **Groups**, the **density heatmap** (previously three levels deep inside the styling panel), **Notes** and the **minimap**. Every row carries its own on/off switch and opens onto its own settings, so switching a layer on and tuning it are the same gesture in the same place — the heatmap's parameters are no longer a greyed card whose switch lives on the far side of the window. Hiding a layer hides it in PNG and SVG exports too. A layer with nothing in it yet — no groups, no notes — greys its switch and says why, rather than offering a toggle with no visible effect. + * **Presentation mode** (**⛶** on the rail, **⇧F**, Escape to leave) hides the rail and the inspector for a clean screenshot — a better answer than the old HUD's ✕, which could strand you behind a 90 px restore chip. + * **Group membership has its own section** under **Selection**: one **+ Add to group** button opening a list of your groups by name. + * **SVG export is a visible row** in the ⤓ Export menu alongside PNG 1×/2×/4×, JSON and Excel, instead of hiding inside the 📷 popover. + * **The workbench** (bottom, over the stage) hosts **Data**, **Query**, **Metrics** and **Assistant** as four tabs that never close each other — Query and Data used to silently evict one another from a single slot, and the assistant was a fifth docked panel competing for width. It occludes the canvas and never the inspector, remembers a height per tab, and **⤢** expands it to full height for spreadsheet work. Metrics and the assistant moved in with it; the assistant now reads in a centred 720 px column with **⚙ Settings** and **🗑 Clear** in the workbench toolbar. + * **⌘K / Ctrl+K finds anything by name.** One search box over every control in the app, plus every node and edge: type `heat` for the density heatmap and its nine parameters, `arrow` for the edge arrow fields, an airport's name to centre the graph on it. Each row prints **where the control lives** — `Inspector › Overlays › Density Heatmap` — and its accelerator, so the palette teaches the layout instead of replacing it; **↵** runs a command, **⇥** just takes you to it and rings it. The index is read from the interface itself, so nothing can be missing from it. The old *Focus Elements* card, two datalists under an inspector section, is gone — the palette does its job. + * **⤢ expands the filters over the stage.** The inspector's filter list lifts onto a full-width surface with search and multi-column layout, active filters first, so a graph with 40 properties is readable without scrolling a 356 px column. It stops at the top of the workbench, so filters and the query editor can be used together. + * **Messages come to you.** Loading results, warnings and errors appear as a **toast over the stage** and fade on their own (errors linger longest, ✕ dismisses early) instead of only accumulating in a strip at the foot of the panel that could be off screen when it mattered. Nothing is lost: every message still lands in the **Activity log** at the foot of the inspector, which is now a collapsed disclosure with a line count — open it to read back the session, including the Neo4j Cypher trace. + * **A keyboard cheat sheet** opens with **?**. `Y` now jumps the inspector to the appearance controls rather than toggling a panel. + * **The filter panel is half the height, with nothing removed.** Node and edge filters are a **Nodes | Edges** segment carrying live counts rather than two full-width accordions — the split is always exactly those two, so it never needed the panel's loudest treatment. Sub-groups are a small-caps label and a hairline instead of a second filled bar. A numeric filter is **one line**: its exact min and max sit at the two ends of the slider, rounded while you read them and exact the moment you click into one, so the hover value bubbles that used to show the same two numbers a second time are gone. **Search moved into the panel**, so you no longer have to expand over the stage to find a property among 40 — searching spans both nodes and edges. On a 15-property template the panel went from 855 px to 362 px. + * **Nothing selected now offers buttons, not prose.** The Selection context's empty state named three ways to build a selection and made two of them clickable; **➰ Lasso**, **◈ By name or ID** and **📝 Query editor** are all one click now. +* **Bubble groups: as many as you want, and all in one place.** Groups were capped at four and built through three unlabelled glyphs in three different tabs — an 18 px four-quadrant pie on every filter row, an identical pie in the Selection tab that did something different, and a hover-✕ on a badge. The pie's four wedges *were* the cap. + * **Overlays › Groups is now the whole feature.** One row per group carrying its colour, its name, how many nodes it holds, a **+ / -** button, and a **⋯** menu (select members, duplicate, clear, delete). Open a row for its fill, outline, padding and label settings. A new workspace starts with **no groups** and an empty state that says what a group is, instead of four empty ones that read as decoration. + * **Groups have names.** The row's name field *is* the label drawn on the bubble, so the list and the canvas can never disagree. No more "top-left" and "Bubble Set 3" for the same thing. + * **Make as many as you need** — **+ New group**, **+ From selection**, or from a filter. Beyond the original four brand colours new groups take evenly spaced hues. + * **One menu, both ways in.** The filter row's chip and the Selection tab's **+ Add to group** open the same named checklist, with a ✓ for groups you are fully in and a **–** for ones you are partly in. Each also offers *New group from this filter / from selection*. The filter-row chip is hollow when unassigned, filled with the group's colour when in one, and ringed when in several — hover it for the names. + * **A group can be fed by a filter and by a selection at once** — it always could, but nothing said so. The row now spells it out: *⚙ Node › type · +8 manual*. Changing the filter moves only the filter-driven half. Removing a node that still matches the filter says so rather than looking like a click that did nothing, and **⋯** can clear either source on its own. + * **Auto-detect creates groups instead of overwriting them.** Choose how many communities to turn into groups; existing groups are left alone, so the confirmation dialog that used to warn you about losing them is gone. + * **A new group dresses for the layout it lands in.** Padding, corridor width and *Avoid other nodes* used to start at one fixed default, which was simultaneously too fat for a group threaded through a dense neighbourhood and too skimpy for one sitting in open space. A group created from a selection, from a filter or by auto-detect now measures its surroundings once — how close the nearest non-members crowd in, how far apart its own members sit — and starts with settings to match: tight and steering around bystanders in a hairball, generous and cheap (avoidance off) when alone. Nothing is ever recomputed behind your back afterwards — the sliders show the picked values and stay yours — and **⋯ › ✨ Re-tune shape** re-runs the measurement on demand after a re-layout. An empty **+ New group** keeps the stock defaults; duplicating a group copies its settings untouched. + * **Dragging nodes stays responsive.** Fitting a bubble outline is expensive — on a 120-node group with *Avoid other nodes* on it costs about a third of a second — and it used to re-run on every frame while you dragged. A 60-step drag ran 60 fits and blocked for ~49 s. Cheap fits still track the node live; past one frame's budget the hull coasts on its last shape during the drag and re-fits once, when you let go. The same drag now runs **0 fits while dragging and one on release, ~3 s**. + * Saved graphs load unchanged, including files written before this release. +* **Undo and redo, for the whole workspace.** `↶ ↷` on the rail (and `Ctrl+Z` / `Ctrl+Y`, `⌘Z` / `⌘Y` on a Mac) take back the last change to how the workspace looks: styles, filters and queries, arrange and re-layout, bubble-group membership. The button's tooltip names what it would undo — *Undo: Arrange selection (grid)* — and a toast confirms it. Selection undo/redo stays where it was, on the selection chip. Loading a file, editing the data table, and merging from Excel or Neo4j rebuild the graph itself and clear the history rather than pretending they can be reversed; the last 20 steps are kept (5 on graphs over 2,000 nodes, where a snapshot is large). +* **Boolean properties get a boolean filter.** A property whose values are all `true`/`false`/`1`/`0` is classified as boolean and rendered as an **Any / True / False** toggle, instead of a two-item dropdown or — for `1`/`0` — a range slider from 0 to 1. Classification is purely data-driven: if the column later gains a third distinct value it reverts to numeric on the next rebuild. The query language gained `IS TRUE` / `IS FALSE` to match, and saved workspaces migrate on load. +* **Mixed-type columns stay visible.** A column holding both numbers and text used to be deleted from the filter list with only a transient warning, so the property silently vanished from the app. It is now rendered as a disabled row stating the reason and the offending counts. +* **Neo4j queries are visible in the activity log.** Every Cypher statement the connector sends — the initial fetch, row-count preflights, expansions, additional queries, and stitch passes — now appears as a grey 🛢️-prefixed line in the activity log at the foot of the inspector, so it's always clear what was asked of the server. Long queries are collapsed to one line and truncated to keep the log legible. +* **Neo4j connector.** Fetch a graph straight from a Neo4j server: a new **🛢️ Neo4j Database** card on the landing page (and an entry in the rail's ◆ app menu) opens a connection dialog for server URL, credentials, optional database name, and a Cypher query returning nodes, relationships, or paths. Before fetching, the connector counts the matching rows and asks for confirmation above 2,000; after fetching, a property checklist shows each property's type and example values and lets you drop unwanted ones — long arrays such as embeddings start deselected. Nodes are colored per entity label and edges per relationship type (when there is more than one) — edge colors use a dedicated mid-lightness palette that stays legible over both the light and dark theme backgrounds. Property groups use the most specific label of a stored class hierarchy, and list properties become pipe-separated multi-value categories and booleans true/false categories so the regular filters work on them. The connection settings (never the password) are remembered locally. Uses the Neo4j HTTP API on port 7474/7473 with no driver dependency; Neo4j Aura (Bolt-only) is not supported. +* **Stack Overflow demo.** No Neo4j server at hand? The connection dialog can fill itself in: a link loads the settings for [Neo4j Labs' public read-only demo server](https://github.com/neo4j-graph-examples) with a query fetching the best-answered `neo4j`-tagged Stack Overflow questions and their askers, answers, and tags (~1,500 nodes / ~1,800 relationships) — review and press **Fetch** to try the connector, expand, and join queries without any setup. Content from [Stack Overflow](https://stackoverflow.com) contributors, licensed [CC BY-SA](https://creativecommons.org/licenses/by-sa/4.0/). +* **Growing a Neo4j graph in place.** After a Neo4j import, the graph can be extended without starting over — both features live behind the active session (the password is kept in memory only and never persisted) and disappear when another data source replaces the graph: + * **🛢️ Expand** (inspector › Act on selection): with nodes selected, a checklist shows what surrounds them — one row per relationship type and neighbor label, with counts, everything preselected — and fetches the checked groups. New neighbors appear next to the node they connect to and float into place with a short force animation that feels the whole network but moves only them; existing nodes never move. The same **Stitch** checkbox as the join-query dialog (on by default) additionally fetches the relationships between the new neighbors and everything already loaded — including among the new neighbors themselves, which the expansion pattern alone never returns. + * **🛢️ Add query** (rail): runs an additional Cypher query against the connected server and merges the results into the current graph, even when they are disconnected from it. The same row-count confirmation as the initial import applies. A **Stitch** checkbox (on by default) runs one extra query that fetches all relationships between the new results and everything already loaded — individual queries only return the relationships their own pattern matched, so without it two queries about different entities never reveal how their neighborhoods interconnect. + * Merged data reuses the property exclusions chosen at import time (re-import to change them), entity colors stay stable across merges, and node identity follows Neo4j's ids — re-fetched nodes refresh their properties instead of duplicating. Filter narrowing resets on merge. Expansion requires Neo4j 5+ (`elementId`); the plain import keeps working on older servers. +* **Tooltips are the app's own.** Every hint the app carries used to be a native browser tooltip: half a second of delay, the operating system's styling, no formatting, and nothing at all for a keyboard user. One layer now intercepts them on hover *and* on keyboard focus and draws them in the app's own type — the control's name in bold ahead of its description, accelerators such as **(F)** or **(⌘K)** as key chips, and indented trees kept as written. The tip is hoverable, so a long one can be read at leisure. Nothing is lost to assistive technology: the underlying accessible names are unchanged. +* **A graphics-capable browser is checked before you pick a data source, not after.** GLL needs WebGL2. When it is missing or disabled, the landing page now says so above the buttons and disables the actions that need a renderer (open a file, STRING, Neo4j, the tour), instead of letting you choose a source, wait through the load, and find an error where the graph should be. Downloading the Excel template needs no renderer and stays available. +* **The filter panel says which section is doing the filtering.** Only one of Nodes / Edges is on screen at a time, so a narrowed filter in the *other* one could empty the graph with nothing anywhere explaining it. Each segment now badges how many of its own filters constrain — `Edges 2/14` — and the *"N of M filters constrain the graph"* census is shown under **OR** as well, not only under AND. +* **The guided tour has a way out.** A **Skip tour** button in the footer, and the popup's × says *Exit tour* rather than *Close popup* — on a 14-step tour that replaces your graph with sample data to start, the only exit was that unlabelled ×. +* **The remembered Neo4j connection can be forgotten.** The saved URL, username, database and query (never the password) persisted indefinitely with no way to clear them, and the query often carries literal identifiers from your data. The connection dialog offers **Forget saved connection** when there is something saved. +* **The workbench resize handle works from the keyboard.** Arrow keys move it, Shift+arrow crosses the stage quickly; it was drag-only before. + +### Performance + +* **Bubble outlines are painted, not rebuilt, on every pan and zoom.** Each group's hull was re-projected point by point and its curve rebuilt for every camera-changed frame; the cached hull is now one path in graph space, drawn under the camera transform, so panning and zooming is a transform change. Measured over 62k outline points: 1.90 ms → 12.08 ms of paint work at 50 groups was the regression that made the unbounded group count expensive, and it is gone. Groups whose outline lies entirely off-screen are skipped before any of that — zoomed into one of 50, the other 49 were still filling and stroking off-canvas. +* **Undo snapshots stopped scaling with the graph.** Every filter interaction, drag end and style write took a full snapshot including a whole-layout `JSON.stringify` retained as a string. The signature is a hash now and a change that changed nothing clones nothing: 19.15 ms → 9.93 ms at 10,000 nodes, 3.65 → 1.87 at 2,000, and 25–157 MB of dead signature strings at the configured history depth no longer exist. +* **Returning to a computed metric is free again.** The repaint rewrote every cached node tooltip's HTML twice — a parse and a serialize per node, per switch. The metric line is composed onto the tooltip at hover time instead. +* **Neo4j imports no longer cost two server round trips.** The row-count preflight fully re-executed your query just to compare against the 2,000-row threshold; it is skipped when a trailing `LIMIT` already answers the question, and otherwise capped one row past the threshold. A preflight that outruns its own 30-second budget also lets the fetch proceed (which has ten times the timeout) instead of killing the import. +* **Expand and stitch stopped doing work the query had already ruled out.** Expand traversed every relationship of every selected node and only then filtered to the types you kept, so keeping 1 of 20 neighbour groups still walked all 20; the chosen types are in the pattern now. Stitching stays incremental after a merge that skipped it, rather than re-sweeping the whole accumulated graph on every later pass. +* **Node textures stop thrashing the atlas.** The fade introduced this release folded each node's exact fill and unrounded size into the texture cache key, so a graph with degree-scaled sizes could blow the cache — which clears wholesale, re-minting an SVG per shape node and rechurning the image atlas on the next frame. Bake sizes are quantized to half a pixel. + +### Fixes + +* **The density heatmap's "dim graph" now actually dims.** It never did: the switch replaced every node and edge colour with a flat, fully **opaque** grey, and an opaque grey node hides the density field underneath it exactly as much as an opaque red one did — the heat layer is painted below the graph, so no amount of ramp tuning could fix it. It is now a **Fade graph** slider: nodes fade toward transparent, and past 40% the two things that actually block the view — **labels and edges** — drop out entirely, leaving the field and the node positions. Labels were the largest occluder of all and the old switch never touched them. Selected and hovered elements keep their normal treatment throughout. The heatmap's defaults are also stronger (opacity 0.7, intensity 0.25, contrast 0.85), because the old ones read as a smudge rather than a density gradient once the graph stopped covering them — workspaces that saved their own values keep them, and a workspace saved with the old switch on loads as a 70% fade. +* **Same-named properties are distinguishable in the pie-slice picker.** When properties with the same name exist on different groups (common after a Neo4j import, where every label can carry `name`, `score`, …), the **Map Properties to Pie Slices** dialog showed identical rows with no way to tell them apart. Colliding names now show their group in parentheses — `score (Cell)` vs `score (Document)` — in both the property list and the numeric slice-color rows, every property row gets a hover tooltip with its full `group > name` path, and the list is sorted by name so ambiguous twins sit next to each other. Unique names stay short as before. +* **The AND join says which filters are actually doing work.** Under **AND** a filter only constrains the graph once you narrow it from the range or values it loaded with — one left at its default means "don't care". That rule was invisible, so an untouched panel switched to AND produced an empty query for no apparent reason, and ticking a filter's checkbox appeared to do nothing. The filter toolbar now reads *"3 of 15 filters constrain the graph"* (or names the all-defaults case outright), and filters that carry no constraint have their name muted. Under OR, where active and constraining are the same thing, nothing is shown. +* **"Complete cases only" no longer empties the graph.** With the filter join set to **AND**, ticking **Complete cases only** after narrowing filters of just one element type wiped out the other type entirely — narrow a node property and every edge vanished; narrow an edge property and every node vanished, taking the whole graph with it. Elements of a type with no narrowed filter of its own were being judged against the other type's conditions, which they can never satisfy. A type carrying no filters is now simply unconstrained, while an element of a *filtered* type that is missing the value is still excluded, which is what the setting is for. The query language gained the `IS FOREIGN` predicate — true only when a property belongs to the other element type — to express it. +* **Boolean properties now filter correctly.** A boolean data property (e.g. `mined: true` on an edge) was classified as numeric and became a degenerate range slider whose condition could never match, silently hiding every element carrying it under an **OR** filter join — while **AND** appeared to work only because un-narrowed filters don't constrain. Booleans from any data source (Neo4j, Excel boolean cells, JSON payloads, live API pushes) are now normalized to categorical `true`/`false` filters at the import boundary. Reload affected data to pick up the fix. +* **Rail menus no longer truncate at the bottom of the window.** A menu anchored to a control near the bottom border — the group checklist on a filter row far down the panel, most visibly — opened downward off screen. Menus now slide up just enough to stay inside the viewport, the same way they already avoided the right edge, and a menu taller than the window itself scrolls instead of clipping. +* **Hovering no longer highlights through a filtered-out edge.** ✨ Hover lit up every neighbour incident to the hovered node, including ones whose connecting edge the filters had already removed — a highlight asserting a connection the graph was no longer showing. The traversal now skips hidden edges and hidden nodes, so the highlighted neighbourhood is the one you can actually see. +* **Bubble outlines no longer paint a solid wedge between two corridors.** A group whose members split into two arms could fill the empty space between them, next to the group's label. Two independent causes, both fixed: the marching grid the outline is traced on is now rounded to whole pixels — a fractional cell made adjacent contour cells collapse onto the same pixel and short-circuit the traced ring, and whether a graph hit it depended on node size, since the cell is derived from the members' mean radius (sweeping radius 4–40 px on the reported layout: 15 of 145 radii wedged before, 0 after) — and the smooth curve painted from that ring no longer overshoots at a sharp notch, because each control point is now damped by how sharply the ring turns there. Raising **Padding** above ~0.3 used to hide the second cause; it no longer needs to. +* **Bubble outlines are smoother, especially where they hug nodes.** The hull polygon kept the *extreme* points of the tracing grid's wobble — simplification connects ripple peaks rather than averaging them out — so the painted curve carried kinks and long angular chords that no amount of curve smoothing could round without re-opening the wedge bug above. The traced ring is now conditioned once per fit: resampled to evenly spaced points and smoothed with a shrink-compensated filter, which removes the noise instead of tracing it (sharpest corner on a wobbly test hull: 128° → 73°). Genuinely sharp features — the notch where two corridors meet — stay sharp, the tight finger-like look at minimum padding is untouched, and members keep their guaranteed clearance: anything the smoothing shaves is repaired the same way avoid-node pressure always was. +* **Metrics repaint when you return to one you have already computed.** Switching to a metric worked once. Coming back to it left the previous metric's ranking, graph-level table and 🛈 popup on screen, because the cache guard skipped the repaint along with the recompute — a first visit had no cache entry and looked fine, which disguised it as "cannot go backwards". Rendering is now separate from computing; the expensive calculation still runs at most once per metric per visible subgraph. **⤢** also expands the workbench to the full stage rather than 92%, where the leftover strip of canvas read as a rendering artefact. +* **Accessibility fixes from an axe-core pass.** The two range inputs behind every numeric filter had no accessible name, so a screen reader announced two anonymous sliders per property with no way to tell the lower threshold from the upper (WCAG 4.1.2); they are named now. White on the dark theme's brand fill measured 3.43:1 on the inspector pills and the OR/AND segment (WCAG 1.4.3 AA) and is now a dark foreground, as the design specifies for dark mode. The app also draws its own focus ring: controls without one fell through to Chrome's near-black `outline: auto`, which lingered on whatever the ⌘K palette had just revealed. Known and untouched: target-size (2.5.8) on the app's 14–18 px icon buttons, which needs a global sizing decision. +* **Undo covers node drags and notes.** Dragging nodes and adding, editing, moving, styling or deleting a note all wrote straight into the workspace without recording a step, so the next unrelated undo snapped dragged nodes back to where they were, or deleted text you had just typed, with no redo to recover it. Both are history steps now, one per gesture rather than one per keystroke or colour drag — and a note placement abandoned without typing records nothing at all. Relatedly, a change that changes nothing (a section reset with nothing narrowed, a re-applied query) no longer files an undo step whose before and after are identical, which used to make undo appear to do nothing and pushed the real previous state a press further away. +* **Actions that destroy unsaved work ask first.** *Open graph file…* replaced the loaded graph, its workspaces, styles and groups on one click, while its neighbour *Close graph and start over* had always confirmed; the assistant's 🗑 Clear dropped a whole conversation the same way, and the assistant's history is not part of the undo snapshot. Both confirm now, and clearing an empty conversation still happens silently. +* **Escape means cancel in a note.** Escape while editing committed the text instead of discarding it — so on a note you had just placed it saved the untouched "Text" default, which is exactly what the placement toast promises Escape avoids. It now abandons a freshly placed note and restores the previous text on an existing one. Pasting more than 2,000 characters also says that it trimmed, instead of silently truncating and looking like a failed paste. The **✎ Note** button shows while the tool is armed and disarms on a second press, like ➰ Lasso, and the note's style popover flips above the note when it would otherwise open off the bottom of the window. +* **Keyboard and screen-reader fixes.** Every plain popup — the keyboard sheet, the tour, the confirmations — is a real dialog now: it announces itself, takes focus, and closes on Escape (the topmost one only, so a confirmation over a dialog no longer takes both away). The rail's and group rows' **⋯** menus were unreachable by keyboard entirely; opening one moves focus into it and Escape returns it to the button, and Escape now stops at the open menu instead of also closing whatever is behind it. A menu inside a scrolling panel closes when the panel scrolls rather than floating away from its row. And a control that is disabled can finally tell you why: `pointer-events: none` meant "Nothing to undo" and "Select nodes first" could never be hovered at all, so those tooltips now work while the clicks stay blocked — except on a dimmed card's **▾** header, which stays foldable, since a section you cannot use is exactly the one you want out of the way. +* **Bare-key hotkeys no longer hijack browser and OS chords.** The shortcut switch matched on the key alone, so Ctrl+A toggled the assistant, Ctrl+F fitted the view, and Ctrl+S and Ctrl+P fired GLL's own exports over the browser's save and print dialogs. Undo and redo remain the only shortcuts that take a modifier. +* **Filter panel fixes.** A section's ⟳ *Reset to defaults* skipped boolean **Any / True / False** toggles, so a narrowed one kept constraining the graph invisibly under AND. 📝 *Add to query* on a boolean row wrote a literal `(undefined)` into the editor instead of `IS TRUE` / `IS FALSE`. Typing an exact threshold and clicking away kept the number on screen without applying it (and rendered `NaN` for non-numeric input); it now applies on blur exactly as Enter does, and an out-of-range value is clamped with the same warning the query path gives instead of silently snapping back. Under the manual-query lock the OR/AND toggle and *Complete cases only* looked live while doing nothing — they are dimmed with the rest now. A dimmed inert row under AND no longer claims that clicking it will hide elements. Searching the property list no longer throws away your folded groups to show a match, and a search that matches nothing says so instead of leaving a blank panel. +* **Both handles of a numeric filter are grabbable.** Two pointer bugs in the new one-line filter row, found before release. The row's styling is written for the two exact-value boxes at the ends of the track, but it also reached the slider's own two range handles nested inside it, capping them at `10ch` — half the track. Both native handles ended up crammed into the left half while the visible thumbs were still drawn across the full width, so the right-hand thumb had no hit area beneath it and pressing it moved the *left* threshold to wherever you clicked. Separately, a tooltip took the first click on anything it covered: a tip is tall enough to blanket four slider rows, so reaching a handle under one cost a press to dismiss the tip and a second to grab the thumb. Tips are transparent to the pointer now, and stay open while hovered (WCAG 1.4.13) by testing where the pointer is rather than by being in its way. +* **Command palette fixes.** Enter on a control the app had disabled by class — Arrange Selection with fewer than two nodes, say — ran it anyway, which flashed a loading overlay and filed an empty undo step; those rows dim and reveal like every other unavailable control. Nodes and edges hidden by the filters are no longer offered, since focusing one moved the camera onto empty canvas. The footer says what ↵ will actually do to the highlighted row rather than always "run", the result count admits when the element list is capped, and on Windows and Linux the key chips are back in tooltips (they were built as "Ctrl Z" with a space, which nothing parsed). +* **Neo4j connector fixes.** A write-shaped Cypher query (`CREATE`, `MERGE`, `SET`, `DELETE`, …) was **executed twice** — once by the row-count preflight, once by the fetch — silently duplicating writes; the preflight is skipped for those queries now. Closing a dialog mid-query used to leave the request running to its five-minute timeout and then act on it anyway, popping a property checklist or replacing your graph after you had dismissed it; Cancel stays live while a query runs and both it and × abort. Cancelling the property checklist no longer throws away the fetched data and the password you typed. Expand and *Add query* now explain failures the way the connection dialog always did, rather than showing raw transport text, and a failing stitch pass degrades to merging without the extra links instead of discarding the whole fetch. On a Neo4j 4.x server, expand and stitch used to run queries that silently matched nothing and reported "no neighbors"; they now say they need Neo4j 5 or newer. A scheme-less URL like `localhost:7474` is rejected with what to type instead of failing later as a network error, Enter submits the connection form, the dialog says that importing replaces the loaded graph, and both merge flows say up front that merging resets filter narrowing and the undo history. Stitching after an expand also scales with the new batch instead of re-scanning everything loaded so far. +* **Group fixes.** A group chip's *in several groups* ring was invisible (it referenced an undefined colour), a group action that failed died silently instead of reporting, and a partly-covered group row's tooltip described the state instead of what the click would do. +* **Smaller interface fixes.** Closing the workbench while it was expanded left the expansion set, so the next tab opened over the whole stage with the canvas gone. Undoing a slider tweak announced itself with the full-screen "Switching Workspace" overlay. Expanded cards in dark mode got a light-grey wash that made the Groups intro unreadable (1.49:1), and the card hover bar's white title measured 3.44:1 in dark; both are fixed contrast. Checking a filter category logged a bare "Showing", the delete-workspace confirmation still said "view", and the activity-log tooltip promised "every message this session" over a 200-line ring buffer. +* **Toasts are visible before a graph is loaded.** The toast layer sat below the landing page, so every failure that happens *before* there is a graph — a bad file, a failed STRING fetch, an empty gene field — left you on the landing page with no message at all. Toasts now sit above it, and above modal overlays. The stack also evicts the oldest *non-error* first, so four routine messages can no longer push a red one off screen. +* **A failed load no longer costs you the graph you had.** Opening a file destroyed the loaded graph *before* parsing the new one, so any parse failure left an empty stage with nothing to go back to; the file is parsed first now. Re-picking the same file after a failure works too (the input was never cleared, so the second attempt fired no event at all), and an unsupported extension gives one message instead of two contradictory ones. +* **⟳ Reset in the Data Editor really discards.** It restored the table but kept the pending edits, so the next **✔ Apply** replayed every cell change and row deletion you had just discarded into the rebuilt graph. +* **Dialogs say what they will destroy, and Cancel is the default.** Every confirmation focused **OK**, so Enter on one you had not read yet took the destructive branch — including *"Opening a file replaces the loaded graph, discarding every workspace, style and group"*. Cancel takes focus now and each button names its action (*Delete workspace*, *Discard and continue*, *Reload*, *Clear*, *Connect anyway*) instead of saying "OK". Deleting a workspace also names what goes with it — positions, styles, groups, notes — and that its undo history becomes unusable, which *"Are you sure?"* did not. Renaming a workspace starts from its current name rather than an empty box, and an empty name says why nothing happened. +* **Clicks and keystrokes that used to do nothing now answer.** Typing text that is not a number into a threshold box reverted on blur with no word; the greyed 🛢️ **Expand** returned in silence (its tooltip has to stay reachable, so the click does arrive); *Add to / Remove from Selection* in Metrics no-opped with nothing highlighted; the pie picker's greyed **Apply** gave no reason; **Empty all groups** reported *"Cleared all bubble groups"* over groups that were already empty. Each of them now says what it wants or what it did. The Notes and Groups overlay switches also became reachable enough to *explain* themselves — disabled by attribute, they were invisible to the tooltip layer that carries the only text saying why they cannot toggle. +* **The command palette opens the pane it acts on.** Running a workbench toolbar control (+ Node, ⤒ Import, ✔ Apply) from the palette clicked it with its tab still hidden, so the effect landed where you could not see it. +* **✗ Clear in the query editor is ✗ Clear text.** It emptied the box without re-applying, leaving the graph filtered by the query that was no longer on screen and the filter panel still locked. The label, its tooltip, the query help and the tour step now say what it does, and ⟳ **Sync** remains the control that hands filtering back to the panel. **🔍 Filter** and **🎯 Select** also swap their tooltips for *"Fix the highlighted syntax error first"* while the query does not parse, instead of promising to filter. +* **Notes say when something is lost.** Escape on a freshly placed note correctly discards it (above) — but silently, with no undo entry to get it back; it now says *"Note discarded."* the way abandoning the placement tool always did. Loading a workspace carrying more than 500 notes dropped the rest without a word, while the same feature warns when it trims a single note's text; it now reports how many did not load, and the limit. The ✎ Note tooltip also mentions that a single click opens the style popover, which is the only pointer route to *Delete note*. +* **Assistant: a cancelled or empty turn leaves nothing behind.** Cancelling the over-budget modal had already rendered your message as a bubble and cleared the input, so the question was lost, an orphan bubble stayed on screen and the budget meter went stale. Nothing is rendered until the turn actually goes out, and the question goes back in the box. A turn that streams no visible content says *(no reply)* instead of finalizing as a blank bubble — and no longer pushes an empty message into the history that is replayed on every later request. *Open in query editor* asks before replacing a hand-typed query (the sibling *Select* action always preserved it), and a backend error that merely mentions "model" — *"model requires more system memory"* — is no longer reported as a missing model with an `ollama pull` instruction. Generic failures carry the backend's own message rather than *"check the console"*, which a packaged desktop build does not have. +* **Neo4j: failures name their cause.** A non-401 HTTP status was reported as bare *"Neo4j returned HTTP 404."*; it now names the likely cause and, for a 404, the database it looked for and the Bolt-vs-HTTP port mistake. Cypher errors show the server's sentence rather than the driver constant in front of it, a timeout suggests a next step, and every terminal failure logs the underlying error for debugging. A transport failure is identified at the point it happens instead of being inferred from the error's JavaScript type — an ordinary bug in the merge path used to be reported to you as a network problem. The **Fetch** button also stays disabled across the size confirmation and the property checklist (those are separate dialogs, so a second concurrent fetch was one click away), a failed import no longer leaves the previous source's *"Live (API)"* label describing a Neo4j graph, the two required fields are marked as such, and the rail's 🛢️ button reads **Add** — matching its own tooltip and popup title, where "Join" collided with the Query tab and with the Excel merge's meaning of the word. +* **Groups: settings read back, and a workspace switch cleans up after itself.** Switching workspaces left the outgoing workspace's bubbles painted on the canvas — refitted to the new positions, with nothing in the Groups list to explain them. *Avoid other nodes* was refused outright above a graph-size threshold; it is priced per group now, so a small group on a large graph gets the routing it can afford, with a confirmation when it would actually be slow. The fill- and stroke-opacity sliders never re-read their stored values, so a load or a ✨ Re-tune left them showing the old numbers. The 🧩 Auto-detect configurator answers Escape, reports its open state to assistive technology and closes when the panel scrolls, like every other menu. +* **Metrics: an unscorable view is a result.** A recompute that produced no values — one visible node after filtering — left the previous metric's numbers on screen under the new metric's name. Opening Metrics with no graph loaded also flashed "Calculating" and then showed empty panels with no explanation. +* **Accessibility.** Workbench tabs report their pressed state; the palette's match count and the filter search's empty note announce themselves; group membership rows report checked / partial / unchecked; the boolean Any/True/False segment gained the group role and pressed state its siblings had. Modal dialogs trap Tab instead of letting focus escape to the page behind, closing a rail menu by picking an item returns focus to its button (not only on Escape), and editing a group no longer discards focus when the list rebuilds. Six dark-theme contrast failures in the assistant's budget modal, the note delete button and the rail's highlighted tab are on theme tokens now, as is the selection chip's warning text; the guided tour's step previews were 63 hard-coded light-theme colours inside a themed popup, one of them at 1.5:1. +* **Security and privacy.** The bundled service validates the `Host` header against its bind address, so a page you visit cannot reach your loopback-bound graph by DNS rebinding. Sending Neo4j credentials over plain `http:` to anything but your own machine now asks first — base64 Basic auth is encoding, not encryption. Executed Cypher (which carries literal values from your data) is kept out of the context sent to the assistant's LLM endpoint. Imported text — labels, ids, descriptions, property names and values — is escaped in the hover tooltips and the colour and numeric scale pickers, closing three injection paths from a crafted file. Packaged desktop and single-file builds now carry the same `Content-Security-Policy` the served build does, the release workflow builds from the lockfile with pinned actions, and a tracked bibliography no longer leaks a local home path. +* **Smaller fixes.** The rail's **Hover** toggle painted "off" in the app's loudest red and "on" in green, so off shouted louder than on; it uses the same `.active` treatment as its neighbours. Hovering the tour's **← Back** button turned it brand red — the destructive colour. The shared modal button had no disabled style, so a dead **Fetch** or **Save** looked live and lit up on hover. A disabled command-palette row could not be clicked with the mouse while the keyboard reached it. Filter-range focus rings used a hard-coded light-theme value that barely showed in dark. The rail's controls shared one corner radius, and the 15 half-pixel font sizes this release introduced were rounded onto whole pixels. + +* **Security fixes.** Imported column headers and element ids are escaped before reaching the Data Editor's table, closing two injection paths from a crafted file. An imported Excel header can no longer be named `__proto__`, `constructor` or `prototype` and have its values land on an object's prototype — those columns are refused with a warning naming them. The bundled service sends a baseline `Content-Security-Policy` and `X-Frame-Options: DENY` with its HTML. `SECURITY.md` now states plainly that its reads are unauthenticated by design and that a `session` id is a routing key rather than a secret. Dependency CVEs were cleared where a patched version exists in range (node-tar, lodash, DOMPurify, exceljs's uuid, and an unused `inline-source-cli` dropped). The desktop shell moves from **Electron 36 to Electron 43** (Chromium 150), which is what it took to clear Electron's own advisories — renderer use-after-frees, command-line switch injection, and `window.open` scoping — none of them reachable by an in-range update. The app code needed no changes; the one user-visible consequence is that the macOS build now requires **macOS Monterey (12) or newer** instead of Big Sur (11). Windows 10+ and Linux are unchanged. + ## 1.16.1 — 2026-07-22 ### Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e35bd17..4aede4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ dnf install libxcrypt-compat wine # only needed for cross-platform builds | `npm run bundle:serve` | Dev server with watch + sourcemaps (recommended for development) | | `npm run serve` | Static http-server on :8000 | | `npm run serve:api` | Standalone HTTP ingest service with live SSE viewer (see [SERVICE.md](SERVICE.md)) | -| `npm start` | Electron app (injects version automatically) | +| `npm start` | Electron app (injects version automatically; the first run after a fresh `npm install` downloads the ~110 MB Electron runtime, since Electron 43 no longer fetches it at install time) | | `npm run bundle` | One-off bundle (no minify) | | `npm run bundle:prod` | Production bundle (minified) | | `npm test` | Run the vitest unit test suite | diff --git a/README.md b/README.md index c09c9c7..ee16801 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Then: | Launch screen | Query editor | Selection tools and bubble sets | |:-----------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------:| -| Open Excel or JSON files, explore demo networks, or take a tour; zero install with portable versions | Write expressive queries with boolean logic, nested conditions, and range operators to filter your graph | Lasso select elements, undo and redo, focus and expand neighborhoods, and group nodes visually | +| Open Excel or JSON files, fetch a graph straight from a Neo4j server, explore demo networks, or take a tour; zero install with portable versions | Write expressive queries with boolean logic, nested conditions, and range operators to filter your graph | Lasso select elements, undo and redo, focus and expand neighborhoods, and group nodes visually | | Graph canvas with filters | Network metrics and data editor | Styling panel | | Filter by any property using range sliders and dropdown checklists, inspect node and edge metadata via tooltips, and navigate large graphs with a minimap | Compute centrality metrics like degree, betweenness, closeness, eigenvector, and PageRank, and edit your graph data live in a built-in spreadsheet | Customize shapes, sizes, colors, labels, halos, badges, arrows, pie-chart nodes, animated edge flow, and bubble set appearance per element, with an optional density-heatmap overlay | | Workspace management | Graph Assistant | Numerical color mapping | diff --git a/SECURITY.md b/SECURITY.md index c3872ca..7e1cf4e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,8 +7,8 @@ applied to the latest minor release line; please upgrade before reporting. | Version | Supported | | ------- | ------------------ | -| 1.15.x | :white_check_mark: | -| < 1.15 | :x: | +| 1.17.x | :white_check_mark: | +| < 1.17 | :x: | ## Reporting a vulnerability @@ -48,6 +48,15 @@ Graph Lens Lite has two attack surfaces worth calling out: a firewall or reverse proxy. Auth bypass, request-smuggling, path traversal, and denial-of-service against this endpoint are in scope. + Two properties of that service are by design, not oversights. **Reads are + unauthenticated:** `GET /api/graph` and `GET /api/events` serve whatever the + last push stored, so the bearer token gates writing, not viewing. And a + **`session` id is a routing key, not a secret** — it is a plain query-string + parameter, it appears in logs and browser history, and knowing one is enough + to read that session's graph. Off loopback, put authentication in front of + the service (reverse proxy) rather than relying on session ids being hard to + guess. + Out of scope: issues that require an already-compromised host, social engineering, or attacks that depend on the user disabling the documented loopback default without further protection. diff --git a/install-service.sh b/install-service.sh index 5715f60..e5dea38 100755 --- a/install-service.sh +++ b/install-service.sh @@ -50,7 +50,9 @@ Environment=PATH=${NODE_DIR}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/ NoNewPrivileges=true ProtectSystem=full -ReadWritePaths=${WORKING_DIR} +# No ReadWritePaths: the service holds its graph in memory and never writes to +# disk, so granting write over the whole checkout only widens what a +# compromise of it could reach. PrivateTmp=true [Install] diff --git a/manuscript/reference.bib b/manuscript/reference.bib index 6ad8df2..107034f 100644 --- a/manuscript/reference.bib +++ b/manuscript/reference.bib @@ -35,7 +35,6 @@ @article{bastianGephiOpenSource2009 copyright = {Copyright (c) 2021 Proceedings of the International AAAI Conference on Web and Social Media}, langid = {english}, keywords = {webatlas}, - file = {/home/glory/Zotero/storage/PQCTQ2HA/Bastian et al. - 2009 - Gephi An Open Source Software for Exploring and Manipulating Networks.pdf} } @article{bavelasCommunicationPatternsTaskOriented1950a, @@ -88,7 +87,6 @@ @ai.utoronto.ca pmcid = {PMC2788933}, pmid = {19837718}, keywords = {Computational Biology,Computer Graphics,Database Management Systems,Software,User-Computer Interface}, - file = {/home/glory/Zotero/storage/P37JCY7I/Brown et al. - 2009 - NAViGaTOR Network Analysis, Visualization and Graphing Toronto.pdf} } @article{bunielloOpenTargetsPlatform2025, @@ -107,7 +105,6 @@ @article{bunielloOpenTargetsPlatform2025 pmcid = {PMC11701534}, urldate = {2025-12-24}, abstract = {The Open Targets Platform (https://platform.opentargets.org) is a unique, open-source, publicly-available knowledge base providing data and tooling for systematic drug target identification, annotation, and prioritisation. Since our last report, we have expanded the scope of the Platform through a number of significant enhancements and data updates, with the aim to enable our users to formulate more flexible and impactful therapeutic hypotheses. In this context, we have completely revamped our target--disease associations page with more interactive facets and built-in functionalities to empower users with additional control over their experience using the Platform, and added a new Target Prioritisation view. This enables users to prioritise targets based upon clinical precedence, tractability, doability and safety attributes. We have also implemented a direction of effect assessment for eight sources of target--disease association evidence, showing the effect of genetic variation on the function of a target is associated with risk or protection for a trait to inform on potential mechanisms of modulation suitable for disease treatment. These enhancements and the introduction of new back and front-end technologies to support them have increased the impact and usability of our resource within the drug discovery community.}, - file = {/home/glory/Zotero/storage/TCEG6G92/Buniello et al. - 2025 - Open Targets Platform facilitating therapeutic hypotheses building in drug discovery.pdf;/home/glory/Zotero/storage/KR9GR2NZ/gkae1128.html} } @misc{claptonSheetJS, @@ -117,7 +114,6 @@ @misc{claptonSheetJS abstract = {SheetJS Tools for Excel Spreadsheets}, howpublished = {https://sheetjs.com/}, langid = {american}, - file = {/home/glory/Zotero/storage/7HARSRR3/sheetjs.com.html} } @inproceedings{dangBioLinkerBottomupExploration2017, @@ -133,7 +129,6 @@ @inproceedings{dangBioLinkerBottomupExploration2017 urldate = {2025-05-14}, abstract = {Systems biologists and cancer researchers require interactive visualization tools that enable them to more easily navigate and discover patterns at different levels of the biological hierarchy of signaling pathways. Furthermore, biologists are often interested in understanding and exploring the causal biochemical links between processes. When exploring the literature of particular biological pathways or specific proteins within those pathways, biologists find it useful to know the contexts in which biochemical links are active and, importantly, to be aware of potential conflicts when different experiments introduce alternative interpretations of the function of a pathway or biochemical reaction. We introduce BioLinker, a interactive visualization system that helps users to perform bottom-up exploration of complex protein interaction networks. Five interconnected views provide the user with a range of ways to explore pathway data, including views that show potential conflicts within pathway databases and publications and that highlight contextual information about individual proteins. Additionally, we discuss system details to show how our system manages the large amount of protein interactions extracted from the literature of biological pathways.}, keywords = {Cancer,Data visualization,Dynamic network visualization,Genomics,Indexes,Potential conflict matrix,Protein interaction network,Proteins}, - file = {/home/glory/Zotero/storage/ZR9NIPZ6/8031603.html} } @article{duPINA30Mining2021, @@ -152,7 +147,6 @@ @article{duPINA30Mining2021 pmcid = {PMC7779002}, urldate = {2025-05-14}, abstract = {Protein--protein interactions (PPIs) are crucial to mediate biological functions, and understanding PPIs in cancer type-specific context could help decipher the underlying molecular mechanisms of tumorigenesis and identify potential therapeutic options. Therefore, we update the Protein Interaction Network Analysis (PINA) platform to version 3.0, to integrate the unified human interactome with RNA-seq transcriptomes and mass spectrometry-based proteomes across tens of cancer types. A number of new analytical utilities were developed to help characterize the cancer context for a PPI network, which includes inferring proteins with expression specificity and identifying candidate prognosis biomarkers, putative cancer drivers, and therapeutic targets for a specific cancer type; as well as identifying pairs of co-expressing interacting proteins across cancer types. Furthermore, a brand-new web interface has been designed to integrate these new utilities within an interactive network visualization environment, which allows users to quickly and comprehensively investigate the roles of human interacting proteins in a cancer type-specific context. PINA is freely available at https://omics.bjcancer.org/pina/.}, - file = {/home/glory/Zotero/storage/77F9IC72/Du et al. - 2021 - PINA 3.0 mining cancer interactome.pdf;/home/glory/Zotero/storage/K47UL6QQ/5999889.html} } @article{ehlersIntroductionSurveyBiological2025, @@ -185,7 +179,6 @@ @article{evgeniouMetaAnalysisHumanTranscriptomics2021 urldate = {2026-02-20}, abstract = {Peritoneal dialysis (PD) is one therapeutic option for patients with end-stage kidney disease (ESKD). Molecular profiling of samples from PD patients using different Omics technologies has led to the discovery of dysregulated molecular processes due to PD treatment in recent years. In particular, a number of transcriptomics (TX) datasets are currently available in the public domain in the context of PD. We set out to perform a meta-analysis of TX datasets to identify dysregulated receptor-ligand interactions in the context of PD-associated complications. We consolidated transcriptomics profiles from twelve untargeted genome-wide gene expression studies focusing on human cell cultures or samples from human PD patients. Gene set enrichment analysis was used to identify enriched biological processes. Receptor-ligand interactions were identified using data from CellPhoneDB. We identified 2591 unique differentially expressed genes in the twelve PD studies. Key enriched biological processes included angiogenesis, cell adhesion, extracellular matrix organization, and inflammatory response. We identified 70 receptor-ligand interaction pairs, with both interaction partners being dysregulated on the transcriptional level in one of the investigated tissues in the context of PD. Novel receptor-ligand interactions without prior annotation in the context of PD included BMPR2-GDF6, FZD4-WNT7B, ACKR2-CCL2, or the binding of EPGN and EREG to the EGFR, as well as the binding of SEMA6D to the receptors KDR and TYROBP. In summary, we have consolidated human transcriptomics datasets from twelve studies in the context of PD and identified sets of novel receptor-ligand pairs being dysregulated in the context of PD that warrant investigation in future functional studies.}, langid = {english}, - file = {/home/glory/Zotero/storage/BCTWGGFG/Evgeniou et al. - 2021 - A Meta-Analysis of Human Transcriptomics Data in the Context of Peritoneal Dialysis Identifies Novel.pdf} } @article{formicaMolecularPathwaysInvolved2020, @@ -201,7 +194,6 @@ @article{formicaMolecularPathwaysInvolved2020 pmid = {32320858}, urldate = {2026-02-20}, langid = {english}, - file = {/home/glory/Zotero/storage/VCQQG5LJ/Formica and Peters - 2020 - Molecular pathways involved in injury-repair and ADPKD progression.pdf} } @article{freemanGraphiaPlatformGraphbased2022, @@ -222,7 +214,6 @@ @article{freemanGraphiaPlatformGraphbased2022 abstract = {Graphia is an open-source platform created for the graph-based analysis of the huge amounts of quantitative and qualitative data currently being generated from the study of genomes, genes, proteins metabolites and cells. Core to Graphia's functionality is support for the calculation of correlation matrices from any tabular matrix of continuous or discrete values, whereupon the software is designed to rapidly visualise the often very large graphs that result in 2D or 3D space. Following graph construction, an extensive range of measurement algorithms, routines for graph transformation, and options for the visualisation of node and edge attributes are available, for graph exploration and analysis. Combined, these provide a powerful solution for the interpretation of high-dimensional data from many sources, or data already in the form of a network or equivalent adjacency matrix. Several use cases of Graphia are described, to showcase its wide range of applications in the analysis biological data. Graphia runs on all major desktop operating systems, is extensible through the deployment of plugins and is freely available to download from https://graphia.app/., Graphia is a new visual analytics platform specifically created for the network-based analysis of large and complex data, such as that generated in huge amounts by modern biological analyses. It works in a data agnostic, hypothesis-free manner to generate correlation networks from any table of numerical or discrete values, thereafter providing a means to rapidly visualise the often very large networks that result, in either 2D or 3D space. Following network construction, the tool offers an extensive range of analysis algorithms, routines for network transformation, and options for the visualisation of metadata. This provides a powerful analysis solution for the exploration and interpretation of high-dimensional data from any source, as well as any data already defined as a network. Several use cases of Graphia are described to showcase its wide range of applications in the analysis biological data. Graphia is open source and free to all.}, pmcid = {PMC9352203}, pmid = {35877685}, - file = {/home/glory/Zotero/storage/TQIDVB9B/Freeman et al. - 2022 - Graphia A platform for the graph-based visualisation and analysis of high dimensional data.pdf} } @article{freemanSetMeasuresCentrality1977a, @@ -254,7 +245,6 @@ @article{gebeshuberComputationalDrugRepositioning2023 pmid = {37059330}, urldate = {2025-05-14}, abstract = {Focal segmental glomerulosclerosis (FSGS) is a glomerular lesion often associated with nephrotic syndrome. It is also associated with a high risk of progression to end-stage kidney disease. Current treatment of FSGS is limited to systemic corticosteroids or calcineurin inhibition, along with inhibitors of the renin-angiotensin-aldosterone system. FSGS is heterogeneous in etiology, and novel therapies targeting specific, dysregulated molecular pathways represent a major unmet medical need. We have generated a network-based molecular model of FSGS pathophysiology using previously established systems biology workflows to allow computational evaluation of compounds for their predicted interference with molecular processes contributing to FSGS. We identified the anti-platelet drug clopidogrel as a therapeutic option to counterbalance dysregulated FSGS pathways. This prediction of our computational screen was validated by testing clopidogrel in the adriamycin FSGS mouse model. Clopidogrel improved key FSGS outcome parameters and significantly reduced urinary albumin to creatinine ratio (P {$<$} 0.01) and weight loss (P {$<$} 0.01), and ameliorated histopathological damage (P {$<$} 0.05). Clopidogrel is used to treat several cardiovascular diseases linked to chronic kidney disease. Clopidogrel's favorable safety profile and its efficacy in the adriamycin mouse FSGS model thus recommend it as an attractive drug repositioning candidate for clinical trial in FSGS.}, - file = {/home/glory/Zotero/storage/AVVUGEEP/Gebeshuber et al. - 2023 - Computational drug repositioning of clopidogrel as a novel therapeutic option for focal segmental gl.pdf;/home/glory/Zotero/storage/LW7PX9M2/S1931524423000579.html} } @misc{jacomyGephiGephilite2025, @@ -298,7 +288,6 @@ @article{karlssonSingleCellType2021 abstract = {Single-cell RNA analysis has been integrated with spatial protein profiling to create a single--cell type map of human tissues. , Advances in molecular profiling have opened up the possibility to map the expression of genes in cells, tissues, and organs in the human body. Here, we combined single-cell transcriptomics analysis with spatial antibody-based protein profiling to create a high-resolution single--cell type map of human tissues. An open access atlas has been launched to allow researchers to explore the expression of human protein-coding genes in 192 individual cell type clusters. An expression specificity classification was performed to determine the number of genes elevated in each cell type, allowing comparisons with bulk transcriptomics data. The analysis highlights distinct expression clusters corresponding to cell types sharing similar functions, both within the same organs and between organs.}, copyright = {https://creativecommons.org/licenses/by-nc/4.0/}, langid = {english}, - file = {/home/glory/Zotero/storage/9I9MPFD9/Karlsson et al. - 2021 - A single–cell type transcriptomics map of human tissues.pdf} } @article{liuPaintOmics4New2022, @@ -317,7 +306,6 @@ @article{liuPaintOmics4New2022 pmcid = {PMC9252773}, urldate = {2025-05-14}, abstract = {PaintOmics is a web server for the integrative analysis and visualisation of multi-omics datasets using biological pathway maps. PaintOmics 4 has several notable updates that improve and extend analyses. Three pathway databases are now supported: KEGG, Reactome and MapMan, providing more comprehensive pathway knowledge for animals and plants. New metabolite analysis methods fill gaps in traditional pathway-based enrichment methods. The metabolite hub analysis selects compounds with a high number of significant genes in their neighbouring network, suggesting regulation by gene expression changes. The metabolite class activity analysis tests the hypothesis that a metabolic class has a higher-than-expected proportion of significant elements, indicating that these compounds are regulated in the experiment. Finally, PaintOmics 4 includes a regulatory omics module to analyse the contribution of trans-regulatory layers (microRNA and transcription factors, RNA-binding proteins) to regulate pathways. We show the performance of PaintOmics 4 on both mouse and plant data to highlight how these new analysis features provide novel insights into regulatory biology. PaintOmics 4 is available at https://paintomics.org/.}, - file = {/home/glory/Zotero/storage/45Q2VGT4/Liu et al. - 2022 - PaintOmics 4 new tools for the integrative analysis of multi-omics datasets supported by multiple p.pdf;/home/glory/Zotero/storage/9MTJAQDX/6591534.html} } @article{milacicReactomePathwayKnowledgebase2024, @@ -335,7 +323,6 @@ @article{milacicReactomePathwayKnowledgebase2024 pmcid = {PMC10767911}, urldate = {2025-12-22}, abstract = {The Reactome Knowledgebase (https://reactome.org), an Elixir and GCBR core biological data resource, provides manually curated molecular details of a broad range of normal and disease-related biological processes. Processes are annotated as an ordered network of molecular transformations in a single consistent data model. Reactome thus functions both as a digital archive of manually curated human biological processes and as a tool for discovering functional relationships in data such as gene expression profiles or somatic mutation catalogs from tumor cells. Here we review progress towards annotation of the entire human proteome, targeted annotation of disease-causing genetic variants of proteins and of small-molecule drugs in a pathway context, and towards supporting explicit annotation of cell- and tissue-specific pathways. Finally, we briefly discuss issues involved in making Reactome more fully interoperable with other related resources such as the Gene Ontology and maintaining the resulting community resource network.}, - file = {/home/glory/Zotero/storage/5C5XHSGM/Milacic et al. - 2024 - The Reactome Pathway Knowledgebase 2024.pdf;/home/glory/Zotero/storage/2T6J848L/gkad1025.html} } @article{mutoDefiningCellularComplexity2022, @@ -354,7 +341,6 @@ @article{mutoDefiningCellularComplexity2022 urldate = {2026-02-20}, abstract = {Abstract Autosomal dominant polycystic kidney disease (ADPKD) is the leading genetic cause of end stage renal disease characterized by progressive expansion of kidney cysts. To better understand the cell types and states driving ADPKD progression, we analyze eight ADPKD and five healthy human kidney samples, generating single cell multiomic atlas consisting of \textasciitilde 100,000 single nucleus transcriptomes and \textasciitilde 50,000 single nucleus epigenomes. Activation of proinflammatory, profibrotic signaling pathways are driven by proximal tubular cells with a failed repair transcriptomic signature, proinflammatory fibroblasts and collecting duct cells. We identify GPRC5A as a marker for cyst-lining collecting duct cells that exhibits increased transcription factor binding motif availability for NF-{$\kappa$}B, TEAD, CREB and retinoic acid receptors. We identify and validate a distal enhancer regulating GPRC5A expression containing these motifs. This single cell multiomic analysis of human ADPKD reveals previously unrecognized cellular heterogeneity and provides a foundation to develop better diagnostic and therapeutic approaches.}, langid = {english}, - file = {/home/glory/Zotero/storage/H4AM7NVE/Muto et al. - 2022 - Defining cellular complexity in human autosomal dominant polycystic kidney disease by multimodal sin.pdf} } @inbook{newmanMathematicsNetworks2018a, @@ -410,7 +396,6 @@ @article{orchardMIntActProjectIntAct2014 urldate = {2026-02-20}, copyright = {http://creativecommons.org/licenses/by/3.0/}, langid = {english}, - file = {/home/glory/Zotero/storage/UQDU7SD4/Orchard et al. - 2014 - The MIntAct project—IntAct as a common curation platform for 11 molecular interaction databases.pdf} } @article{oughtredBioGRIDDatabaseComprehensive2021, @@ -432,7 +417,6 @@ @article{oughtredBioGRIDDatabaseComprehensive2021 pmcid = {PMC7737760}, pmid = {33070389}, keywords = {Animals,biological network,chemical interaction,COVID-19,CRISPR screen,Databases Factual,drug target,genetic interaction,Humans,Mice,phenotype,post-translational modification,protein interaction,Protein Interaction Mapping,Proteins,SARS-CoV-2,ubiquitin-proteasome system,User-Computer Interface}, - file = {/home/glory/Zotero/storage/RK4HX9GR/Oughtred et al. - 2021 - The BioGRID database A comprehensive biomedical resource of curated protein, genetic, and chemical.pdf} } @article{shannonCytoscapeSoftwareEnvironment2003, @@ -454,7 +438,6 @@ @article{shannonCytoscapeSoftwareEnvironment2003 abstract = {Cytoscape is an open source software project for integrating biomolecular interaction networks with high-throughput expression data and other molecular states into a unified conceptual framework. Although applicable to any system of molecular components and interactions, Cytoscape is most powerful when used in conjunction with large databases of protein-protein, protein-DNA, and genetic interactions that are increasingly available for humans and model organisms. Cytoscape's software Core provides basic functionality to layout and query the network; to visually integrate the network with expression profiles, phenotypes, and other molecular states; and to link the network to databases of functional annotations. The Core is extensible through a straightforward plug-in architecture, allowing rapid development of additional computational analyses and features. Several case studies of Cytoscape plug-ins are surveyed, including a search for interaction pathways correlating with changes in gene expression, a study of protein complexes involved in cellular recovery to DNA damage, inference of a combined physical/functional interaction network for Halobacterium, and an interface to detailed stochastic/kinetic gene regulatory models.}, langid = {english}, pmid = {14597658}, - file = {/home/glory/Zotero/storage/N9GIV84C/Shannon et al. - 2003 - Cytoscape A Software Environment for Integrated Models of Biomolecular Interaction Networks.pdf} } @article{songSystemsBiologyAutosomal2009, @@ -489,7 +472,6 @@ @article{stanleySecondaryAnalysisConcurrent2024 pmcid = {PMC11405464}, urldate = {2026-06-29}, langid = {english}, - file = {/home/glory/Zotero/storage/X9J259RY/Stanley et al. - 2024 - A secondary analysis of concurrent use of metformin and tolvaptan in ADPKD tolvaptan trials.pdf} } @article{szklarczykSTRINGDatabase20252025, @@ -508,7 +490,6 @@ @article{szklarczykSTRINGDatabase20252025 pmcid = {PMC11701646}, urldate = {2025-05-14}, abstract = {Proteins cooperate, regulate and bind each other to achieve their functions. Understanding the complex network of their interactions is essential for a systems-level description of cellular processes. The STRING database compiles, scores and integrates protein--protein association information drawn from experimental assays, computational predictions and prior knowledge. Its goal is to create comprehensive and objective global networks that encompass both physical and functional interactions. Additionally, STRING provides supplementary tools such as network clustering and pathway enrichment analysis. The latest version, STRING 12.5, introduces a new `regulatory network', for which it gathers evidence on the type and directionality of interactions using curated pathway databases and a fine-tuned language model parsing the literature. This update enables users to visualize and access three distinct network types---functional, physical and regulatory---separately, each applicable to distinct research needs. In addition, the pathway enrichment detection functionality has been updated, with better false discovery rate corrections, redundancy filtering~and improved visual displays. The resource now also offers improved annotations of clustered networks and provides users with downloadable network embeddings, which facilitate the use of STRING networks in machine learning and allow cross-species transfer of protein information. The STRING database is available online at https://string-db.org/.}, - file = {/home/glory/Zotero/storage/H9QB4EM4/Szklarczyk et al. - 2025 - The STRING database in 2025 protein networks with directionality of regulation.pdf;/home/glory/Zotero/storage/2FMHRD87/7903368.html} } @misc{teamQwen35OmniTechnicalReport2026, @@ -526,7 +507,6 @@ @misc{teamQwen35OmniTechnicalReport2026 abstract = {In this work, we present Qwen3.5-Omni, the latest advancement in the Qwen-Omni model family. Representing a significant evolution over its predecessor, Qwen3.5-Omni scales to hundreds of billions of parameters and supports a 256k context length. By leveraging a massive dataset comprising heterogeneous text-vision pairs and over 100 million hours of audio-visual content, the model demonstrates robust omni-modality capabilities. Qwen3.5-Omni-plus achieves SOTA results across 215 audio and audio-visual understanding, reasoning, and interaction subtasks and benchmarks, surpassing Gemini-3.1 Pro in key audio tasks and matching it in comprehensive audio-visual understanding. Architecturally, Qwen3.5-Omni employs a Hybrid Attention Mixture-of-Experts (MoE) framework for both Thinker and Talker, enabling efficient long-sequence inference. The model facilitates sophisticated interaction, supporting over 10 hours of audio understanding and 400 seconds of 720P video (at 1 FPS). To address the inherent instability and unnaturalness in streaming speech synthesis, often caused by encoding efficiency discrepancies between text and speech tokenizers, we introduce ARIA. ARIA dynamically aligns text and speech units, significantly enhancing the stability and prosody of conversational speech with minimal latency impact. Furthermore, Qwen3.5-Omni expands linguistic boundaries, supporting multilingual understanding and speech generation across 10 languages with human-like emotional nuance. Finally, Qwen3.5-Omni exhibits superior audio-visual grounding capabilities, generating script-level structured captions with precise temporal synchronization and automated scene segmentation. Remarkably, we observed the emergence of a new capability in omnimodal models: directly performing coding based on audio-visual instructions, which we call Audio-Visual Vibe Coding.}, archiveprefix = {arXiv}, keywords = {Computer Science - Computation and Language,Electrical Engineering and Systems Science - Audio and Speech Processing}, - file = {/home/glory/Zotero/storage/VLJX4DAX/Qwen Team - 2026 - Qwen3.5-Omni Technical Report.pdf;/home/glory/Zotero/storage/UI79QTGZ/2604.html} } @article{valenzuela-escarcegaLargescaleAutomatedMachine2018, @@ -544,7 +524,6 @@ @article{valenzuela-escarcegaLargescaleAutomatedMachine2018 urldate = {2025-05-26}, abstract = {PubMed, a repository and search engine for biomedical literature, now indexes \>1 million articles each year. This exceeds the processing capacity of human domain experts, limiting our ability to truly understand many diseases. We present Reach, a system for automated, large-scale machine reading of biomedical papers that can extract mechanistic descriptions of biological processes with relatively high precision at high throughput. We demonstrate that combining the extracted pathway fragments with existing biological data analysis algorithms that rely on curated models helps identify and explain a large number of previously unidentified mutually exclusive altered signaling pathways in seven different cancer types. This work shows that combining human-curated `big mechanisms' with extracted `big data' can lead to a causal, predictive understanding of cellular processes and unlock important downstream applications.}, keywords = {REACH;biolinker}, - file = {/home/glory/Zotero/storage/FNPHT96G/Valenzuela-Escárcega et al. - 2018 - Large-scale automated machine reading discovers new cancer-driving mechanisms.pdf;/home/glory/Zotero/storage/GDVUL6L6/5107029.html} } @article{wangG6WebbasedLibrary2021, @@ -562,7 +541,6 @@ @article{wangG6WebbasedLibrary2021 urldate = {2025-05-23}, abstract = {Authoring graph visualization poses great challenges to developers due to its high requirements on both domain knowledge and development skills. Although existing libraries and tools reduce the difficulty of generating graph visualization, there are still many challenges. We work closely with developers and formulate several design goals, then design and implement G6, a web-based library for graph visualization. It combines template-based configuration for high usability and flexible customization for high expressiveness. To enhance development efficiency, G6 proposes a range of optimizations, including state management and interaction modes. We demonstrate its capabilities through an extensive gallery, a quantitative performance evaluation, and an expert interview. G6 was first released in 2017 and has been iterated for 317 versions. It has served as a web-based library for thousands of applications and received 8312 stars on GitHub.}, keywords = {Graph visualization,Node-link diagram,Visualization library,Web-based visualization}, - file = {/home/glory/Zotero/storage/FJ9HZAYK/S2468502X21000619.html} } @article{zhouOmicsNet20Webbased2022, @@ -581,5 +559,4 @@ @article{zhouOmicsNet20Webbased2022 pmcid = {PMC9252810}, urldate = {2025-05-14}, abstract = {Researchers are increasingly seeking to interpret molecular data within a multi-omics context to gain a more comprehensive picture of their study system. OmicsNet (www.omicsnet.ca) is a web-based tool developed to allow users to easily build, visualize, and analyze multi-omics networks to study rich relationships among lists of `omics features of interest. Three major improvements have been introduced in OmicsNet 2.0, which include: (i) enhanced network visual analytics with eleven 2D graph layout options and a novel 3D module layout; (ii) support for three new `omics types: single nucleotide polymorphism (SNP) list from genetic variation studies; taxon list from microbiome profiling studies, as well as liquid chromatography--mass spectrometry (LC--MS) peaks from~untargeted metabolomics;~and (iii) measures to improve research reproducibility by coupling R command history with the release of the companion OmicsNetR package, and generation of persistent links to share interactive network views. We performed a case study using the multi-omics data obtained from a recent large-scale investigation~on inflammatory bowel disease (IBD) and demonstrated that OmicsNet was able to quickly create meaningful multi-omics context to facilitate hypothesis generation and mechanistic insights.}, - file = {/home/glory/Zotero/storage/V6KTIYDY/Zhou et al. - 2022 - OmicsNet 2.0 a web-based platform for multi-omics integration and network visual analytics.pdf;/home/glory/Zotero/storage/WUPC57XE/6593602.html} } diff --git a/package-lock.json b/package-lock.json index 945561c..59e5cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graph-lens-lite", - "version": "1.15.4", + "version": "1.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "graph-lens-lite", - "version": "1.15.4", + "version": "1.17.0", "license": "MIT", "dependencies": { "@antv/layout": "^2.0.0", @@ -32,7 +32,7 @@ "devDependencies": { "@eslint/js": "^9.39.4", "@vitest/coverage-v8": "^4.1.9", - "electron": "^36.3.1", + "electron": "^43.3.0", "electron-builder": "^26.0.12", "esbuild": "^0.25.11", "eslint": "^9.39.4", @@ -40,11 +40,9 @@ "exceljs": "^4.4.0", "globals": "^17.6.0", "inline-source": "^8.0.3", - "inline-source-cli": "^1.2.0", "jsdom": "^29.0.2", "playwright": "^1.60.0", "prettier": "^3.8.4", - "rimraf": "^6.0.1", "vitest": "^4.0.18" } }, @@ -381,28 +379,20 @@ "node": ">=20.19.0" } }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", + "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=22.12.0" } }, "node_modules/@electron/asar": { - "version": "3.2.18", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.18.tgz", - "integrity": "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, "license": "MIT", "dependencies": { @@ -418,9 +408,9 @@ } }, "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -429,9 +419,9 @@ } }, "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -473,9 +463,9 @@ } }, "node_modules/@electron/fuses/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -496,97 +486,37 @@ } }, "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", + "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", "progress": "^2.0.3", - "semver": "^6.2.0", + "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "engines": { - "node": ">=12" + "node": ">=22.12.0" }, "optionalDependencies": { - "global-agent": "^3.0.0" + "undici": "^7.24.4" } }, - "node_modules/@electron/node-gyp": { - "version": "10.2.0-electron.1", - "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", - "integrity": "sha512-7o2pP1DFdFgkNv5CxE4m3iLsf8Xa4pus2eD/yzjdHFzEuZMXOLv4nRpJEAvaLtbkRJHsuhRueIvVa4NFx78UgQ==", + "node_modules/@electron/get/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", "dev": true, "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^8.1.0", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.2.1", - "nopt": "^6.0.0", - "proc-log": "^2.0.1", - "semver": "^7.3.5", - "tar": "^6.2.1", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/@electron/node-gyp/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, "engines": { - "node": ">=12" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/node-gyp/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/node-gyp/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@electron/notarize": { @@ -621,9 +551,9 @@ } }, "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -644,9 +574,9 @@ } }, "node_modules/@electron/osx-sign": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", - "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -694,9 +624,9 @@ } }, "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -717,93 +647,34 @@ } }, "node_modules/@electron/rebuild": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz", - "integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", "dev": true, "license": "MIT", "dependencies": { - "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "node-abi": "^3.45.0", - "node-api-version": "^0.2.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/@electron/rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/rebuild/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/rebuild/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" + "node": ">=22.12.0" } }, "node_modules/@electron/universal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", - "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", "dev": true, "license": "MIT", "dependencies": { - "@electron/asar": "^3.2.7", + "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", @@ -816,9 +687,9 @@ } }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, "license": "MIT", "dependencies": { @@ -831,9 +702,9 @@ } }, "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -844,13 +715,13 @@ } }, "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -892,9 +763,9 @@ } }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, "license": "MIT", "optional": true, @@ -909,9 +780,9 @@ } }, "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "optional": true, @@ -1469,9 +1340,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1543,9 +1414,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1679,13 +1550,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1752,107 +1616,17 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "minipass": "^7.0.4" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1975,9 +1749,9 @@ } }, "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2016,84 +1790,79 @@ "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, + "license": "MIT", "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", "dev": true, "license": "MIT", "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" } }, - "node_modules/@npmcli/move-file/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "tslib": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=8.0.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "dependencies": { + "tslib": "^2.8.1" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, "engines": { - "node": ">=14" + "node": ">=14.18.0" } }, "node_modules/@rolldown/binding-android-arm64": { @@ -2450,26 +2219,6 @@ "node": ">=10" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -2506,9 +2255,9 @@ } }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "dev": true, "license": "MIT", "dependencies": { @@ -2571,25 +2320,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", - "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" + "undici-types": "~7.18.0" } }, "node_modules/@types/responselike": { @@ -2609,25 +2346,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@vitest/coverage-v8": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", @@ -2746,9 +2464,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -2761,19 +2479,15 @@ "integrity": "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==", "license": "MIT" }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, "node_modules/acorn": { "version": "8.15.0", @@ -2799,42 +2513,15 @@ } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 14" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -2852,16 +2539,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2888,60 +2565,148 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", - "dev": true, - "license": "MIT" - }, "node_modules/app-builder-lib": { - "version": "26.0.12", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.0.12.tgz", - "integrity": "sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==", + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", "dev": true, "license": "MIT", "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/asar": "3.2.18", + "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.1", - "@electron/rebuild": "3.7.0", - "@electron/universal": "2.0.1", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", - "builder-util": "26.0.11", - "builder-util-runtime": "9.3.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", - "config-file-ts": "0.2.8-rc1", + "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", - "electron-publish": "26.0.11", + "electron-publish": "26.15.3", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", - "is-ci": "^3.0.0", "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", - "minimatch": "^10.0.0", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", "plist": "3.1.0", + "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", - "semver": "^7.3.8", - "tar": "^6.1.12", + "semver": "~7.7.3", + "tar": "^7.5.7", "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "26.0.12", - "electron-builder-squirrel-windows": "26.0.12" + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/app-builder-lib/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/app-builder-lib/node_modules/fs-extra": { @@ -2959,10 +2724,10 @@ "node": ">=12" } }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2972,10 +2737,37 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -2985,7 +2777,49 @@ "node": ">=10" } }, - "node_modules/app-builder-lib/node_modules/universalify": { + "node_modules/app-builder-lib/node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/app-builder-lib/node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/app-builder-lib/node_modules/unzipper/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/unzipper/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", @@ -2995,6 +2829,22 @@ "node": ">= 10.0.0" } }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/archiver": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", @@ -3076,15 +2926,19 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", "dev": true, - "license": "MIT", - "optional": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, "engines": { - "node": ">=0.8" + "node": ">=12.0.0" } }, "node_modules/assertion-error": { @@ -3109,17 +2963,6 @@ "js-tokens": "^10.0.0" } }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -3154,6 +2997,13 @@ "node": ">= 4.0.0" } }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3252,9 +3102,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -3329,35 +3179,35 @@ } }, "node_modules/builder-util": { - "version": "26.0.11", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.0.11.tgz", - "integrity": "sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==", + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", "dev": true, "license": "MIT", "dependencies": { "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.3.1", + "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", - "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/builder-util-runtime": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", - "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", "dev": true, "license": "MIT", "dependencies": { @@ -3384,9 +3234,9 @@ } }, "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3406,157 +3256,30 @@ "node": ">= 10.0.0" } }, - "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=6.0.0" } }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10.6.0" } }, - "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/cacache/node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "license": "MIT", "dependencies": { @@ -3596,16 +3319,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3647,13 +3360,13 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/chromium-pickle-js": { @@ -3664,9 +3377,9 @@ "license": "MIT" }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -3679,143 +3392,6 @@ "node": ">=8" } }, - "node_modules/clap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/clap/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3831,16 +3407,6 @@ "node": ">=12" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -3854,28 +3420,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/coa": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", - "integrity": "sha512-KAGck/eNAmCL0dcT3BiuYwLbExK6lduR8DxM3C1TyDzaXhZHyZ8ooX5I5+na2e3dPFuibfxrGdorr0/Lr7RYCQ==", - "dev": true, - "dependencies": { - "q": "^1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3896,16 +3440,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3968,64 +3502,6 @@ "dev": true, "license": "MIT" }, - "node_modules/config-file-ts": { - "version": "0.2.8-rc1", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", - "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.3.12", - "typescript": "^5.4.3" - } - }, - "node_modules/config-file-ts/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4040,17 +3516,6 @@ "dev": true, "license": "MIT" }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -4289,16 +3754,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -4342,19 +3797,6 @@ "dev": true, "license": "MIT" }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -4443,9 +3885,9 @@ } }, "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -4454,9 +3896,9 @@ } }, "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4467,21 +3909,16 @@ } }, "node_modules/dmg-builder": { - "version": "26.0.12", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.0.12.tgz", - "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.0.12", - "builder-util": "26.0.11", - "builder-util-runtime": "9.3.1", + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" } }, "node_modules/dmg-builder/node_modules/fs-extra": { @@ -4500,9 +3937,9 @@ } }, "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4522,33 +3959,6 @@ "node": ">= 10.0.0" } }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -4594,9 +4004,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz", - "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -4618,9 +4028,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4704,13 +4114,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -4728,38 +4131,38 @@ } }, "node_modules/electron": { - "version": "36.9.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-36.9.5.tgz", - "integrity": "sha512-1UCss2IqxqujSzg/2jkRjuiT3G+EEXgd6UKB5kUekwQW1LJ6d4QCr8YItfC3Rr9VIGRDJ29eOERmnRNO1Eh+NA==", + "version": "43.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.3.0.tgz", + "integrity": "sha512-nLlvu0WFjftWsSaTkV2B/c4NDuJBspTyXu8vKSQ6vLvFt8uG3NgN49LLKcXddwX0GqVvAQDhciWp+4xOdTdhew==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" }, "bin": { - "electron": "cli.js" + "electron": "cli.js", + "install-electron": "install.js" }, "engines": { - "node": ">= 12.20.55" + "node": ">= 22.12.0" } }, "node_modules/electron-builder": { - "version": "26.0.12", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.0.12.tgz", - "integrity": "sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==", + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.0.12", - "builder-util": "26.0.11", - "builder-util-runtime": "9.3.1", + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", - "dmg-builder": "26.0.12", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", "fs-extra": "^10.1.0", - "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" @@ -4773,15 +4176,15 @@ } }, "node_modules/electron-builder-squirrel-windows": { - "version": "26.0.12", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz", - "integrity": "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==", + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "app-builder-lib": "26.0.12", - "builder-util": "26.0.11", + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", "electron-winstaller": "5.4.0" } }, @@ -4824,17 +4227,18 @@ } }, "node_modules/electron-publish": { - "version": "26.0.11", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.0.11.tgz", - "integrity": "sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==", + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", "dev": true, "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "26.0.11", - "builder-util-runtime": "9.3.1", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", - "form-data": "^4.0.0", + "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" @@ -4856,9 +4260,9 @@ } }, "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4923,17 +4327,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -4974,16 +4367,6 @@ "dev": true, "license": "MIT" }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5012,9 +4395,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -5220,9 +4603,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -5288,20 +4671,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -5399,44 +4768,12 @@ } }, "node_modules/exponential-backoff": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", - "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "dev": true, "license": "Apache-2.0" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, "node_modules/fast-csv": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", @@ -5471,15 +4808,22 @@ "dev": true, "license": "MIT" }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/fdir": { "version": "6.5.0", @@ -5543,9 +4887,9 @@ "license": "MIT" }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5553,9 +4897,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -5565,20 +4909,6 @@ "node": ">=10" } }, - "node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -5600,48 +4930,18 @@ "dev": true, "license": "ISC" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -5667,34 +4967,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5734,19 +5006,6 @@ "node": ">=0.6" } }, - "node_modules/fstream/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/fstream/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -5878,9 +5137,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -5889,9 +5148,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5920,20 +5179,6 @@ "node": ">=10.0" } }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/globals": { "version": "17.6.0", "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", @@ -6144,29 +5389,6 @@ "graphology-types": ">=0.23.0" } }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6221,9 +5443,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -6335,47 +5557,6 @@ "node": ">= 14" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6441,23 +5622,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true, - "license": "ISC" - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6494,485 +5658,16 @@ "node": ">=16.0.0" } }, - "node_modules/inline-source-cli": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/inline-source-cli/-/inline-source-cli-1.2.0.tgz", - "integrity": "sha512-ygKu4DqCykWujseAVLTxowwI0a+E87l7sNf4nRenp+HGQxrWu3XW+tnURXPgA5aVZ2pyLtdn4jdysYRCnOqDFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inline-source": "^5.0.0", - "yargs": "^4.1.0" - }, - "bin": { - "inline-source": "dist/index.js" - } + "node_modules/is-any-array": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz", + "integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==", + "license": "MIT" }, - "node_modules/inline-source-cli/node_modules/ansi-regex": { + "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/inline-source-cli/node_modules/cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/inline-source-cli/node_modules/css-tree": { - "version": "1.0.0-alpha25", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha25.tgz", - "integrity": "sha512-XC6xLW/JqIGirnZuUWHXCHRaAjje2b3OIB0Vj5RIJo6mIi/AdJo30quQl5LxUl0gkXDIrTrFGbMlcZjyFplz1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "^1.0.0", - "source-map": "^0.5.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/csso": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-3.4.0.tgz", - "integrity": "sha512-wkOd9kDYBDHUPOS5/dB2XZuQOuJ0wy83MLZfUliyZj2KXybPE542HjqBySZBBY5+7I83xJ1ymzORp3MXwi+Sxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "1.0.0-alpha25" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/inline-source-cli/node_modules/dom-serializer/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/inline-source-cli/node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/inline-source-cli/node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/inline-source-cli/node_modules/domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "1" - } - }, - "node_modules/inline-source-cli/node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/inline-source-cli/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/inline-source-cli/node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true, - "license": "ISC" - }, - "node_modules/inline-source-cli/node_modules/htmlparser2": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", - "integrity": "sha512-RSOwLNCnCLDRB9XpSfCzsLzzX8COezhJ3D4kRBNWh0NC/facp1hAMmM8zD7kC01My8vD6lGEbPMlbRW/EwGK5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^1.3.0", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/inline-source-cli/node_modules/inline-source": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/inline-source/-/inline-source-5.2.7.tgz", - "integrity": "sha512-RvMOGMXxAqqve4ld128B7TYyNR2aP1LB38dcSpWFmqXrhKPuey1+yFU6kFUgyH8IWX+gRZdWtHN4eQ9d0IpFZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "csso": "3.4.x", - "htmlparser2": "3.9.x", - "is-plain-obj": "1.1.x", - "object-assign": "4.1.x", - "svgo": "0.7.x", - "uglify-js": "3.3.x" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inline-source-cli/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/js-yaml": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", - "integrity": "sha512-eIlkGty7HGmntbV6P/ZlAsoncFLGsNoM27lkTzS+oneY/EiNhj+geqD9ezg/ip+SW6Var0BJU2JtV0vEUZpWVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/inline-source-cli/node_modules/mdn-data": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.2.0.tgz", - "integrity": "sha512-esDqNvsJB2q5V28+u7NdtdMg6Rmg4khQmAVSjUiX7BY/7haIv0K2yWM43hYp0or+3nvG7+UaTF1JHz31hgU1TA==", - "dev": true, - "license": "MPL-2.0" - }, - "node_modules/inline-source-cli/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/inline-source-cli/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/inline-source-cli/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/inline-source-cli/node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "license": "ISC" - }, - "node_modules/inline-source-cli/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/inline-source-cli/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/inline-source-cli/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/svgo": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", - "integrity": "sha512-jT/g9FFMoe9lu2IT6HtAxTA7RR2XOrmcrmCtGnyB/+GQnV6ZjNn+KOHZbZ35yL81+1F/aB6OeEsJztzBQ2EEwA==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", - "dev": true, - "license": "MIT", - "dependencies": { - "coa": "~1.0.1", - "colors": "~1.1.2", - "csso": "~2.3.1", - "js-yaml": "~3.7.0", - "mkdirp": "~0.5.1", - "sax": "~1.2.1", - "whet.extend": "~0.9.9" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/svgo/node_modules/csso": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", - "integrity": "sha512-FmCI/hmqDeHHLaIQckMhMZneS84yzUZdrWDAvJVVxOwcKE1P1LF9FGmzr1ktIQSxOw6fl3PaQsmfg+GN+VvR3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "clap": "^1.0.9", - "source-map": "^0.5.3" - }, - "bin": { - "csso": "bin/csso" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-source-cli/node_modules/y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/inline-source-cli/node_modules/yargs": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", - "integrity": "sha512-LqodLrnIDM3IFT+Hf/5sxBnEGECrfdC1uIbgZeJmESCSo4HoCAaKEus8MylXHAkdacGc0ye+Qa+dpkuom8uVYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "lodash.assign": "^4.0.3", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.1", - "which-module": "^1.0.0", - "window-size": "^0.2.0", - "y18n": "^3.2.1", - "yargs-parser": "^2.4.1" - } - }, - "node_modules/inline-source-cli/node_modules/yargs-parser": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", - "integrity": "sha512-9pIKIJhnI5tonzG6OnCFlz/yln8xHYcGl+pn3xR0Vzff0vzN1PbNRaelgfgRUwZ3s4i3jvxT9WhmUGL4whnasA==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^3.0.0", - "lodash.assign": "^4.0.6" - } - }, - "node_modules/invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-any-array": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz", - "integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==", - "license": "MIT" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -7002,33 +5697,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -7036,26 +5704,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true, - "license": "MIT" - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -7064,9 +5712,9 @@ "license": "MIT" }, "node_modules/isbinaryfile": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz", - "integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", "dev": true, "license": "MIT", "engines": { @@ -7122,33 +5770,16 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", + "async": "^3.2.6", "filelist": "^1.0.4", - "minimatch": "^3.1.2" + "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" @@ -7157,28 +5788,14 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { @@ -7189,9 +5806,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -7211,13 +5828,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true, - "license": "MIT" - }, "node_modules/jsdom": { "version": "29.0.2", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", @@ -7489,19 +6099,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "invert-kv": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7794,23 +6391,6 @@ "dev": true, "license": "ISC" }, - "node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7828,16 +6408,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", - "dev": true, + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.defaults": { @@ -7939,23 +6512,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -7963,151 +6519,58 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", - "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" + "yallist": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" } }, - "node_modules/make-fetch-happen/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, - "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "6", - "debug": "4" + "semver": "^7.5.3" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/marked": { @@ -8189,16 +6652,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -8210,139 +6663,88 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, "engines": { - "node": ">= 8" + "node": "18 || 20 || >=22" } }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">= 8" + "node": "20 || >=22" } }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 8" + "node": ">= 18" } }, "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, "bin": { "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/ml-array-max": { @@ -8401,9 +6803,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, "funding": [ { @@ -8426,50 +6828,19 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", + "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "semver": "^7.6.3" }, "engines": { - "node": ">=10" + "node": ">=22.12.0" } }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/node-api-version": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", @@ -8480,19 +6851,6 @@ "semver": "^7.3.5" } }, - "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -8533,50 +6891,78 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "abbrev": "^1.0.0" + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" }, "bin": { - "nopt": "bin/nopt.js" + "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "BSD-2-Clause", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, "bin": { - "semver": "bin/semver" + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-path": { @@ -8615,26 +7001,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -8668,80 +7034,27 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "ISC", + "dependencies": { + "wrappy": "1" } }, - "node_modules/os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { - "lcid": "^1.0.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, "node_modules/p-cancelable": { @@ -8786,29 +7099,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -8838,19 +7128,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -8877,19 +7154,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -8910,62 +7174,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -8988,13 +7196,6 @@ "url": "https://github.com/sponsors/jet2jet" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9015,37 +7216,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=16.0.0" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" + "node": ">= 16" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/playwright": { @@ -9121,9 +7320,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -9141,7 +7340,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9206,13 +7405,13 @@ } }, "node_modules/proc-log": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", - "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/process-nextick-args": { @@ -9232,13 +7431,6 @@ "node": ">=0.4.0" } }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, - "license": "ISC" - }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -9253,6 +7445,18 @@ "node": ">=10" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -9274,16 +7478,24 @@ "node": ">=6" } }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" + "node": ">=16.0.0" } }, "node_modules/quick-lru": { @@ -9312,35 +7524,6 @@ "read-binary-file-arch": "cli.js" } }, - "node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -9367,9 +7550,9 @@ } }, "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -9399,13 +7582,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", - "dev": true, - "license": "ISC" - }, "node_modules/resedit": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", @@ -9424,27 +7600,6 @@ "url": "https://github.com/sponsors/jet2jet" } }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -9475,20 +7630,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -9499,103 +7640,6 @@ "node": ">= 4" } }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", - "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -9676,17 +7720,10 @@ ], "license": "MIT" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", "dev": true, "license": "WTFPL OR ISC", "dependencies": { @@ -9694,11 +7731,14 @@ } }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/saxes": { "version": "5.0.1", @@ -9714,13 +7754,16 @@ } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-compare": { @@ -9746,14 +7789,7 @@ }, "funding": { "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" + } }, "node_modules/setimmediate": { "version": "1.0.5", @@ -9822,89 +7858,6 @@ "node": ">=10" } }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", - "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9936,42 +7889,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/splaytree": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.2.3.tgz", @@ -9986,20 +7903,8 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } + "license": "BSD-3-Clause", + "optional": true }, "node_modules/stackback": { "version": "0.0.2", @@ -10050,22 +7955,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10079,33 +7968,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-utf8": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10145,33 +8007,20 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.4.tgz", + "integrity": "sha512-GsNRis4e8jxn2Y9ENz/8lbJ93CstG8svtMnuRaHbiF2LTJ5tK0/q3t/URPq9Zc7zVWBJnNnJMIp6bevK7bSmNg==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -10223,21 +8072,20 @@ "license": "MIT" }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-stream": { @@ -10257,14 +8105,14 @@ "node": ">=6" } }, - "node_modules/tar/node_modules/minipass": { + "node_modules/tar/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/temp": { @@ -10309,9 +8157,9 @@ } }, "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10331,20 +8179,6 @@ "node": ">= 10.0.0" } }, - "node_modules/temp/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/temp/node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -10471,9 +8305,9 @@ "license": "MIT" }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { @@ -10569,48 +8403,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.3.28", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.28.tgz", - "integrity": "sha512-68Rc/aA6cswiaQ5SrE979UJcXX+ADA1z33/ZsPd+fbAiVdjZ16OXdbtGO+rJUUBgK6qdf3SOPhQf3K/ybF5Miw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "commander": "~2.15.0", - "source-map": "~0.6.1" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/uglify-js/node_modules/commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true, - "license": "MIT" - }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { @@ -10618,38 +8414,12 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, - "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -10737,40 +8507,17 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vitest": { @@ -11493,16 +9240,6 @@ "node": ">=18" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -11513,6 +9250,20 @@ "node": ">= 8" } }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -11548,16 +9299,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha512-mmIPAft2vTgEILgPeZFqE/wWh24SEsR/k+N9fJ3Jxrz44iDFy9aemCxdksfURSHYFCLmvs/d/7Iso5XjPpNfrA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11574,13 +9315,6 @@ "node": ">= 8" } }, - "node_modules/which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", - "dev": true, - "license": "ISC" - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -11598,19 +9332,6 @@ "node": ">=8" } }, - "node_modules/window-size": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha512-UD7d8HFA2+PZsbKyaOCEy8gMh1oDtHgJh1LfgjQ4zVXmYjAT/kvz3PueITKuqDiIXQe7yzpPnxX3lNc+AhQMyw==", - "dev": true, - "license": "MIT", - "bin": { - "window-size": "cli.js" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11639,25 +9360,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11710,9 +9412,9 @@ "license": "ISC" }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { @@ -11738,17 +9440,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index ebe0052..52278cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graph-lens-lite", - "version": "1.16.1", + "version": "1.17.0", "main": "src/package/electron_app.js", "description": "Visualise and explore property graphs in a lightweight desktop app.", "homepage": "https://github.com/Delta4AI/GraphLensLite", @@ -80,10 +80,14 @@ ] } }, + "overrides": { + "uuid": "^11.1.1", + "undici": "^7.29.0" + }, "devDependencies": { "@eslint/js": "^9.39.4", "@vitest/coverage-v8": "^4.1.9", - "electron": "^36.3.1", + "electron": "^43.3.0", "electron-builder": "^26.0.12", "esbuild": "^0.25.11", "eslint": "^9.39.4", @@ -91,11 +95,9 @@ "exceljs": "^4.4.0", "globals": "^17.6.0", "inline-source": "^8.0.3", - "inline-source-cli": "^1.2.0", "jsdom": "^29.0.2", "playwright": "^1.60.0", "prettier": "^3.8.4", - "rimraf": "^6.0.1", "vitest": "^4.0.18" }, "dependencies": { diff --git a/pyproject.toml b/pyproject.toml index fd7e833..b24ec3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,11 @@ readme = "README.md" requires-python = ">=3.13.1" dependencies = [ "openpyxl>=3.1.5", - "pandas>=2.2.3", - "playwright>=1.58.0", + # Capped: uv.lock pins 2.2.3 and PyPI is a full major ahead (3.x), which + # nothing here has been run against. Lift the cap as its own tested bump. + "pandas>=2.2.3,<3", + # Same floor as the npm devDependency: both ecosystems drive a browser + # (scripts/screenshot_tool.py here, the perf scripts there) and a version + # split between them is a difference in behaviour nobody is tracking. + "playwright>=1.60.0", ] diff --git a/scripts/perf_benchmark.js b/scripts/perf_benchmark.js index 6e80bc3..09a5944 100644 --- a/scripts/perf_benchmark.js +++ b/scripts/perf_benchmark.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* global window, document, requestAnimationFrame, Image, Node */ // browser globals run inside page.evaluate() callbacks +/* global window, document, requestAnimationFrame */ // browser globals run inside page.evaluate() callbacks // Renderer performance benchmark (MIGRATION.md Phase 0 acceptance gates). // Boots the app in headless chromium against the benchmark fixture and // measures: load time, first-interaction stall, wheel-zoom FPS, drag-pan FPS diff --git a/scripts/resize_redraw_check.js b/scripts/resize_redraw_check.js index 3c19be2..a289c06 100644 --- a/scripts/resize_redraw_check.js +++ b/scripts/resize_redraw_check.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* global window, document, requestAnimationFrame, Image, Node */ // browser globals run inside page.evaluate() callbacks +/* global window, document, requestAnimationFrame, Image */ // browser globals run inside page.evaluate() callbacks // Manual regression check: panel toggles must not blank the graph. // // Repro for the "graph disappears after sidebar/bottom-bar toggle until the diff --git a/server/handler.js b/server/handler.js index af7b8cd..fee2ad3 100644 --- a/server/handler.js +++ b/server/handler.js @@ -168,6 +168,35 @@ function handleEvents(req, res, { store }, url) { }); } +/** + * The hostnames this service answers to: the configured bind address, plus + * the loopback aliases that reach a loopback bind. The port is not part of + * the check — a rebinding attacker controls the NAME, not the port, and the + * service is routinely reached on an ephemeral one. + * + * A bind to 0.0.0.0 (or an empty host) is deliberately reachable by any name, + * so it opts out of the check entirely. + * + * @returns {Set|null} lowercased hostnames, or null for "allow any" + */ +function allowedHostSet(config) { + const host = String(config.host || "").toLowerCase(); + if (!host || host === "0.0.0.0" || host === "::") return null; + const names = new Set([host]); + if (host === "127.0.0.1" || host === "::1" || host === "localhost") { + names.add("127.0.0.1").add("localhost").add("[::1]").add("::1"); + } + return names; +} + +/** The hostname half of a Host header, with the port and IPv6 brackets off. */ +function hostnameOf(headerValue) { + const raw = String(headerValue || "").toLowerCase().trim(); + if (raw.startsWith("[")) return raw.slice(1, raw.indexOf("]")); + const colon = raw.lastIndexOf(":"); + return colon === -1 ? raw : raw.slice(0, colon); +} + /** * Build the HTTP request handler for the ingest service. * @@ -180,6 +209,7 @@ function handleEvents(req, res, { store }, url) { * @returns {(req: import('http').IncomingMessage, res: import('http').ServerResponse) => void} */ function createHandler({ store, config, staticDir, version, serveStatic = defaultServeStatic }) { + const allowedHosts = allowedHostSet(config); return function handler(req, res) { let url; try { @@ -188,6 +218,14 @@ function createHandler({ store, config, staticDir, version, serveStatic = defaul sendJson(res, 400, { success: false, error: "Malformed request URL." }); return; } + // DNS rebinding: the service is loopback-bound, but a page on any site the + // user visits can point a hostname it controls at 127.0.0.1 and then read + // the graph, since GET /api/graph and /api/events need no token. The + // browser sends that hostname in Host, so checking it is what closes it. + if (allowedHosts && !allowedHosts.has(hostnameOf(req.headers.host))) { + sendJson(res, 403, { success: false, error: "Host not allowed." }); + return; + } const pathname = url.pathname; const method = req.method || "GET"; diff --git a/server/static.js b/server/static.js index ac314d4..af50ab4 100644 --- a/server/static.js +++ b/server/static.js @@ -23,6 +23,35 @@ const CONTENT_TYPES = { const DEFAULT_DOCUMENT = "graph_lens_lite.html"; +/** + * Baseline hardening for served documents. + * + * Deliberately permissive where the app genuinely needs it: inline `onclick` + * handlers and inline styles are everywhere, the layout worker runs from a + * `blob:`, the CSS carries `data:` images, and the Neo4j/STRING connectors + * fetch whatever origin the user types in. What it still buys is real — no + * external script or plugin origin, no `` rewrite, no framing — and + * anything stricter would cost features rather than add safety. + */ +const CONTENT_SECURITY_POLICY = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: blob:", + "font-src 'self' data:", + "connect-src *", + "worker-src 'self' blob:", + "object-src 'none'", + "base-uri 'none'", + "frame-ancestors 'none'", +].join("; "); + +/** Only documents can be framed or have a base URI, so only they carry these. */ +const DOCUMENT_HEADERS = { + "Content-Security-Policy": CONTENT_SECURITY_POLICY, + "X-Frame-Options": "DENY", +}; + /** * Resolve a URL path to an absolute file path inside `rootDir`, rejecting any * path-traversal attempt. Returns null when the request escapes the root. @@ -94,6 +123,7 @@ function serveStatic(req, res, rootDir) { return; } + const isDocument = path.extname(filePath).toLowerCase() === ".html"; res.writeHead(200, { "Content-Type": contentTypeFor(filePath), "Content-Length": stats.size, @@ -101,6 +131,7 @@ function serveStatic(req, res, rootDir) { "Cache-Control": "no-cache", ETag: etag, "Last-Modified": lastModified, + ...(isDocument ? DOCUMENT_HEADERS : {}), }); fs.createReadStream(filePath).pipe(res); }); @@ -131,4 +162,10 @@ function isNotModified(req, etag, mtimeMs) { return false; } -module.exports = { serveStatic, resolveStaticPath, contentTypeFor, DEFAULT_DOCUMENT }; +module.exports = { + serveStatic, + resolveStaticPath, + contentTypeFor, + DEFAULT_DOCUMENT, + CONTENT_SECURITY_POLICY, +}; diff --git a/src/config.js b/src/config.js index 89d03ea..3d99287 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,7 @@ /** * Defaults for the graph, layouts and UI */ -const VERSION = "1.16.0"; +const VERSION = "1.17.0"; const DEFAULTS = { NODE: { @@ -69,16 +69,19 @@ const DEFAULTS = { }, // Atmospheric canvas layer (heatmap_layer.js): node-density heatmap below // the bubble-set canvas. Off by default; toggled from the workspace toolbar. - // OPACITY/INTENSITY/GAMMA/BANDWIDTH_SCALE/DIM_GRAPH are the runtime knobs + // OPACITY/INTENSITY/GAMMA/BANDWIDTH_SCALE/FADE_GRAPH are the runtime knobs // exposed in the toolbar settings popover — these are the initial values. HEATMAP: { ENABLED: false, MAX_RESOLUTION: 1024, // offscreen splat canvas long-side px cap BANDWIDTH: 0, // splat radius in graph units; 0 → auto (heatBandwidth) BANDWIDTH_SCALE: 1, // multiplier on the (auto or explicit) bandwidth - OPACITY: 0.55, // layer alpha — keeps the field atmospheric - INTENSITY: 0.18, // per-splat center alpha; densities saturate at ~1/INTENSITY overlaps - GAMMA: 0.7, // density exponent before the ramp; < 1 boosts low-density haze + // Tuned against the airport template with FADE_GRAPH on: the previous + // 0.55/0.18/0.7 read as a smudge rather than a density gradient once the + // graph stopped occluding it. + OPACITY: 0.7, // layer alpha — keeps the field atmospheric + INTENSITY: 0.25, // per-splat center alpha; densities saturate at ~1/INTENSITY overlaps + GAMMA: 0.85, // density exponent before the ramp; < 1 boosts low-density haze // Density floor: pixels below this density clear entirely, the rest // renormalizes over the ramp. A lone node peaks at exactly INTENSITY, so // a value just above it shows only overlapping nodes (clusters). @@ -161,9 +164,12 @@ const DEFAULTS = { ], }, }, - // Dim every non-emphasized node/edge while the heatmap is on, so the + // Fade every non-emphasized node/edge while the heatmap is on, so the // density field reads through the graph (the layer sits below everything). - DIM_GRAPH: false, + // 0 leaves the graph alone, 1 fades it out entirely; labels cut out past + // 0.4 because they are the largest occluder. Off by default so switching + // the heatmap on never silently restyles the graph. + FADE_GRAPH: 0, }, LAYOUT: "force", // Keys define the layout template vocabulary (workspace-creation dropdown). @@ -195,103 +201,35 @@ const DEFAULTS = { EXPENSIVE_LAYOUTS: ["dagre", "mds"], LAYOUT_NODE_WARNING_THRESHOLD: 2000, CUSTOM_LAYOUT_NAME: "custom", - BUBBLE_GROUP_STYLE: { - "groupOne": { - fill: '#403C53', - fillOpacity: 0.25, - stroke: '#C33D35', - strokeOpacity: 1, - virtualEdges: true, - padding: 0.1, - corridor: 0.25, - avoidance: 1, - label: true, - labelText: 'group one', - labelFill: '#fff', - labelFontSize: 12, - labelPadding: 2, - labelBackground: true, - labelBackgroundFill: '#403C53', - labelBackgroundRadius: 5, - labelCloseToPath: true, - labelAutoRotate: true, - labelOffsetX: 0, - labelOffsetY: 0, - labelPlacement: 'bottom', - }, - "groupTwo": { - fill: '#c33d35', - fillOpacity: 0.25, - stroke: '#403c53', - strokeOpacity: 1, - virtualEdges: true, - padding: 0.1, - corridor: 0.25, - avoidance: 1, - label: true, - labelText: 'group two', - labelFill: '#fff', - labelFontSize: 12, - labelPadding: 2, - labelBackground: true, - labelBackgroundFill: '#c33d35', - labelBackgroundRadius: 5, - labelCloseToPath: true, - labelAutoRotate: true, - labelOffsetX: 0, - labelOffsetY: 0, - labelPlacement: 'bottom', - }, - "groupThree": { - fill: '#EFB0AA', - fillOpacity: 0.4, - stroke: '#8CA6D9', - strokeOpacity: 1, - virtualEdges: true, - padding: 0.1, - corridor: 0.25, - avoidance: 1, - label: true, - labelText: 'group three', - labelFill: '#fff', - labelFontSize: 12, - labelPadding: 2, - labelBackground: true, - labelBackgroundFill: '#EFB0AA', - labelBackgroundRadius: 5, - labelCloseToPath: true, - labelAutoRotate: true, - labelOffsetX: 0, - labelOffsetY: 0, - labelPlacement: 'bottom', - }, - "groupFour": { - fill: '#8CA6D9', - fillOpacity: 0.4, - stroke: '#EFB0AA', - strokeOpacity: 1, - virtualEdges: true, - padding: 0.1, - corridor: 0.25, - avoidance: 1, - label: true, - labelText: 'group four', - labelFill: '#fff', - labelFontSize: 12, - labelPadding: 2, - labelBackground: true, - labelBackgroundFill: '#8CA6D9', - labelBackgroundRadius: 5, - labelCloseToPath: true, - labelAutoRotate: true, - labelOffsetX: 0, - labelOffsetY: 0, - labelPlacement: 'bottom', - }, - }, - BUBBLE_GROUP_QUADRANT_POSITIONS: { - groupOne: "top-left", groupTwo: "top-right", groupThree: "bottom-left", groupFour: "bottom-right" + // Everything a bubble group's style holds that does NOT depend on which group + // it is. There is no per-group literal any more: a workspace owns its own + // `bubbleSetStyle` map and may hold any number of groups, so the only thing + // config can supply is one template plus a colour cycle + // (see bubbleGroupStyle). Geometry defaults 0.1 / 0.25 / avoidance-on are the + // values tuned in 1.16.0; changing them here changes only NEW groups, because + // a saved workspace stores its own resolved values. + BUBBLE_GROUP_STYLE_TEMPLATE: { + fillOpacity: 0.25, + strokeOpacity: 1, + padding: 0.1, + corridor: 0.25, + avoidance: 1, + label: true, + labelFill: '#fff', + labelFontSize: 12, + labelPadding: 2, + labelBackground: true, + labelBackgroundRadius: 5, + labelCloseToPath: true, + labelAutoRotate: true, + labelOffsetX: 0, + labelOffsetY: 0, + labelPlacement: 'bottom', }, + // The brand colours the original four groups used, in their original order, + // so the first four groups a user creates look exactly as they always have. + // Past the end, bubbleGroupColor walks the hue circle by the golden angle. + BUBBLE_GROUP_PALETTE: ['#403C53', '#C33D35', '#EFB0AA', '#8CA6D9'], STYLES: { NODE_FORM: {"●": "circle", "◆": "diamond", "⬢": "hexagon", "■": "rect", "▲": "triangle", "★": "star"}, NODE_COLORS: {red: "#C33D35", purple: "#403C53", blue: "#8CA6D9", pink: "#EFB0AA", grey: "#ABACBD"}, @@ -389,14 +327,22 @@ const CFG = { // renders hover fast enough that no automatic node/edge-count cutoff is needed). DISABLE_HOVER_EFFECT: false, -// if network is greater than defined threshold, bubble groups may span across non-bubble group members. -// Measured with the sigma renderer (bubblesets-js createOutline, virtualEdges on, viewport-scale -// coordinates, 2026-06-10): 60 members + 1000 avoid rects ≈ 69 ms, +2000 ≈ 194 ms, 300 members + -// 5700 avoid ≈ 12 s — virtual-edge routing around obstacles is O(members × avoid). 1000 keeps a -// full per-group outline recompute under the ~100 ms interactivity budget; camera pan/zoom reuses -// cached outlines and is unaffected (src/graph/bubble_layer.js). - MAX_NODES_BEFORE_DISABLING_AVOID_MEMBERS_IN_BUBBLE_GROUPS: 1000, - AVOID_MEMBERS_IN_BUBBLE_GROUPS: false, +// Fitting a bubble group's hull around non-members costs O(members × avoid) — +// bubblesets-js routes virtual edges around every obstacle. Measured against +// computeOutlineGeometry (node, 2026-08-07): +// +// 5 members × 2 000 avoid → 48 ms 100 × 3 000 → 935 ms +// 20 members × 10 000 avoid → 540 ms 200 × 2 000 → 1 939 ms +// 10 members × 50 000 avoid → 1 646 ms 300 × 5 700 → 20 345 ms +// +// ≈ 6 µs per (member × avoid) pair, within ~2-3× across that whole range. +// This used to be gated on TOTAL NODE COUNT (> 1000 → avoidance silently off), +// which is the wrong variable: a 5-member group on a 10 000-node graph costs a +// quarter of a second and was refused, while a 300-member group is the only +// shape that ever needed refusing. Groups now estimate their own cost and ask +// only when it exceeds this budget — the user is allowed to wait. +// Camera pan/zoom reuses cached outlines and is unaffected (bubble_layer.js). + AVOID_FIT_CONFIRM_MS: 500, // Maximum capacity of selection memory MAX_SELECTION_MEMORY: 25, @@ -422,4 +368,61 @@ const CFG = { // setup/settings modal — nothing to keep in sync here. } -export {VERSION, DEFAULTS, CFG} \ No newline at end of file +// Successive multiples of the golden angle land far apart on the hue circle, +// which is what keeps an unbounded number of generated colours separable. +const GOLDEN_ANGLE_DEG = 137.508; + +// Names that reach an object's prototype through plain assignment, so they are +// refused wherever imported text becomes a live object key (Excel headers, +// merged property groups). Same set as the JSON reviver in server/validate.js. +const UNSAFE_OBJECT_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + +/** HSL (h 0-360, s/l 0-1) to `#rrggbb`. */ +function hslToHex(h, s, l) { + const a = s * Math.min(l, 1 - l); + const channel = (n) => { + const k = (n + h / 30) % 12; + const v = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); + return Math.round(255 * v).toString(16).padStart(2, '0'); + }; + return `#${channel(0)}${channel(8)}${channel(4)}`; +} + +/** + * Fill colour for the group at `index`. The palette covers the first four, then + * the hue circle is walked by the golden angle so any number of groups stays + * visually separable without a hand-authored list. + * + * Always `#rrggbb`: `` (the group row's swatch) accepts + * nothing else, so an `hsl()` string here silently collapsed every group past + * the fourth onto the swatch's fallback colour. + * + * @param {number} index zero-based creation order + * @returns {string} hex colour + */ +function bubbleGroupColor(index) { + const palette = DEFAULTS.BUBBLE_GROUP_PALETTE; + if (index < palette.length) return palette[index]; + return hslToHex((index * GOLDEN_ANGLE_DEG) % 360, 0.55, 0.55); +} + +/** + * A complete style object for a new bubble group: the shared template plus the + * three values that identify one group from another. The stroke is the NEXT + * palette entry, which is what the original four did (contrast, not a tint of + * the fill). + * @param {number} index zero-based creation order + * @param {string} [name] label text; defaults to "Group " + */ +function bubbleGroupStyle(index, name) { + const fill = bubbleGroupColor(index); + return { + ...DEFAULTS.BUBBLE_GROUP_STYLE_TEMPLATE, + fill, + stroke: bubbleGroupColor(index + 1), + labelText: name ?? `Group ${index + 1}`, + labelBackgroundFill: fill, + }; +} + +export {VERSION, DEFAULTS, CFG, bubbleGroupColor, bubbleGroupStyle, hslToHex, GOLDEN_ANGLE_DEG, UNSAFE_OBJECT_KEYS} \ No newline at end of file diff --git a/src/gll.js b/src/gll.js index b5e5c60..1183ac1 100644 --- a/src/gll.js +++ b/src/gll.js @@ -20,12 +20,25 @@ import {NumericScalePicker} from './utilities/numeric_scale_picker.js'; import {PieChartPicker} from './utilities/pie_chart_picker.js'; import {DataTable, buildDataTable} from "./utilities/data_editor.js"; import {StringDemoDataLoader} from "./utilities/demo_loader.js"; +import {openNeo4jPopup} from "./utilities/neo4j_loader.js"; +import {expandNeo4jSelection, openNeo4jJoinPopup} from "./utilities/neo4j_session.js"; import {Popup} from "./utilities/popup.js"; import {StaticUtilities} from "./utilities/static.js"; import {generateTourData, GuidedTour} from "./utilities/tour.js"; import {initApiClient} from "./managers/api_client.js"; +import {initRail} from "./managers/rail.js"; import {initTheme} from "./utilities/theme.js"; -import {initSelectionHud} from "./utilities/selection_hud.js"; +import {initUiTooltips} from "./utilities/ui_tooltip.js"; +import {initInspector} from "./managers/inspector.js"; +import {initWorkbench} from "./managers/workbench.js"; +import {initFilterSearch} from "./managers/filter_search.js"; +import {initCommandPalette} from "./managers/command_palette.js"; +import {initHistory} from "./managers/history.js"; +import { + isWebGL2Available, + renderWebGLUnavailableMessage, + WEBGL2_ERROR_MESSAGE, +} from "./graph/webgl_support.js"; // Stores all reference objects @@ -95,6 +108,7 @@ class Cache { this.dataTable = new DataTable(this); this.metrics = new NetworkMetrics(this); this.assistant = new AssistantManager(this); + this.history = initHistory(this); this.buildDataTable = buildDataTable; } @@ -137,6 +151,7 @@ class Cache { this.propIDToDropdownChecklists = new Map(); this.propIDToInvertibleRangeSliders = new Map(); + this.propIDToBooleanToggles = new Map(); this.lastBubbleSetMembers = new Map(); this.bubbleSetChanged = false; @@ -452,25 +467,24 @@ async function startTour() { } window.loadDemoData = loadDemoData; +window.loadNeo4jData = () => openNeo4jPopup(cache); +window.expandNeo4jSelection = () => expandNeo4jSelection(cache); +window.openNeo4jJoinPopup = () => openNeo4jJoinPopup(cache); window.startTour = startTour; window.cache = cache; -window.addEventListener('resize', () => { - if (window.graph !== undefined && window.graph !== null && window.cache.initialized) { - const sidebar = document.getElementById("sidebar"); - const status = document.getElementById("sidebarStatusContainer"); - - status.style.maxWidth = `${sidebar.offsetWidth}px`; - } -}) - window.addEventListener("DOMContentLoaded", () => { // Stored preference wins; prefers-color-scheme is only the first-run default. initTheme(document, window); cache.reset(); + cache.rail = initRail(cache); + cache.inspector = initInspector(); + cache.workbench = initWorkbench(cache); + initFilterSearch(); + cache.palette = initCommandPalette(cache); + initUiTooltips(document); cache.ui.updateDarkModeButton(); - initSelectionHud(); // cache.initialize(); // Display version info @@ -483,23 +497,37 @@ window.addEventListener("DOMContentLoaded", () => { landingVersion.textContent = `v${VERSION}`; } - // Setup sidebar resize functionality - const sidebar = document.getElementById('sidebar'); - const resizeHandle = document.querySelector('.sidebar-resize-handle'); + // Warn on the landing page too: without WebGL2 every load path ends in a + // graph-less stage, so say it before the user picks a data source — and + // disable the actions that would land there (template download still works). + const webglWarning = document.getElementById('landingWebglWarning'); + if (webglWarning && !isWebGL2Available()) { + renderWebGLUnavailableMessage(webglWarning); + webglWarning.hidden = false; + document.querySelectorAll('#landingPage [data-needs-webgl]').forEach((btn) => { + btn.disabled = true; + btn.title = WEBGL2_ERROR_MESSAGE; + }); + } + + // Inspector resize — drags from its left edge (right-docked panel). Same + // shadow-bar gesture as the workbench's top edge (workbench.js). + const inspector = document.getElementById('inspector'); + const resizeHandle = document.querySelector('.inspector-resize-handle'); + const INSPECTOR_MIN_WIDTH = 300; + const INSPECTOR_MAX_WIDTH = 900; let isResizing = false; - let startX = 0; - let startWidth = 0; let shadowBar = null; - resizeHandle.addEventListener('mousedown', (e) => { + const clampInspectorWidth = (w) => + StaticUtilities.clamp(w, INSPECTOR_MIN_WIDTH, INSPECTOR_MAX_WIDTH); + + resizeHandle?.addEventListener('mousedown', (e) => { isResizing = true; - startX = e.clientX; - startWidth = sidebar.offsetWidth; - // Create shadow bar shadowBar = document.createElement('div'); shadowBar.className = 'sidebar-resize-shadow'; - shadowBar.style.left = `${startWidth}px`; + shadowBar.style.left = `${window.innerWidth - inspector.offsetWidth}px`; shadowBar.style.width = '4px'; shadowBar.style.display = 'block'; document.body.appendChild(shadowBar); @@ -511,93 +539,21 @@ window.addEventListener("DOMContentLoaded", () => { }); document.addEventListener('mousemove', (e) => { - if (!isResizing) return; - - const deltaX = e.clientX - startX; - const newWidth = Math.max(200, startWidth + deltaX); - - if (shadowBar) { - shadowBar.style.left = `${newWidth}px`; - } + if (!isResizing || !shadowBar) return; + shadowBar.style.left = `${window.innerWidth - clampInspectorWidth(window.innerWidth - e.clientX)}px`; }); - document.addEventListener('mouseup', (e) => { + document.addEventListener('mouseup', () => { if (!isResizing) return; - const deltaX = e.clientX - startX; - const newWidth = Math.max(200, startWidth + deltaX); - - sidebar.style.width = `${newWidth}px`; - - // Update status container max-width to match the new sidebar width - const status = document.getElementById("sidebarStatusContainer"); - if (status) { - status.style.maxWidth = `${newWidth}px`; - } - - isResizing = false; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - if (shadowBar) { + const rect = shadowBar.getBoundingClientRect(); + inspector.style.width = `${clampInspectorWidth(window.innerWidth - rect.left)}px`; shadowBar.remove(); shadowBar = null; } - }); - - // Setup assistant sidebar resize functionality (mirrors the left sidebar but - // drags from the left edge of the right-docked panel). - const assistantSidebar = document.getElementById('assistantSidebar'); - const assistantResizeHandle = assistantSidebar?.querySelector('.assistant-resize-handle'); - const ASSISTANT_MIN_WIDTH = 300; - const ASSISTANT_MAX_WIDTH = 900; - let isAssistantResizing = false; - let assistantStartX = 0; - let assistantStartWidth = 0; - let assistantShadowBar = null; - - const clampAssistantWidth = (w) => Math.max(ASSISTANT_MIN_WIDTH, Math.min(ASSISTANT_MAX_WIDTH, w)); - - assistantResizeHandle?.addEventListener('mousedown', (e) => { - if (!assistantSidebar.classList.contains('active')) return; - isAssistantResizing = true; - assistantStartX = e.clientX; - assistantStartWidth = assistantSidebar.offsetWidth; - - assistantShadowBar = document.createElement('div'); - assistantShadowBar.className = 'sidebar-resize-shadow'; - assistantShadowBar.style.left = `${window.innerWidth - assistantStartWidth}px`; - assistantShadowBar.style.width = '4px'; - assistantShadowBar.style.display = 'block'; - document.body.appendChild(assistantShadowBar); - document.body.style.cursor = 'ew-resize'; - document.body.style.userSelect = 'none'; - - e.preventDefault(); - }); - - document.addEventListener('mousemove', (e) => { - if (!isAssistantResizing) return; - const deltaX = e.clientX - assistantStartX; - const newWidth = clampAssistantWidth(assistantStartWidth - deltaX); - if (assistantShadowBar) { - assistantShadowBar.style.left = `${window.innerWidth - newWidth}px`; - } - }); - - document.addEventListener('mouseup', () => { - if (!isAssistantResizing) return; - - if (assistantShadowBar) { - const rect = assistantShadowBar.getBoundingClientRect(); - const newWidth = clampAssistantWidth(window.innerWidth - rect.left); - assistantSidebar.style.setProperty('--assistant-width', `${newWidth}px`); - assistantShadowBar.remove(); - assistantShadowBar = null; - } - - isAssistantResizing = false; + isResizing = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; diff --git a/src/graph/annotation_geometry.js b/src/graph/annotation_geometry.js new file mode 100644 index 0000000..661992e --- /dev/null +++ b/src/graph/annotation_geometry.js @@ -0,0 +1,215 @@ +/** + * Pure geometry + trust-boundary validation for text annotations (notes). + * + * Node-safe (no DOM, no sigma at module scope) so both the browser layer + * (annotation_layer.js) and the SVG export (export_svg.js) share ONE source + * of box metrics — the live DOM note, the PNG repaint and the SVG rect must + * all frame the same text identically. + * + * Coordinates: an annotation's x/y is the box's TOP-LEFT corner in app-model + * graph space (y-down, same frame as layout.positions), sized at camera + * ratio 1 — the whole box scales with zoom like the rest of the drawing. + */ + +// Font metrics shared by the DOM note (CSS), the canvas painter and the SVG +// serializer. line-height 1.25 is set explicitly on the DOM element so the +// canvas/SVG line stacking matches browser text layout. +const ANNOTATION_FONT_FAMILY = 'Arial, sans-serif'; +const ANNOTATION_LINE_HEIGHT = 1.25; +const ANNOTATION_PADDING_PX = 6; + +const MIN_FONT_SIZE = 6; +const MAX_FONT_SIZE = 200; +const MAX_BORDER_WIDTH = 20; +const MAX_TEXT_LENGTH = 2000; +// A crafted file must not "note-bomb" the DOM: every surviving record becomes +// a live element repositioned per frame, so the list itself is capped too. +const MAX_ANNOTATIONS = 500; +// Same paint-value policy as export_svg.js SAFE_PAINT_RE: hex/rgb()/hsl()/ +// named colors pass; quotes, angle brackets, ampersands and colons don't. +// Applied at the trust boundary so the DOM, canvas and SVG sinks all inherit +// one rule instead of relying on per-engine leniency for junk values. +const SAFE_COLOR_RE = /^[#a-zA-Z0-9(),.%\s-]+$/; +const MAX_COLOR_LENGTH = 50; + +const MAX_BORDER_RADIUS = 40; + +// App red — readable on both the light and the dark stage background. +const ANNOTATION_DEFAULTS = Object.freeze({ + text: 'Text', + fontSize: 14, + fontColor: '#C33D35', + borderColor: '#C33D35', + borderWidth: 1.5, + borderRadius: 6, + bgColor: null, // transparent; a color makes the note a card + shadow: false, // only painted when bgColor is set (see ANNOTATION_SHADOW) +}); + +// One fixed, tasteful shadow — not a knob. blur is the CSS blur-radius; the +// SVG feDropShadow stdDeviation is blur/2 (both ≈ 2σ of the Gaussian). +// Rendered ONLY together with a background fill: CSS casts an outer shadow +// as if the border-box were opaque, so a shadow on a transparent note would +// read as a floating ring — and SVG could not reproduce it at all. +const ANNOTATION_SHADOW = Object.freeze({ + offsetY: 2, + blur: 8, + color: 'rgba(0, 0, 0, 0.35)', +}); + +const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); + +/** Finite number or the fallback (loaded JSON is untrusted). */ +function finiteOr(value, fallback) { + const n = Number(value); + return Number.isFinite(n) ? n : fallback; +} + +/** Bounded string or the fallback. */ +function stringOr(value, fallback, maxLength) { + if (typeof value !== 'string') return fallback; + return value.length > maxLength ? value.slice(0, maxLength) : value; +} + +/** Bounded, allowlisted CSS color or the fallback. */ +function colorOr(value, fallback) { + const s = stringOr(value, fallback, MAX_COLOR_LENGTH); + return typeof s === 'string' && SAFE_COLOR_RE.test(s) ? s : fallback; +} + +/** + * Validate one raw annotation from a loaded file into the canonical shape. + * Returns null for records that cannot be placed (non-finite coordinates) — + * a note without a position is unrecoverable, everything else has defaults. + * + * @param {unknown} raw + * @returns {{id: string, text: string, x: number, y: number, fontSize: number, + * fontColor: string, borderColor: string, borderWidth: number} | null} + */ +function normalizeAnnotation(raw) { + if (!raw || typeof raw !== 'object') return null; + const x = Number(raw.x); + const y = Number(raw.y); + if (!Number.isFinite(x) || !Number.isFinite(y)) return null; + return { + id: stringOr(raw.id, '', 64) || `ann-${Math.abs(x).toFixed(2)}-${Math.abs(y).toFixed(2)}`, + text: stringOr(raw.text, ANNOTATION_DEFAULTS.text, MAX_TEXT_LENGTH), + x, + y, + fontSize: clamp( + finiteOr(raw.fontSize, ANNOTATION_DEFAULTS.fontSize), + MIN_FONT_SIZE, + MAX_FONT_SIZE + ), + fontColor: colorOr(raw.fontColor, ANNOTATION_DEFAULTS.fontColor), + borderColor: colorOr(raw.borderColor, ANNOTATION_DEFAULTS.borderColor), + borderWidth: clamp(finiteOr(raw.borderWidth, ANNOTATION_DEFAULTS.borderWidth), 0, MAX_BORDER_WIDTH), + borderRadius: clamp( + finiteOr(raw.borderRadius, ANNOTATION_DEFAULTS.borderRadius), + 0, + MAX_BORDER_RADIUS + ), + // null stays null (transparent); an invalid color string drops to null + // rather than to some default paint. + bgColor: raw.bgColor == null ? null : colorOr(raw.bgColor, null), + shadow: raw.shadow === true, + }; +} + +/** + * Trust boundary for a loaded layout's annotations array: keep only records + * that survive normalization. Anything non-array becomes []. + * + * @param {unknown} list + * @returns {Array} + */ +function sanitizeAnnotations(list) { + if (!Array.isArray(list)) return []; + return list.slice(0, MAX_ANNOTATIONS).map(normalizeAnnotation).filter(Boolean); +} + +/** + * Box metrics for an annotation at camera ratio 1 (multiply by the zoom factor + * for on-screen sizes). boxW/boxH are the OUTER border-box — the same rectangle + * the DOM element occupies (content + 2×padding + 2×border). + * + * It also hands back the three shapes a repaint needs — the fill box, the + * border-inset stroke box, and each line's baseline origin — because BOTH export + * sinks (canvas drawExport, SVG annotationPrimitives) needed them and each had + * its own copy of the inset and corner-shrink maths. + * + * @param {object} ann normalized annotation + * @param {(text: string, font: string) => number} measureText + * @returns {{lines: string[], lineHeight: number, pad: number, font: string, + * contentW: number, contentH: number, boxW: number, boxH: number, + * radius: number, + * fillBox: {x: number, y: number, width: number, height: number, radius: number}, + * strokeBox: null|{x: number, y: number, width: number, height: number, + * radius: number, strokeWidth: number}, + * textLines: Array<{text: string, x: number, y: number}>}} + */ +function annotationLayout(ann, measureText) { + const font = `${ann.fontSize}px ${ANNOTATION_FONT_FAMILY}`; + const lines = String(ann.text).split('\n'); + const lineHeight = ann.fontSize * ANNOTATION_LINE_HEIGHT; + let contentW = 0; + for (const line of lines) contentW = Math.max(contentW, measureText(line, font)); + const contentH = lines.length * lineHeight; + const pad = ANNOTATION_PADDING_PX; + const boxW = contentW + 2 * pad + 2 * ann.borderWidth; + const boxH = contentH + 2 * pad + 2 * ann.borderWidth; + const radius = Number.isFinite(ann.borderRadius) ? Math.max(0, ann.borderRadius) : 0; + const originX = ann.borderWidth + pad; + const originY = ann.borderWidth + pad; + + return { + lines, + lineHeight, + pad, + font, + contentW, + contentH, + boxW, + boxH, + radius, + fillBox: { x: 0, y: 0, width: boxW, height: boxH, radius }, + // Stroke CENTRED on the border band, so its outer edge lands on the + // border-box outline exactly like the CSS border; the corner radius shrinks + // with the inset to keep the OUTER curve at `radius`. + strokeBox: + ann.borderWidth > 0 + ? { + x: ann.borderWidth / 2, + y: ann.borderWidth / 2, + width: boxW - ann.borderWidth, + height: boxH - ann.borderWidth, + radius: Math.max(0, radius - ann.borderWidth / 2), + strokeWidth: ann.borderWidth, + } + : null, + // y is the line's CENTRE (canvas paints with textBaseline "middle"); the SVG + // sink adds its own alphabetic-baseline shift. + textLines: lines.map((text, i) => ({ + text, + x: originX, + y: originY + (i + 0.5) * lineHeight, + })), + }; +} + +export { + ANNOTATION_DEFAULTS, + ANNOTATION_SHADOW, + ANNOTATION_FONT_FAMILY, + ANNOTATION_LINE_HEIGHT, + ANNOTATION_PADDING_PX, + MAX_TEXT_LENGTH, + MAX_ANNOTATIONS, + MIN_FONT_SIZE, + MAX_FONT_SIZE, + MAX_BORDER_WIDTH, + MAX_BORDER_RADIUS, + normalizeAnnotation, + sanitizeAnnotations, + annotationLayout, +}; diff --git a/src/graph/annotation_layer.js b/src/graph/annotation_layer.js new file mode 100644 index 0000000..c3312e1 --- /dev/null +++ b/src/graph/annotation_layer.js @@ -0,0 +1,771 @@ +/** + * Browser-only text-annotation ("note") layer. + * + * Unlike the bubble/heatmap layers this one is DOM, not canvas: each note is + * an absolutely-positioned div over the sigma canvases, which buys native + * text rendering, contenteditable editing, CSS borders and per-note pointer + * events for free — a canvas layer would need hand-rolled hit-testing and a + * text-input overlay anyway. The divs are re-anchored on every afterRender + * via graphToViewport and scaled by 1/camera.ratio (transform-origin 0 0), + * so a note pans and zooms with the drawing it annotates. + * + * State lives in the CURRENT workspace's layout object + * (cache.data.layouts[selected].annotations, app-model y-down coordinates, + * flipY at this boundary like layout.positions) — mutating it IS persistence: + * buildExportPayload serializes cache.data wholesale, parseLayouts sanitizes + * on the way back in (annotation_geometry.js). Workspace switches need no + * events: every sync re-reads the selected layout. + * + * Exports: the flattened sigma PNG and the SVG document carry no DOM, so + * drawExport (canvas) and exportPlacements (SVG primitive input) repaint the + * notes from the same annotationLayout metrics the DOM element uses. + */ +import { flipY } from './graph_model.js'; +import { + ANNOTATION_DEFAULTS, + ANNOTATION_SHADOW, + ANNOTATION_FONT_FAMILY, + ANNOTATION_LINE_HEIGHT, + ANNOTATION_PADDING_PX, + MAX_TEXT_LENGTH, + MIN_FONT_SIZE, + MAX_FONT_SIZE, + MAX_BORDER_WIDTH, + MAX_BORDER_RADIUS, + annotationLayout, +} from './annotation_geometry.js'; +import { clampPopoverLeft, clampPopoverTop } from '../utilities/popover_position.js'; +import { FrameCoalescer } from './overlay_frame.js'; + +// A press that travels less than this many screen px is a click (open the +// style popover), not a drag. +const DRAG_THRESHOLD_PX = 3; +const POPOVER_OFFSET_PX = 8; + +// ------------------------------------------------------------ popover inputs +// Row + input builders for the note style popover. Module scope rather than +// closures inside #openPopover, which was 114 lines mostly made of these. +// `after` is the caller's per-edit hook (repaint + mark dirty). + +function popoverRow(labelText, input) { + const label = document.createElement('label'); + label.className = 'annotation-popover-row'; + const span = document.createElement('span'); + span.textContent = labelText; + label.append(span, input); + return label; +} + +function popoverNumberInput(value, min, max, step, onChange, after) { + const input = document.createElement('input'); + input.type = 'number'; + Object.assign(input, { min, max, step, value }); + input.addEventListener('input', () => { + const n = Number(input.value); + if (Number.isFinite(n)) onChange(Math.min(max, Math.max(min, n))); + after(); + }); + return input; +} + +function popoverColorInput(value, onChange, after) { + const input = document.createElement('input'); + input.type = 'color'; + input.value = value; + input.addEventListener('input', () => { + onChange(input.value); + after(); + }); + return input; +} + +function popoverCheckboxInput(checked, onChange, after) { + const input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = checked; + input.addEventListener('input', () => { + onChange(input.checked); + after(); + }); + return input; +} + +/** + * The two rows that depend on each other: background (a toggle plus a colour, + * because has no "none") and shadow, which needs a fill to + * be visible at all (see ANNOTATION_SHADOW) and so switches the background on + * rather than doing nothing. + */ +function buildPopoverFillRows(ann, after) { + const bgColorInput = popoverColorInput(ann.bgColor ?? '#ffffff', (v) => { + if (ann.bgColor != null) ann.bgColor = v; + }, after); + const bgToggle = popoverCheckboxInput(ann.bgColor != null, (on) => { + ann.bgColor = on ? bgColorInput.value : null; + }, after); + const bgControls = document.createElement('span'); + bgControls.className = 'annotation-popover-pair'; + bgControls.append(bgToggle, bgColorInput); + const bgRow = popoverRow('Background', bgControls); + + const shadowToggle = popoverCheckboxInput(ann.shadow === true, (on) => { + ann.shadow = on; + if (on && ann.bgColor == null) { + ann.bgColor = bgColorInput.value; + bgToggle.checked = true; + } + }, after); + return { bgRow, shadowToggle }; +} + +class AnnotationLayer { + /** + * @param {object} adapter SigmaAdapter (owns sigma + graphology) + * @param {object} cache app cache (layout state, ui toasts) + * @param {HTMLElement} container the sigma container (#innerGraphContainer) + */ + constructor(adapter, cache, container) { + this.adapter = adapter; + this.cache = cache; + this.container = container; + this.killed = false; + // Layer visibility, driven by the inspector's Overlays stack. Runtime + // state like heatmapEnabled — never persisted, resets with the adapter. + this.visible = true; + + /** @type {Map} note id → element */ + this.els = new Map(); + this.editingId = null; + this.popover = null; + this.popoverId = null; + this.placementOverlay = null; + // Every other piece of mutable state, declared rather than materialising + // mid-method: what the open editor started from and how it ends (a cancel + // discards, and a cancel on a note that only existed for this edit deletes + // it), whether the popover changed anything worth an undo entry, and the + // handlers/fingerprints the teardown and the per-frame gate compare. + this.editingIsNew = false; + this.editingOriginal = null; + this.editingCancelled = false; + this.popoverDirty = false; + this.placementEscape = null; + this.lastAnns = null; + this.frame = new FrameCoalescer(() => { + if (this.#stale()) this.sync(); + }); + + this.root = document.createElement('div'); + this.root.className = 'annotation-layer'; + container.appendChild(this.root); + + this.renderHandler = () => this.scheduleSync(); + adapter.sigma.on('afterRender', this.renderHandler); + // Camera + note-set fingerprint of the last reconcile, so the per-frame + // hook can tell "nothing moved" from "re-anchor everything" (see #stale). + this.syncStamp = null; + } + + /** + * Show or hide every note, on screen and in both export paths. Hiding also + * closes the styling popover — it would otherwise float over a note that is + * no longer on screen. + */ + setVisible(visible) { + if (this.visible === visible) return; + this.visible = visible; + this.root.hidden = !visible; + if (!visible) { + this.cancelPlacement(); + this.#closePopover(); + return; + } + // Frames rendered while hidden are skipped, so a camera that moved in the + // meantime left the notes anchored to their old spots. Nothing re-renders + // on unhide by itself — re-anchor them here. + if (this.#stale()) this.sync(); + } + + destroy() { + if (this.killed) return; + this.killed = true; + this.frame.kill(); + this.adapter.sigma.off('afterRender', this.renderHandler); + this.cancelPlacement(); + this.#closePopover(); + this.root.remove(); + this.els.clear(); + } + + // ------------------------------------------------------------------- state + + /** The selected workspace's annotations (read-only view; [] when absent). */ + annotations() { + return this.cache.data?.layouts?.[this.cache.data.selectedLayout]?.annotations ?? []; + } + + #annotationById(id) { + return this.annotations().find((a) => a.id === id) ?? null; + } + + // ----------------------------------------------------------------- placing + + /** + * One-shot placement: a crosshair overlay captures the next click, creates + * a note there and opens it for editing. Escape cancels. + */ + armPlacement() { + if (this.placementOverlay) return; + const overlay = document.createElement('div'); + overlay.className = 'annotation-placement-overlay'; + overlay.addEventListener('pointerdown', (event) => { + if (event.button !== 0) return; + const { x, y } = this.#relativePoint(event); + this.cancelPlacement(); + this.#createAt(x, y); + }); + this.placementEscape = (event) => { + if (event.key === 'Escape') { + this.cancelPlacement(); + this.cache.ui?.info?.('Note placement canceled'); + } + }; + document.addEventListener('keydown', this.placementEscape, true); + this.container.appendChild(overlay); + this.placementOverlay = overlay; + this.cache.ui?.setNotePlacementActive?.(true); + } + + cancelPlacement() { + if (!this.placementOverlay) return; + this.placementOverlay.remove(); + this.placementOverlay = null; + // Every route out of placement lands here — the placing click, Escape, + // hiding the layer — so the rail button's armed look is dropped here too. + this.cache.ui?.setNotePlacementActive?.(false); + document.removeEventListener('keydown', this.placementEscape, true); + this.placementEscape = null; + } + + #createAt(viewportX, viewportY) { + const layout = this.cache.data?.layouts?.[this.cache.data.selectedLayout]; + if (!layout) return; + const g = this.adapter.sigma.viewportToGraph({ x: viewportX, y: viewportY }); + const ann = { + id: typeof crypto?.randomUUID === 'function' ? crypto.randomUUID() : `ann-${Date.now()}`, + ...ANNOTATION_DEFAULTS, + x: g.x, + y: flipY(g.y), + }; + (layout.annotations ??= []).push(ann); + this.sync(); + // First note: the Notes row's switch is disabled while there is nothing to + // show, so it has to be re-read here. Not in sync() — that runs per render. + this.cache.ui?.syncOverlays?.(); + const el = this.els.get(ann.id); + if (el) this.#startEditing(ann, el, { selectAll: true }); + } + + /** + * Remove a note by id (popover delete). + * + * `record: false` is for discarding a note the user never really made — a + * fresh placement blurred without typing. Committing there would push an + * undo entry whose before and after are the same state. + */ + removeAnnotation(id, { record = true } = {}) { + const layout = this.cache.data?.layouts?.[this.cache.data.selectedLayout]; + if (!layout?.annotations) return; + layout.annotations = layout.annotations.filter((a) => a.id !== id); + this.#closePopover(); + this.sync(); + this.cache.ui?.syncOverlays?.(); + if (record) this.cache.history?.commit('Delete note'); + } + + // ------------------------------------------------------------------- sync + + scheduleSync() { + this.frame.schedule(); + } + + /** + * Has anything changed that the notes' DOM depends on? This runs on every + * rendered frame — hover included — where a full sync() would rebuild a + * signature string per note (note text runs to 2000 chars) and rewrite every + * transform for nothing. + * + * The fingerprint covers the camera and the note-set identity. Content edits + * mutate a note in place and call sync() directly, so they never need to be + * detected here; a restored workspace (undo) swaps the array, which does show + * up as a new reference. + */ + #stale() { + if (!this.visible) return false; + const camera = this.adapter.sigma.getCamera().getState(); + const anns = this.annotations(); + const stamp = `${camera.x}|${camera.y}|${camera.ratio}|${camera.angle}|${anns.length}`; + if (stamp === this.syncStamp && anns === this.lastAnns) return false; + this.syncStamp = stamp; + this.lastAnns = anns; + return true; + } + + /** Reconcile the DOM notes with the selected workspace, immediately. */ + sync() { + if (this.killed) return; + const anns = this.annotations(); + const seen = new Set(); + for (const ann of anns) { + seen.add(ann.id); + let el = this.els.get(ann.id); + if (!el) { + el = this.#createElement(ann.id); + this.els.set(ann.id, el); + } + this.#applyStyle(ann, el); + this.#place(ann, el); + } + for (const [id, el] of this.els) { + if (seen.has(id)) continue; + el.remove(); + this.els.delete(id); + if (this.editingId === id) this.editingId = null; + } + // A deleted note or a workspace switch must not leave a popover editing + // a note that is no longer on screen. + if (this.popoverId && !seen.has(this.popoverId)) this.#closePopover(); + else if (this.popoverId) this.#positionPopover(); + } + + #createElement(id) { + const el = document.createElement('div'); + el.className = 'annotation-note'; + el.dataset.annotationId = id; + // Keyboard operability for existing notes: focus with Tab, Enter to + // edit, Delete to remove (pointer users dblclick / use the popover). + el.setAttribute('role', 'note'); + el.setAttribute('tabindex', '0'); + el.addEventListener('pointerdown', (event) => this.#onNotePointerDown(id, el, event)); + el.addEventListener('dblclick', (event) => { + event.preventDefault(); + const ann = this.#annotationById(id); + if (ann) this.#startEditing(ann, el, { selectAll: false }); + }); + el.addEventListener('keydown', (event) => { + if (this.editingId === id) return; // typing, not commanding + if (event.key === 'Enter') { + event.preventDefault(); + const ann = this.#annotationById(id); + if (ann) this.#startEditing(ann, el, { selectAll: false }); + } else if (event.key === 'Delete' || event.key === 'Backspace') { + event.preventDefault(); + this.removeAnnotation(id); + } + }); + this.root.appendChild(el); + return el; + } + + /** Restyle only when the note's content/style signature changed. */ + #applyStyle(ann, el) { + const signature = + `${ann.text}|${ann.fontSize}|${ann.fontColor}|${ann.borderColor}|${ann.borderWidth}` + + `|${ann.borderRadius ?? 0}|${ann.bgColor ?? ''}|${ann.shadow ? 1 : 0}`; + if (el.dataset.signature === signature) return; + el.dataset.signature = signature; + // Text as textContent, never innerHTML — note text is user data. + if (this.editingId !== ann.id) el.textContent = ann.text; + const shadow = ann.shadow && ann.bgColor; // ring shadows are unreproducible in exports + Object.assign(el.style, { + font: `${ann.fontSize}px ${ANNOTATION_FONT_FAMILY}`, + lineHeight: String(ANNOTATION_LINE_HEIGHT), + color: ann.fontColor, + padding: `${ANNOTATION_PADDING_PX}px`, + border: ann.borderWidth > 0 ? `${ann.borderWidth}px solid ${ann.borderColor}` : 'none', + borderRadius: `${ann.borderRadius ?? 0}px`, + background: ann.bgColor ?? 'transparent', + boxShadow: shadow + ? `0 ${ANNOTATION_SHADOW.offsetY}px ${ANNOTATION_SHADOW.blur}px ${ANNOTATION_SHADOW.color}` + : 'none', + }); + } + + #place(ann, el) { + const sigma = this.adapter.sigma; + const v = sigma.graphToViewport({ x: ann.x, y: flipY(ann.y) }); + const k = 1 / sigma.getCamera().getState().ratio; + el.style.transform = `translate(${v.x}px, ${v.y}px) scale(${k})`; + } + + // ---------------------------------------------------------- drag / select + + #relativePoint(event) { + const rect = this.container.getBoundingClientRect(); + return { x: event.clientX - rect.left, y: event.clientY - rect.top }; + } + + #onNotePointerDown(id, el, event) { + if (event.button !== 0 || this.editingId === id) return; + event.preventDefault(); + event.stopPropagation(); + const ann = this.#annotationById(id); + if (!ann) return; + + const sigma = this.adapter.sigma; + const start = this.#relativePoint(event); + const g0 = sigma.viewportToGraph(start); + // Grab offset in app-model coordinates: the note keeps its position + // under the cursor for the whole drag. + const dx = ann.x - g0.x; + const dy = ann.y - flipY(g0.y); + let moved = false; + + const onMove = (moveEvent) => { + const p = this.#relativePoint(moveEvent); + if (!moved && Math.hypot(p.x - start.x, p.y - start.y) < DRAG_THRESHOLD_PX) return; + moved = true; + const g = sigma.viewportToGraph(p); + ann.x = g.x + dx; + ann.y = flipY(g.y) + dy; + this.#place(ann, el); + if (this.popoverId === id) this.#positionPopover(); + }; + const onUp = () => { + el.removeEventListener('pointermove', onMove); + el.removeEventListener('pointerup', onUp); + el.removeEventListener('pointercancel', onUp); + if (!moved) this.#openPopover(ann.id); + else this.cache.history?.commit('Move note'); + }; + el.addEventListener('pointermove', onMove); + el.addEventListener('pointerup', onUp); + el.addEventListener('pointercancel', onUp); + // jsdom (tests) lacks pointer capture; without it a fast drag can slip + // off the note mid-move, which is tolerable there. + try { + el.setPointerCapture(event.pointerId); + } catch { + /* no pointer capture available */ + } + } + + // ----------------------------------------------------------------- editing + + #startEditing(ann, el, { selectAll }) { + this.#closePopover(); + this.editingId = ann.id; + // Only #createAt selects all, so this doubles as "the note was just placed" + // — it has to be committed even if the default text is left untouched. + this.editingIsNew = selectAll; + // Escape means cancel (see onKey): restore this on an existing note, drop + // the note entirely on one that was just placed. + this.editingOriginal = ann.text; + this.editingCancelled = false; + el.classList.add('editing'); + // plaintext-only strips markup from paste natively; engines that reject + // the keyword fall back to "true", where the explicit paste handler below + // provides the same guarantee. + try { + el.contentEditable = 'plaintext-only'; + } catch { + el.contentEditable = 'true'; + } + el.focus(); + if (selectAll && typeof window.getSelection === 'function') { + const range = document.createRange(); + range.selectNodeContents(el); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + } + const onKey = (event) => { + if (event.key === 'Escape') { + event.stopPropagation(); + // Flagged before the blur, because blurring runs the COMMIT path: the + // flag is what turns that into a discard, so Escape on a freshly placed + // note cannot save the untouched "Text" default. + this.editingCancelled = true; + el.blur(); + } + }; + // Pasted markup must never render live in the note, regardless of + // whether the engine honored plaintext-only above: always insert the + // clipboard's plain text instead of the default rich paste. + const onPaste = (event) => { + event.preventDefault(); + const text = event.clipboardData?.getData('text/plain') ?? ''; + if (text) document.execCommand?.('insertText', false, text); + }; + const onBlur = () => { + el.removeEventListener('blur', onBlur); + el.removeEventListener('keydown', onKey); + el.removeEventListener('paste', onPaste); + this.#commitEditing(ann.id, el); + }; + el.addEventListener('blur', onBlur); + el.addEventListener('keydown', onKey); + el.addEventListener('paste', onPaste); + } + + #commitEditing(id, el) { + this.editingId = null; + const wasNew = this.editingIsNew === true; + const cancelled = this.editingCancelled === true; + const original = this.editingOriginal; + this.editingIsNew = false; + this.editingCancelled = false; + el.contentEditable = 'false'; + el.classList.remove('editing'); + const ann = this.#annotationById(id); + if (!ann) return; + + if (cancelled) { + // A note that only existed for this edit goes with it; an older one keeps + // the text it had. Either way nothing reaches the undo history. + if (wasNew) { + this.removeAnnotation(id, { record: false }); + // Escape on a fresh note takes the note AND everything typed into it, + // with no undo entry to get it back — so it has to say so, the way + // cancelPlacement already does. + this.cache.ui?.info?.('Note discarded.'); + } else { + el.textContent = original; + delete el.dataset.signature; + this.sync(); + } + return; + } + + // innerText preserves the visual line breaks contenteditable produced. + // Same length cap as the JSON-load boundary, so a save/load round-trip + // never silently truncates what the user just typed. + const typed = (el.innerText ?? el.textContent ?? '').replace(/\n+$/, ''); + const text = typed.slice(0, MAX_TEXT_LENGTH); + if (typed.length > text.length) { + this.cache.ui?.warning?.( + `Note trimmed to ${MAX_TEXT_LENGTH} characters (${typed.length} pasted).`, + ); + } + if (text.trim() === '') { + // A note that only existed for this edit leaves no trace. An older one + // is a deletion the user never asked for by name, so it gets both an + // undo entry and a word about what just happened. + this.removeAnnotation(id, { record: !wasNew }); + if (!wasNew) this.cache.ui?.info?.('Note deleted — its text was emptied.'); + return; + } + const changed = ann.text !== text; + ann.text = text; + delete el.dataset.signature; // force a restyle with the committed text + this.sync(); + // A note is created and opened for typing in one gesture, so this is where + // "add a note" ends as far as undo is concerned. + if (wasNew) this.cache.history?.commit('Add note'); + else if (changed) this.cache.history?.commit('Edit note'); + } + + // ----------------------------------------------------------------- popover + + #openPopover(id) { + this.#closePopover(); + const ann = this.#annotationById(id); + if (!ann) return; + this.popoverId = id; + // Style inputs mutate the note live and fire per keystroke and per colour + // drag, so the undo entry is recorded once when the popover closes. + this.popoverDirty = false; + + const pop = document.createElement('div'); + pop.className = 'annotation-popover'; + pop.setAttribute('role', 'group'); + pop.setAttribute('aria-label', 'Note style'); + // Every input reports through the same hook: repaint now, record one undo + // entry when the popover closes. + const after = () => this.#afterStyleEdit(); + const numberInput = (value, min, max, step, onChange) => + popoverNumberInput(value, min, max, step, onChange, after); + const colorInput = (value, onChange) => popoverColorInput(value, onChange, after); + + const { bgRow, shadowToggle } = buildPopoverFillRows(ann, after); + + pop.append( + // Same bounds the sanitizer clamps to on load — stated once, there. + popoverRow( + 'Font size', + numberInput(ann.fontSize, MIN_FONT_SIZE, MAX_FONT_SIZE, 1, (v) => (ann.fontSize = v)) + ), + popoverRow('Font color', colorInput(ann.fontColor, (v) => (ann.fontColor = v))), + popoverRow('Border color', colorInput(ann.borderColor, (v) => (ann.borderColor = v))), + popoverRow( + 'Border width', + numberInput(ann.borderWidth, 0, MAX_BORDER_WIDTH, 0.5, (v) => (ann.borderWidth = v)) + ), + popoverRow( + 'Corner radius', + numberInput(ann.borderRadius ?? 0, 0, MAX_BORDER_RADIUS, 1, (v) => (ann.borderRadius = v)) + ), + bgRow, + popoverRow('Shadow', shadowToggle) + ); + const del = document.createElement('button'); + del.type = 'button'; + del.className = 'annotation-popover-delete'; + del.textContent = '✗ Delete note'; + del.addEventListener('click', () => this.removeAnnotation(id)); + pop.appendChild(del); + + document.body.appendChild(pop); + this.popover = pop; + this.#positionPopover(); + pop.querySelector('input')?.focus(); + this.#wirePopoverDismiss(id, pop); + } + + /** Outside-pointerdown and Escape, both scoped to this popover's lifetime. + * Escape hands focus back to the note it belongs to. */ + #wirePopoverDismiss(id, pop) { + this.popoverOutside = (event) => { + const noteEl = this.els.get(id); + if (pop.contains(event.target) || noteEl?.contains(event.target)) return; + this.#closePopover(); + }; + document.addEventListener('pointerdown', this.popoverOutside, true); + this.popoverEscape = (event) => { + if (event.key !== 'Escape') return; + event.stopPropagation(); + const noteEl = this.els.get(id); + this.#closePopover(); + noteEl?.focus(); + }; + document.addEventListener('keydown', this.popoverEscape, true); + } + + /** A style input changed the note: repaint now, record the undo on close. */ + #afterStyleEdit() { + this.popoverDirty = true; + this.sync(); + } + + #positionPopover() { + const el = this.els.get(this.popoverId); + if (!el || !this.popover) return; + const rect = el.getBoundingClientRect(); + // Below the note by default, above it when the colour rows and Delete would + // fall off the bottom edge (same flip as the tooltip layer). The clamp is + // the last resort for a popover taller than the space on either side. + const height = this.popover.offsetHeight; + let top = rect.bottom + POPOVER_OFFSET_PX; + if (top + height > window.innerHeight - POPOVER_OFFSET_PX) { + top = rect.top - height - POPOVER_OFFSET_PX; + } + this.popover.style.top = `${clampPopoverTop(top, height, window.innerHeight)}px`; + this.popover.style.left = `${clampPopoverLeft( + rect.left, + this.popover.offsetWidth, + window.innerWidth + )}px`; + } + + #closePopover() { + if (!this.popover) return; + this.popover.remove(); + this.popover = null; + this.popoverId = null; + document.removeEventListener('pointerdown', this.popoverOutside, true); + this.popoverOutside = null; + document.removeEventListener('keydown', this.popoverEscape, true); + this.popoverEscape = null; + if (this.popoverDirty) { + this.popoverDirty = false; + this.cache.history?.commit('Style note'); + } + } + + // ------------------------------------------------------------------ export + + /** + * Notes with their current viewport anchor (CSS px, top-left) and zoom + * factor — the SVG export's input, mirroring the live transform exactly. + * + * @returns {Array<{ann: object, x: number, y: number, k: number}>} + */ + exportPlacements() { + if (!this.visible) return []; + const sigma = this.adapter.sigma; + const k = 1 / sigma.getCamera().getState().ratio; + return this.annotations().map((ann) => { + const v = sigma.graphToViewport({ x: ann.x, y: flipY(ann.y) }); + return { ann, x: v.x, y: v.y, k }; + }); + } + + /** + * Repaint the notes onto a PNG-export context at `scale` device px per CSS + * px, ABOVE the flattened sigma image (the live DOM sits above every + * canvas). Geometry is the shared annotationLayout metrics, so the export + * matches the DOM box to the pixel. + * + * @param {CanvasRenderingContext2D} ctx + * @param {number} scale + */ + drawExport(ctx, scale) { + const placements = this.exportPlacements(); + if (placements.length === 0) return; + ctx.save(); + try { + ctx.setTransform(scale, 0, 0, scale, 0, 0); + for (const { ann, x, y, k } of placements) { + const layout = annotationLayout(ann, (text, font) => { + ctx.font = font; + return ctx.measureText(text).width; + }); + ctx.save(); + ctx.translate(x, y); + ctx.scale(k, k); + // Every rectangle and origin below comes from annotationLayout, so this + // and the SVG export cannot disagree about the border inset, the corner + // shrink or where a line sits. + const { fillBox, strokeBox } = layout; + if (ann.bgColor) { + if (ann.shadow) { + // Canvas shadows live in DEVICE px (the transform does not apply + // to them), so the CSS-px values scale by the full device factor. + ctx.shadowColor = ANNOTATION_SHADOW.color; + ctx.shadowOffsetY = ANNOTATION_SHADOW.offsetY * scale * k; + ctx.shadowBlur = ANNOTATION_SHADOW.blur * scale * k; + } + ctx.fillStyle = ann.bgColor; + ctx.beginPath(); + ctx.roundRect(fillBox.x, fillBox.y, fillBox.width, fillBox.height, fillBox.radius); + ctx.fill(); + ctx.shadowColor = 'transparent'; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 0; + } + if (strokeBox) { + ctx.strokeStyle = ann.borderColor; + ctx.lineWidth = strokeBox.strokeWidth; + ctx.beginPath(); + ctx.roundRect( + strokeBox.x, + strokeBox.y, + strokeBox.width, + strokeBox.height, + strokeBox.radius + ); + ctx.stroke(); + } + ctx.fillStyle = ann.fontColor; + ctx.font = layout.font; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + for (const line of layout.textLines) ctx.fillText(line.text, line.x, line.y); + ctx.restore(); + } + } finally { + ctx.restore(); + } + } +} + +export { AnnotationLayer }; diff --git a/src/graph/bubble_geometry.js b/src/graph/bubble_geometry.js index fc65ff3..4342104 100644 --- a/src/graph/bubble_geometry.js +++ b/src/graph/bubble_geometry.js @@ -1,5 +1,5 @@ /** - * Node-safe bubble-set geometry helpers (MIGRATION.md Phase 4). + * Node-safe bubble-set geometry helpers. * * Pure math between the app's bubble groups and bubblesets-js: rect * building, outline computation/sampling, label anchoring and the cache @@ -40,6 +40,24 @@ const OUTLINE_SIMPLIFY_TOLERANCE_RATIO = 1; // float-noise duplicate/degenerate vertices that trip its sweep line. const CLIP_SNAP = 32; +// Refit-time conditioning of the FIELD ring (marching-squares outline only — +// geometric reconstructions are exact shapes already). Simplification alone +// cannot remove field wobble: Douglas-Peucker keeps the EXTREME points of the +// grid-scale ripple, so the painted curve still weaves through its peaks, and +// the sharp survivors stay sharp because the painter's turn-falloff damping +// deliberately refuses to round them (the wedge fix). Averaging is what +// removes noise — the ring is resampled to uniform spacing and smoothed with +// Taubin's shrink-compensated λ|μ Laplacian, once per refit, never per frame. +// A conditioned ring also has evenly spaced, gently turning vertices, which +// is exactly the input the Catmull-Rom painter renders smoothest. The result +// is guarded: if smoothing pinches a thin corridor into a self-intersection, +// the unconditioned ring is kept, and the enclosure guarantee downstream +// repairs any member the smoothing shaved. +const RING_RESAMPLE_SPACING_CELLS = 1.5; +const RING_SMOOTH_PASSES = 8; +const RING_SMOOTH_LAMBDA = 0.5; +const RING_SMOOTH_MU = -0.53; + // bubblesets-js' influence-field defaults (nodeR0 15, nodeR1 50, edgeR0 10, // edgeR1 20, morphBuffer 10, pixelGroup 4) are ABSOLUTE pixel radii tuned // for ~15 px-radius nodes. Used raw they give every group a constant ~15 px @@ -106,6 +124,24 @@ const HOLE_GAP_RADIUS_RATIO = 0.25; // 4 px default — a coarser grid visibly wobbles the outline around large // nodes (jaggies/spline hooks at pinch points), it only ever gets finer for // small nodes. +// +// It must also be a WHOLE number of pixels, for the same reason the sample +// stride above must be. bubblesets-js maps pixels to marching cells with +// PotentialArea.scaleX = floor((x - originX) / pixelGroup) but maps cells +// back with invertScaleX = round(i * pixelGroup + originX). That round trip +// only agrees when the cell is an integer; at a fractional cell adjacent +// contour cells can invert onto the same pixel and the traced ring +// short-circuits across its own interior — painting the empty space between +// two corridors as one solid wedge. No downstream repair can catch it: the +// resulting ring is simple and does not self-intersect. +// +// Measured on the reported layout, sweeping the node radius 4→40 px: 15 of +// 145 radii wedged with a fractional cell, 0 with a rounded one. Over 400 +// random layouts a fractional cell inflated the hull past 1.5× the rounded +// one 43 times (worst 10.4×) versus 15 the other way (worst 2.2×, and those +// are honest corridor-thickness differences, not fills). The ratio below +// lands off a whole number for mean member radii of 3.75–15 px, which is +// squarely where ordinary graphs sit — hence "weird at times". const FIELD_PIXEL_GROUP_MIN = 1; const FIELD_PIXEL_GROUP_MAX = 4; // Capsule links are gently ARCED tubes (quadratic, deterministic bulge), so @@ -153,7 +189,10 @@ function meanMemberRadius(memberRects) { } /** - * Compute one group's outline polygon from member/avoid rects. + * Compute one group's geometry from member/avoid rects: the outer hull ring + * plus interior HOLES carved around non-members the hull swallowed (see + * carveAvoidHoles — the field alone can only fjord the boundary). Render with + * even-odd fill. * * The influence field is sized from the group's MEAN MEMBER RADIUS (see the * ratio constants above), so hulls stay visually proportional whatever node @@ -162,34 +201,12 @@ function meanMemberRadius(memberRects) { * * @param {Array<{x,y,width,height}>} memberRects * @param {Array<{x,y,width,height}>} avoidRects - * @param {{virtualEdges?: boolean, padding?: number, corridor?: number, - * avoidance?: number}} [opts] - * virtualEdges routes connecting corridors around avoid rects (the - * per-group style enables it); its cost is O(members × avoid) — see - * MAX_NODES_BEFORE_DISABLING_AVOID_MEMBERS_IN_BUBBLE_GROUPS in config.js - * for the measured budget. + * @param {{padding?: number, corridor?: number, avoidance?: number}} [opts] * padding multiplies the node influence radii (body extent past members); * corridor multiplies the edge influence radii (arm thickness). Both are * user style knobs, clamped to [0.05, 4]; 1 = one mean-radius of margin. * avoidance is a switch (numeric for JSON back-compat): 0 lets the hull * cover non-members, anything > 0 steers around them and carves holes. - * @returns {Array<{x: number, y: number}>} closed polygon (empty only when - * there are no members — a lost field reconstructs geometrically) - */ -function computeOutlinePoints(memberRects, avoidRects = [], opts = {}) { - return computeOutlineGeometry(memberRects, avoidRects, opts).outer; -} - -/** - * Full group geometry: the outer hull ring plus interior HOLES carved around - * non-members the hull swallowed (see carveAvoidHoles — the field alone can - * only fjord the boundary). Render with even-odd fill. Same options as - * computeOutlinePoints. - * - * @param {Array<{x,y,width,height}>} memberRects - * @param {Array<{x,y,width,height}>} avoidRects - * @param {{virtualEdges?: boolean, padding?: number, corridor?: number, - * avoidance?: number}} [opts] * @returns {{outer: Array<{x: number, y: number}>, * holes: Array>}} */ @@ -204,9 +221,10 @@ function computeOutlineGeometry(memberRects, avoidRects = [], opts = {}) { const avoidRaw = Number(opts.avoidance ?? 1); const avoidance = Number.isFinite(avoidRaw) && avoidRaw <= 0 ? 0 : 1; const unit = meanMemberRadius(memberRects); - const pixelGroupPx = Math.min( - FIELD_PIXEL_GROUP_MAX, - Math.max(FIELD_PIXEL_GROUP_MIN, FIELD_PIXEL_GROUP_RATIO * unit) + // Rounded LAST, so the whole-pixel cell the library needs is also inside + // the [MIN, MAX] range (both bounds are already whole numbers). + const pixelGroupPx = Math.round( + Math.min(FIELD_PIXEL_GROUP_MAX, Math.max(FIELD_PIXEL_GROUP_MIN, FIELD_PIXEL_GROUP_RATIO * unit)) ); // Field radii track the knobs all the way down — NO grid floor. A field // thinner than a marching cell simply registers nothing, and the enclosure @@ -233,7 +251,7 @@ function computeOutlineGeometry(memberRects, avoidRects = [], opts = {}) { let outline = []; if (nodeFieldVisible || edgeFieldVisible) { const path = bubblesets.createOutline(memberRects, effectiveAvoidRects, [], { - virtualEdges: opts.virtualEdges !== false && edgeFieldVisible, + virtualEdges: edgeFieldVisible, nodeR0: nodeR0px, nodeR1: nodeR1px, edgeR0: edgeR0px, @@ -274,6 +292,7 @@ function computeOutlineGeometry(memberRects, avoidRects = [], opts = {}) { const repaired = repairSelfIntersections(outline); if (repaired.length >= 3) outline = repaired; } + outline = conditionFieldRing(outline, pixelGroupPx); } outline = ensureMembersEnclosed(outline, memberRects, nodeR0px, linkRPx); // Hole breathing room = the padding radius (per-node proportional floor @@ -281,6 +300,86 @@ function computeOutlineGeometry(memberRects, avoidRects = [], opts = {}) { return carveAvoidHoles(outline, memberRects, effectiveAvoidRects, nodeR0px, nodeR0px); } +/** + * Resample a closed ring to (near-)uniform vertex spacing by walking its + * perimeter. Spacing grows as needed to respect the point cap. + * + * @param {Array<{x: number, y: number}>} ring closed ring, open form + * @param {number} spacing target vertex spacing (px) + * @returns {Array<{x: number, y: number}>} + */ +function resampleRing(ring, spacing) { + const n = ring.length; + let perimeter = 0; + for (let i = 0; i < n; i++) { + const a = ring[i]; + const b = ring[(i + 1) % n]; + perimeter += Math.hypot(b.x - a.x, b.y - a.y); + } + if (perimeter === 0) return ring; + const count = Math.max(8, Math.min(OUTLINE_MAX_SAMPLED_POINTS, Math.round(perimeter / spacing))); + const step = perimeter / count; + const out = []; + let next = 0; // arc length of the next emitted point + let walked = 0; + for (let i = 0; i < n && out.length < count; i++) { + const a = ring[i]; + const b = ring[(i + 1) % n]; + const len = Math.hypot(b.x - a.x, b.y - a.y); + while (next <= walked + len && out.length < count) { + const t = len === 0 ? 0 : (next - walked) / len; + out.push({ x: a.x + t * (b.x - a.x), y: a.y + t * (b.y - a.y) }); + next += step; + } + walked += len; + } + return out.length >= 3 ? out : ring; +} + +/** + * Taubin λ|μ smoothing of a closed ring: alternating Laplacian steps with a + * positive (shrink) and slightly larger negative (re-inflate) factor, so + * noise damps without the curve-shortening collapse plain Laplacian has. + * + * @param {Array<{x: number, y: number}>} ring closed ring, open form + * @returns {Array<{x: number, y: number}>} + */ +function taubinSmoothRing(ring) { + const n = ring.length; + let pts = ring; + for (let pass = 0; pass < 2 * RING_SMOOTH_PASSES; pass++) { + const factor = pass % 2 === 0 ? RING_SMOOTH_LAMBDA : RING_SMOOTH_MU; + const out = new Array(n); + for (let i = 0; i < n; i++) { + const prev = pts[(i - 1 + n) % n]; + const cur = pts[i]; + const nxt = pts[(i + 1) % n]; + out[i] = { + x: cur.x + factor * ((prev.x + nxt.x) / 2 - cur.x), + y: cur.y + factor * ((prev.y + nxt.y) / 2 - cur.y), + }; + } + pts = out; + } + return pts; +} + +/** + * Condition the field ring for painting (see the RING_SMOOTH_* note): + * uniform resample + Taubin smoothing, reverted wholesale if the smoothed + * ring self-intersects (a pinched-shut thin corridor). + * + * @param {Array<{x: number, y: number}>} ring + * @param {number} pixelGroupPx marching-grid cell size (px) + * @returns {Array<{x: number, y: number}>} + */ +function conditionFieldRing(ring, pixelGroupPx) { + if (ring.length < 8) return ring; + const spacing = RING_RESAMPLE_SPACING_CELLS * pixelGroupPx; + const smoothed = taubinSmoothRing(resampleRing(ring, spacing)); + return polygonSelfIntersects(smoothed) ? ring : smoothed; +} + /** * Punch interior holes for avoid nodes the hull swallowed. Only nodes whose * CENTER lies inside the outer ring get a disc (boundary-adjacent ones are @@ -458,7 +557,7 @@ function unionPolygons(...geoms) { try { return polygonClipping.union(geoms[0], ...geoms.slice(1)); } catch (e) { - if (globalThis.__BUBBLE_DEBUG) console.error('union failed:', e.message); + console.warn('bubble union failed:', e.message); return null; } } @@ -769,90 +868,90 @@ function outlineLabelAnchor(points, placement = "top") { return { x: anchor.x, y: anchor.y, angle, nx, ny }; } -/** - * Order-insensitive identity key for a member id set. Sorting keeps the key - * stable across the manager's Set→Array conversions; NUL-joined so ids - * containing spaces cannot make distinct sets collide. - * - * @param {Iterable} ids - * @returns {string} - */ -function idsKey(ids) { - return [...ids].sort().join("\u0000"); -} +// --- group label + outline paint constants ------------------------------- +// Node-safe, and the ONE definition: the canvas layer and the SVG export both +// draw a group's label, and export_svg carried its own literals annotated +// "// bubble_layer.js" — a comment is not a dependency, so the two drifted. +const OUTLINE_STROKE_WIDTH = 2; +// Off-path labels clear the outline by half the padded font box plus this gap. +const LABEL_STANDOFF_PX = 8; +const BUBBLE_LABEL_FONT_FAMILY = "Arial, sans-serif"; /** - * Checksum over member positions so a node drag invalidates the cached - * outline. 32-bit FNV-style integer fold (Math.imul) over coordinates - * quantized to 1/8 px plus the index: exact integer arithmetic is immune to - * IEEE754 accumulation loss at large coordinate magnitudes and member - * counts, and folding the index in keeps it order-sensitive (transpositions - * hash differently). An optional per-point `s` (on-screen node radius) folds - * in too, so a node-size change invalidates the cached outline — the fit - * consumes radii, not just positions. + * Resolve a group label to pure geometry: where the text goes, whether it is + * rotated onto the path, and the background box behind it. Both sinks then only + * PAINT — bubble_layer #drawLabel and export_svg bubbleLabelPrimitives used to + * re-implement the standoff maths, the rotation predicate and the background + * rect line for line. * - * @param {Array<{x: number, y: number, s?: number}>} points - * @returns {string} - */ -function positionsChecksum(points) { - let hash = 0x811c9dc5; - let i = 0; - for (const p of points) { - hash = Math.imul(hash ^ Math.round(p.x * 8), 0x01000193); - hash = Math.imul(hash ^ Math.round(p.y * 8), 0x01000193); - hash = Math.imul(hash ^ Math.round((p.s ?? 0) * 8), 0x01000193); - hash = Math.imul(hash ^ i, 0x01000193); - i++; - } - return `${points.length}:${hash >>> 0}`; -} - -// Style fields that change the painted result; everything else the manager -// passes (members, avoidMembers) is keyed elsewhere by the layer. -const STYLE_KEY_FIELDS = [ - "fill", - "fillOpacity", - "stroke", - "strokeOpacity", - "virtualEdges", - "padding", - "corridor", - "avoidance", - "label", - "labelText", - "labelFill", - "labelFontSize", - "labelPadding", - "labelBackground", - "labelBackgroundFill", - "labelBackgroundRadius", - "labelOffsetX", - "labelOffsetY", - "labelPlacement", - "labelCloseToPath", - "labelAutoRotate", -]; - -/** - * Stable key over the style options that affect painting. JSON-serialized - * per field so a literal "undefined" string can never collide with a - * missing field; null and missing collide on purpose (both paint via the - * group defaults). + * The `label` on/off gate stays with the callers: the live layer reads it from + * manager-merged opts, the export resolves it through opts ?? defaults. * - * @param {object} opts group style options (manager-passed) - * @returns {string} + * @param {{x: number, y: number, angle: number, nx: number, ny: number}|null} anchor + * @param {object} opts per-group style + * @param {object} defaults BUBBLE_GROUP_STYLE_TEMPLATE + * @param {(text: string, font: string) => number} measureText + * @returns {null|{text: string, font: string, fontSize: number, x: number, + * y: number, lx: number, ly: number, rotated: boolean, angle: number, + * fill: string, background: null|{x: number, y: number, width: number, + * height: number, radius: number, fill: string}}} */ -function styleKey(opts = {}) { - return STYLE_KEY_FIELDS.map((field) => JSON.stringify(opts[field] ?? null)).join("|"); +function resolveBubbleLabel(anchor, opts = {}, defaults = {}, measureText) { + const text = String(opts.labelText ?? defaults.labelText ?? ""); + if (!text || !anchor) return null; + + const placement = opts.labelPlacement ?? defaults.labelPlacement ?? "bottom"; + const closeToPath = opts.labelCloseToPath ?? defaults.labelCloseToPath ?? true; + const autoRotate = opts.labelAutoRotate ?? defaults.labelAutoRotate ?? true; + const fontSize = opts.labelFontSize ?? defaults.labelFontSize ?? 12; + const padding = opts.labelPadding ?? defaults.labelPadding ?? 2; + + const standoff = closeToPath ? 0 : fontSize / 2 + padding + LABEL_STANDOFF_PX; + const x = anchor.x + anchor.nx * standoff + (opts.labelOffsetX ?? 0); + const y = anchor.y + anchor.ny * standoff + (opts.labelOffsetY ?? 0); + const font = `${fontSize}px ${BUBBLE_LABEL_FONT_FAMILY}`; + const textWidth = measureText(text, font); + + // Rotation only makes sense hugging the path; "center" has no tangent. + const rotated = autoRotate && closeToPath && placement !== "center"; + // Rotated labels paint inside a translate(x,y) rotate(angle) frame, so their + // own coordinates are relative to it. + const lx = rotated ? 0 : x; + const ly = rotated ? 0 : y; + + const background = (opts.labelBackground ?? defaults.labelBackground) + ? { + x: lx - textWidth / 2 - padding, + y: ly - fontSize / 2 - padding, + width: textWidth + 2 * padding, + height: fontSize + 2 * padding, + radius: opts.labelBackgroundRadius ?? defaults.labelBackgroundRadius ?? 5, + fill: opts.labelBackgroundFill ?? defaults.labelBackgroundFill ?? "#403C53", + } + : null; + + return { + text, + font, + fontSize, + x, + y, + lx, + ly, + rotated, + angle: anchor.angle, + fill: opts.labelFill ?? defaults.labelFill ?? "#fff", + background, + }; } export { nodeViewportRect, - computeOutlinePoints, computeOutlineGeometry, polygonSelfIntersects, outlineLabelAnchor, - idsKey, - positionsChecksum, - styleKey, + resolveBubbleLabel, + OUTLINE_STROKE_WIDTH, + LABEL_STANDOFF_PX, + BUBBLE_LABEL_FONT_FAMILY, }; diff --git a/src/graph/bubble_layer.js b/src/graph/bubble_layer.js index e522e7a..8eb2777 100644 --- a/src/graph/bubble_layer.js +++ b/src/graph/bubble_layer.js @@ -1,5 +1,5 @@ /** - * Browser-only bubble-set rendering layer (MIGRATION.md Phase 4). + * Browser-only bubble-set rendering layer. * * Two 2d canvases registered with sigma: the fill/outline canvas sits ABOVE * the node layer (createCanvasContext + afterLayer: "nodes") so the group @@ -20,8 +20,6 @@ * a filter event can never leave a stale outline painted) * - camera pan/zoom → cheap per-frame reprojection of the cached graph * points; the camera is NOT in the identity key, so zoom never re-fits - * This replaces the G6 plugin's recompute-per-draw churn (the patched - * updateBubbleSetsPath path coalescing, issue #7195) with an owned cache. */ import { DEFAULTS } from '../config.js'; import { @@ -29,18 +27,30 @@ import { computeOutlineGeometry, polygonSelfIntersects, outlineLabelAnchor, - idsKey, - positionsChecksum, - styleKey, + resolveBubbleLabel, + OUTLINE_STROKE_WIDTH, } from './bubble_geometry.js'; +import { idsKey, positionsChecksum, styleKey } from './overlay_keys.js'; import { smoothClosedPath } from './bubble_smoothing.js'; +import { prepareOverlayCanvas } from './dpr_watch.js'; +import { FrameCoalescer } from './overlay_frame.js'; const LAYER_NAME = 'bubbleSets'; const LABEL_LAYER_NAME = 'bubbleSetsLabels'; -const OUTLINE_STROKE_WIDTH = 2; -// Extra screen-px gap (beyond half the font box) between the outline and a -// label drawn with labelCloseToPath: false. -const LABEL_STANDOFF_PX = 8; +// --- refit deferral (drag responsiveness) ---------------------------------- +// Fitting an outline is bubblesets' marching-squares pass over an influence +// field; on a dense graph with avoidance on it costs far more than a frame. +// #paint runs on every sigma afterRender, so dragging a member used to re-fit +// 60×/second and the whole canvas crawled. +// +// A fit whose last run stayed inside this budget keeps re-fitting live — small +// graphs are unaffected and the hull tracks the node exactly as before. +const CHEAP_FIT_MS = 8; +// Past it, MEMBERSHIP and STYLE changes still fit immediately (they are +// discrete user actions), but POSITION changes coast on the cached hull and +// re-fit once motion stops. The hull lags the node mid-drag and snaps true on +// release, which is the trade the alternative cannot buy: a locked UI. +const REFIT_SETTLE_MS = 90; class BubbleSetLayer { /** @@ -51,6 +61,9 @@ class BubbleSetLayer { this.adapter = adapter; this.cache = cache; this.killed = false; + // Layer visibility, driven by the inspector's Overlays stack. Runtime + // state like heatmapEnabled — never persisted, resets with the adapter. + this.visible = true; /** @type {Map, avoidMembers: string[], opts: object}>} */ this.groups = new Map(); @@ -59,8 +72,14 @@ class BubbleSetLayer { /** @type {Map, graphHoles: Array>}>} */ this.outlines = new Map(); - this.rafHandle = null; + this.frame = new FrameCoalescer(() => this.#paint()); this.lastPaintSignature = null; + /** Last fit duration per group, in ms — drives refit deferral. */ + this.fitDurations = new Map(); + this.settleHandle = null; + // Set while a settled refit is running so #syncGroupOutline stops + // deferring and actually fits. + this.forceRefit = false; const sigma = adapter.sigma; // createCanvasContext returns the Sigma instance (fluent API); the canvas @@ -107,10 +126,24 @@ class BubbleSetLayer { }; } + /** + * Forget a group entirely. Without this a deleted group keeps its state slot + * and its cached outline, and #drawOutlines keeps painting a hull for a group + * that no longer exists. + * @param {string} group + */ + removeGroup(group) { + this.groups.delete(group); + this.outlines.delete(group); + this.fitDurations.delete(group); + this.scheduleRedraw(); + } + destroy() { if (this.killed) return; this.killed = true; - if (this.rafHandle !== null) cancelAnimationFrame(this.rafHandle); + this.frame.kill(); + clearTimeout(this.settleHandle); this.adapter.sigma.off('afterRender', this.renderHandler); // sigma.kill() may or may not remove custom layer canvases; remove() on // an already-detached node is a no-op, so drop ours defensively. @@ -118,12 +151,40 @@ class BubbleSetLayer { this.labelCanvas?.remove(); } + /** Show or hide every bubble, on screen and in both export paths. */ + setVisible(visible) { + if (this.visible === visible) return; + this.visible = visible; + this.scheduleRedraw(); + } + scheduleRedraw() { - if (this.killed || this.rafHandle !== null) return; - this.rafHandle = requestAnimationFrame(() => { - this.rafHandle = null; - this.#paint(); - }); + this.frame.schedule(); + } + + /** Did this group's last fit blow the frame budget? */ + #fitIsExpensive(group) { + if (this.forceRefit) return false; + return (this.fitDurations.get(group) ?? 0) > CHEAP_FIT_MS; + } + + /** + * Re-fit every deferred outline once the graph stops moving. Restarted on + * each deferred frame, so a continuous drag fits exactly once, on release. + */ + #scheduleSettledRefit() { + if (this.killed) return; + clearTimeout(this.settleHandle); + this.settleHandle = setTimeout(() => { + this.settleHandle = null; + if (this.killed) return; + this.forceRefit = true; + try { + this.#paint(); + } finally { + this.forceRefit = false; + } + }, REFIT_SETTLE_MS); } // ------------------------------------------------------------ group state @@ -170,41 +231,84 @@ class BubbleSetLayer { // zoom with members always enclosed. let outlinesChanged = false; const active = []; + // Viewport cull: 1.17 made the group count unbounded (auto-group mints up to + // 50), and zoomed into one of them the other 49 were still building a + // Path2D, stroking and filling entirely off-canvas. The bbox is graph-space + // and cached with the rings, so the test is four comparisons per group. + const view = viewportTransform(sigma); + const cullRect = view ? visibleGraphRect(view, width, height, CULL_MARGIN_PX) : null; for (const [group, state] of this.groups) { if (state.members.size === 0) { if (this.outlines.delete(group)) outlinesChanged = true; continue; } outlinesChanged = this.#syncGroupOutline(group, state) || outlinesChanged; - if (this.outlines.get(group)?.graphPoints.length) active.push([group, state]); + const cached = this.outlines.get(group); + if (!cached?.graphPoints.length) continue; + if (cullRect && cached.bbox && !rectsIntersect(cached.bbox, cullRect)) continue; + active.push([group, state]); } + // Hiding paints an empty frame rather than skipping the paint: a skip + // would leave the last outlines on a canvas sigma never clears for us. + const shown = this.visible ? active : []; + // Skip repainting when neither the outlines nor the view changed (sigma // re-renders on hover etc. without clearing custom layers). const signature = `${width}x${height}x${dpr}|${camera.x},${camera.y},${camera.ratio},${camera.angle}` + - `|${active.map(([g]) => g).join(',')}`; + `|${shown.map(([g]) => g).join(',')}`; if (!outlinesChanged && signature === this.lastPaintSignature) return; this.lastPaintSignature = signature; - this.#prepareCanvas(this.canvas, this.ctx, width, height, dpr); - this.#prepareCanvas(this.labelCanvas, this.labelCtx, width, height, dpr); - this.#drawOutlines(active, sigma); + prepareOverlayCanvas(this.canvas, this.ctx, width, height, dpr); + prepareOverlayCanvas(this.labelCanvas, this.labelCtx, width, height, dpr); + this.#drawOutlines(shown, sigma, dpr, view); } - /** Reproject each cached graph-space outline to viewport px and paint it. */ - #drawOutlines(active, sigma) { + /** + * Paint each cached graph-space outline under the camera transform. + * + * The path itself is built once per fit, in graph space, and every camera + * move is then a `setTransform` rather than a fresh projection and bezier + * path per point per frame — which is what made 50 groups over 62k points + * cost twelve milliseconds of paint JS per frame. Labels stay in viewport + * space (their font size and standoff are screen quantities), so only their + * one anchor point is projected. + */ + #drawOutlines(active, sigma, dpr, view) { + const defaults = this.cache.DEFAULTS.BUBBLE_GROUP_STYLE_TEMPLATE; + if (!view) return; + + this.ctx.save(); + try { + this.ctx.setTransform( + dpr * view.sx, 0, 0, dpr * view.sy, + dpr * view.tx, dpr * view.ty + ); + for (const [group, state] of active) { + const cached = this.outlines.get(group); + cached.path ??= ringsPath(cached.graphPoints, cached.graphHoles ?? []); + // A stroke under a scaled transform would thicken with the zoom. + this.#drawGroup(this.ctx, cached.path, state, defaults, Math.abs(view.sx)); + } + } finally { + this.ctx.restore(); + } + for (const [group, state] of active) { - // Reprojection assumes camera.angle === 0 (the app never rotates the camera). + if (!state.opts.label) continue; const cached = this.outlines.get(group); - const points = cached.graphPoints.map((p) => sigma.graphToViewport(p)); - const holes = (cached.graphHoles ?? []).map((h) => h.map((p) => sigma.graphToViewport(p))); - const defaults = this.cache.DEFAULTS.BUBBLE_GROUP_STYLE[group] ?? {}; - const drawn = this.#drawGroup(this.ctx, points, state, defaults, holes); - // Labels paint on the top canvas (afterLayer: "labels") so they read - // over member-node labels; the body/outline stayed on the bottom one. - if (drawn && state.opts.label) { - this.#drawLabel(this.labelCtx, points, state.opts, defaults); + const placement = state.opts.labelPlacement ?? defaults.labelPlacement ?? 'bottom'; + if (cached.anchorPlacement !== placement) { + // Sigma's map is a uniform scale plus a y flip, so the extreme vertex, + // the tangent angle and the outward normal all survive it — only the + // anchor's position needs projecting, once per frame. + cached.anchor = outlineLabelAnchor(cached.graphPoints, placement); + cached.anchorPlacement = placement; + } + if (cached.anchor) { + this.#drawLabel(this.labelCtx, projectAnchor(cached.anchor, view), state.opts, defaults); } } } @@ -221,7 +325,7 @@ class BubbleSetLayer { * opts: object, defaults: object}>} */ exportOutlines() { - const graph = this.adapter.graph; + if (!this.visible) return []; const sigma = this.adapter.sigma; const out = []; for (const [group, state] of this.groups) { @@ -230,12 +334,7 @@ class BubbleSetLayer { let graphPoints = cached?.graphPoints; let graphHoles = cached?.graphHoles ?? []; if (!graphPoints?.length) { - const visibleMembers = []; - for (const id of state.members.keys()) { - if (!graph.hasNode(id)) continue; - const attrs = graph.getNodeAttributes(id); - if (!attrs.hidden) visibleMembers.push({ id, attrs }); - } + const visibleMembers = this.#visibleNodes(state.members.keys()); if (visibleMembers.length === 0) continue; ({ outer: graphPoints, holes: graphHoles } = this.#fitGraphOutline(state, visibleMembers)); } @@ -245,7 +344,7 @@ class BubbleSetLayer { points: graphPoints.map((p) => sigma.graphToViewport(p)), holes: graphHoles.map((h) => h.map((p) => sigma.graphToViewport(p))), opts: state.opts, - defaults: this.cache.DEFAULTS.BUBBLE_GROUP_STYLE[group] ?? {}, + defaults: this.cache.DEFAULTS.BUBBLE_GROUP_STYLE_TEMPLATE, }); } return out; @@ -267,7 +366,7 @@ class BubbleSetLayer { try { ctx.setTransform(scale, 0, 0, scale, 0, 0); for (const { points, holes, opts, defaults } of groups) { - this.#drawGroup(ctx, points, { opts }, defaults, holes ?? []); + this.#drawGroup(ctx, ringsPath(points, holes ?? []), { opts }, defaults); } } finally { ctx.restore(); @@ -287,33 +386,15 @@ class BubbleSetLayer { try { ctx.setTransform(scale, 0, 0, scale, 0, 0); for (const { points, opts, defaults } of groups) { - if (opts.label) this.#drawLabel(ctx, points, opts, defaults); + if (!opts.label) continue; + const placement = opts.labelPlacement ?? defaults.labelPlacement ?? 'bottom'; + this.#drawLabel(ctx, outlineLabelAnchor(points, placement), opts, defaults); } } finally { ctx.restore(); } } - /** Resize (if needed) and clear a layer canvas, scaled to the device ratio. */ - #prepareCanvas(canvas, ctx, width, height, dpr) { - if (canvas.width !== width * dpr || canvas.height !== height * dpr) { - canvas.width = width * dpr; - canvas.height = height * dpr; - } - // Own the CSS display size too: sigma.createCanvasContext only sets - // position:absolute, and sigma.resize() (the sole writer of element.style - // width/height) runs once at construction — before this canvas exists — and - // early-returns on unchanged dimensions. Left unset, the canvas displays at - // its backing-store size (width*dpr CSS px), which is dpr* too large on a - // >1 DPR display, so the group lands in the wrong place until a panel toggle - // forces a real sigma resize. Setting it here keeps the overlay 1:1 with the - // WebGL layers regardless of sigma's resize timing or the display's DPR. - if (canvas.style.width !== `${width}px`) canvas.style.width = `${width}px`; - if (canvas.style.height !== `${height}px`) canvas.style.height = `${height}px`; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - ctx.clearRect(0, 0, width, height); - } - /** * Recompute the group's outline when its identity (members, positions, * style) changes. The fit is in graph space and zoom-invariant, so the @@ -322,23 +403,16 @@ class BubbleSetLayer { * @returns {boolean} true when the cached outline was replaced */ #syncGroupOutline(group, state) { - const graph = this.adapter.graph; const sigma = this.adapter.sigma; - const memberPositions = []; - const visibleMembers = []; - for (const id of state.members.keys()) { - if (!graph.hasNode(id)) continue; - const attrs = graph.getNodeAttributes(id); - if (attrs.hidden) continue; - visibleMembers.push({ id, attrs }); - // The ratio-1 radius feeds the fit (see #fitGraphOutline), so fold it - // into the checksum: a node-size change must invalidate the outline. - memberPositions.push({ - x: attrs.x, - y: attrs.y, - s: sigma.scaleSize(attrs.size ?? DEFAULTS.NODE.SIZE / 2, 1), - }); - } + const visibleMembers = this.#visibleNodes(state.members.keys()); + // The ratio-1 radius feeds the fit (see #fitGraphOutline), so fold it into + // the checksum: a node-size change must invalidate the outline. + const checksumPoint = ({ attrs }) => ({ + x: attrs.x, + y: attrs.y, + s: sigma.scaleSize(attrs.size ?? DEFAULTS.NODE.SIZE / 2, 1), + }); + const memberPositions = visibleMembers.map(checksumPoint); // Avoid-node MOVES reshape the fit too (their negative field pushes the // outline), so their positions/sizes fold into the key as well — without @@ -348,29 +422,24 @@ class BubbleSetLayer { // the manager passes no avoid members at all (see getAvoidMembers). const avoidanceRaw = Number(state.opts.avoidance ?? 1); const avoidanceActive = !Number.isFinite(avoidanceRaw) || avoidanceRaw !== 0; - const avoidPositions = []; - if (avoidanceActive) { - for (const id of state.avoidMembers) { - if (!graph.hasNode(id)) continue; - const attrs = graph.getNodeAttributes(id); - if (attrs.hidden) continue; - avoidPositions.push({ - x: attrs.x, - y: attrs.y, - s: sigma.scaleSize(attrs.size ?? DEFAULTS.NODE.SIZE / 2, 1), - }); - } - } + const avoidPositions = avoidanceActive + ? this.#visibleNodes(state.avoidMembers).map(checksumPoint) + : []; // membersKey/avoidKey are precomputed in #updateGroup, so the per-frame // cost here is the O(n) position checksums. Hidden flips stay correct: an // unhide changes visibleMembers.length, and a same-count hidden swap // changes the checksum. No camera/viewport term — the graph-space outline // is the same at every zoom. - const key = - `${state.membersKey}|${state.avoidKey}|${visibleMembers.length}` + - `|${styleKey(state.opts)}|${positionsChecksum(memberPositions)}` + - `|${positionsChecksum(avoidPositions)}`; + // + // Split in two so the two kinds of change can be treated differently: + // identity (what is in the group and how it is styled) always re-fits; + // position may be deferred while the graph is in motion. + const identityKey = + `${state.membersKey}|${state.avoidKey}|${visibleMembers.length}|${styleKey(state.opts)}`; + const positionKey = + `${positionsChecksum(memberPositions)}|${positionsChecksum(avoidPositions)}`; + const key = `${identityKey}|${positionKey}`; const cached = this.outlines.get(group); if (cached && cached.key === key) return false; @@ -379,7 +448,17 @@ class BubbleSetLayer { return cached !== undefined; } + // Position-only change on a group whose last fit blew the frame budget: + // keep painting the cached hull and re-fit once motion settles. Without a + // cached hull there is nothing to coast on, so the first fit always runs. + if (cached && cached.identityKey === identityKey && this.#fitIsExpensive(group)) { + this.#scheduleSettledRefit(); + return false; + } + + const started = performance.now(); const { outer: graphPoints, holes: graphHoles } = this.#fitGraphOutline(state, visibleMembers); + this.fitDurations.set(group, performance.now() - started); // computeOutlineGeometry repairs self-intersections, so a bad fit here is // a collapse to nothing or the rare unrepairable self-cross. Never let it // replace a good outline. @@ -389,15 +468,23 @@ class BubbleSetLayer { if (cached?.graphPoints.length) { this.outlines.set(group, { key, + identityKey, graphPoints: cached.graphPoints, graphHoles: cached.graphHoles ?? [], + bbox: cached.bbox ?? ringBBox(cached.graphPoints), }); return true; } // No prior good outline yet: stay absent until a clean fit appears. return this.outlines.delete(group); } - this.outlines.set(group, { key, graphPoints, graphHoles }); + this.outlines.set(group, { + key, + identityKey, + graphPoints, + graphHoles, + bbox: ringBBox(graphPoints), + }); return true; } @@ -416,33 +503,11 @@ class BubbleSetLayer { * holes: Array>}} graph-space rings */ #fitGraphOutline(state, visibleMembers) { - const graph = this.adapter.graph; - const sigma = this.adapter.sigma; - const { width, height } = sigma.getDimensions(); - const camera = sigma.getCamera().getState(); - const cx = width / 2; - const cy = height / 2; - const r = camera.ratio; - // graphToViewport divides the offset-from-centre by the ratio; multiply it - // back to land at the ratio-1 viewport (ratio cancels, so this is the same - // at any zoom). Node radius is taken at ratio 1 too. - const toRefRect = (attrs) => { - const v = sigma.graphToViewport({ x: attrs.x, y: attrs.y }); - const rx = cx + (v.x - cx) * r; - const ry = cy + (v.y - cy) * r; - return nodeViewportRect(rx, ry, sigma.scaleSize(attrs.size ?? DEFAULTS.NODE.SIZE / 2, 1)); - }; + const { toRefRect, toGraph } = this.#refSpace(); const memberRects = visibleMembers.map(({ attrs }) => toRefRect(attrs)); - const avoidRects = []; - for (const id of state.avoidMembers) { - if (!graph.hasNode(id)) continue; - const attrs = graph.getNodeAttributes(id); - if (attrs.hidden) continue; - avoidRects.push(toRefRect(attrs)); - } + const avoidRects = this.referenceRects(state.avoidMembers); const outlineOpts = { - virtualEdges: state.opts.virtualEdges, padding: state.opts.padding, corridor: state.opts.corridor, avoidance: state.opts.avoidance, @@ -455,41 +520,81 @@ class BubbleSetLayer { } // Reference-viewport → graph space (undo the ratio-1 mapping, then the // camera): the cache holds zoom-independent graph coords. - const toGraph = (p) => - sigma.viewportToGraph({ x: cx + (p.x - cx) / r, y: cy + (p.y - cy) / r }); return { outer: geometry.outer.map(toGraph), holes: geometry.holes.map((h) => h.map(toGraph)), }; } + /** + * The ratio-1 reference space the fit runs in: graph coords → the viewport + * the camera would show at ratio 1 (graphToViewport divides the + * offset-from-centre by the ratio; multiplying it back cancels the zoom, so + * the mapping is the same at any camera). Node radius is taken at ratio 1 + * too. `toGraph` is the inverse, for caching fitted rings zoom-free. + */ + #refSpace() { + const sigma = this.adapter.sigma; + const { width, height } = sigma.getDimensions(); + const { ratio } = sigma.getCamera().getState(); + const cx = width / 2; + const cy = height / 2; + return { + toRefRect: (attrs) => { + const v = sigma.graphToViewport({ x: attrs.x, y: attrs.y }); + const rx = cx + (v.x - cx) * ratio; + const ry = cy + (v.y - cy) * ratio; + return nodeViewportRect(rx, ry, sigma.scaleSize(attrs.size ?? DEFAULTS.NODE.SIZE / 2, 1)); + }, + toGraph: (p) => + sigma.viewportToGraph({ x: cx + (p.x - cx) / ratio, y: cy + (p.y - cy) / ratio }), + }; + } + + /** + * The visible nodes of an id set, with their attributes — the ONE definition + * of "a member that counts": present in the graph and not hidden. Three copies + * of this loop (#syncGroupOutline, exportOutlines, referenceRects) could + * disagree about that, and a disagreement means the fit and the export draw + * different hulls. + * + * @param {Iterable} ids + * @returns {Array<{id: string, attrs: object}>} + */ + #visibleNodes(ids) { + const graph = this.adapter.graph; + const out = []; + for (const id of ids) { + if (!graph.hasNode(id)) continue; + const attrs = graph.getNodeAttributes(id); + if (attrs.hidden) continue; + out.push({ id, attrs }); + } + return out; + } + + /** + * Reference-space rects for a set of node ids (hidden or missing nodes + * skipped) — exactly the rects a fit would consume, exposed so the + * bubble-set manager can measure a group's surroundings for layout-aware + * initial settings (bubble_tuning.js). + * + * @param {Iterable} ids + * @returns {Array<{x: number, y: number, width: number, height: number}>} + */ + referenceRects(ids) { + const { toRefRect } = this.#refSpace(); + return this.#visibleNodes(ids).map(({ attrs }) => toRefRect(attrs)); + } + /** * Paint one group's body + outline from viewport-projected rings (outer + * avoid holes, even-odd fill; stroke() outlines every ring). Returns true * when something was painted (false when the outline is too small), so the * caller knows whether a label belongs on the top canvas. */ - #drawGroup(ctx, points, { opts }, defaults, holes = []) { - if (!points || points.length < 2) return false; - - const path = new Path2D(); - // Rings paint as Catmull-Rom curves (smoothClosedPath — same control - // points the SVG export emits); rings too small to smooth fall back to - // the polyline. The polygon stays the geometric source of truth. - const addRing = (ring) => { - const segments = smoothClosedPath(ring); - if (!segments) { - path.moveTo(ring[0].x, ring[0].y); - for (let i = 1; i < ring.length; i++) path.lineTo(ring[i].x, ring[i].y); - path.closePath(); - return; - } - path.moveTo(segments[0].x0, segments[0].y0); - for (const s of segments) path.bezierCurveTo(s.c1x, s.c1y, s.c2x, s.c2y, s.x, s.y); - path.closePath(); - }; - addRing(points); - for (const hole of holes) if (hole.length >= 3) addRing(hole); + #drawGroup(ctx, path, { opts }, defaults, strokeScale = 1) { + if (!path) return false; ctx.save(); try { @@ -498,7 +603,9 @@ class BubbleSetLayer { ctx.fill(path, 'evenodd'); ctx.globalAlpha = opts.strokeOpacity ?? defaults.strokeOpacity ?? 1; ctx.strokeStyle = opts.stroke ?? defaults.stroke ?? '#403C53'; - ctx.lineWidth = OUTLINE_STROKE_WIDTH; + // The stroke is a screen quantity, so it has to survive the transform + // the path is painted under. + ctx.lineWidth = OUTLINE_STROKE_WIDTH / strokeScale; ctx.stroke(path); ctx.globalAlpha = 1; } finally { @@ -513,55 +620,131 @@ class BubbleSetLayer { * the outward normal; labelAutoRotate aligns on-path labels with the * outline tangent. labelOffsetX/Y stay additive in screen space. */ - #drawLabel(ctx, points, opts, defaults) { - const text = opts.labelText ?? defaults.labelText ?? ''; - if (!text) return; - const placement = opts.labelPlacement ?? defaults.labelPlacement ?? 'bottom'; - const closeToPath = opts.labelCloseToPath ?? defaults.labelCloseToPath ?? true; - const autoRotate = opts.labelAutoRotate ?? defaults.labelAutoRotate ?? true; - const anchor = outlineLabelAnchor(points, placement); - if (!anchor) return; - - const fontSize = opts.labelFontSize ?? defaults.labelFontSize ?? 12; - const padding = opts.labelPadding ?? defaults.labelPadding ?? 2; - // Off-path labels clear the outline by half the padded font box plus a - // fixed gap, pushed along the outward normal (no-op for "center"). - const standoff = closeToPath ? 0 : fontSize / 2 + padding + LABEL_STANDOFF_PX; - const x = anchor.x + anchor.nx * standoff + (opts.labelOffsetX ?? 0); - const y = anchor.y + anchor.ny * standoff + (opts.labelOffsetY ?? 0); - - ctx.font = `${fontSize}px Arial, sans-serif`; + #drawLabel(ctx, anchor, opts, defaults) { + // Geometry from resolveBubbleLabel (bubble_geometry.js), so this and the SVG + // export cannot disagree about standoff, rotation or the background box. + // Measuring needs the font set first; the resolver hands back the same + // string it measured with. + const label = resolveBubbleLabel(anchor, opts, defaults, (text, font) => { + ctx.font = font; + return ctx.measureText(text).width; + }); + if (!label) return; + + ctx.font = label.font; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - const textWidth = ctx.measureText(text).width; - - // Rotation only makes sense hugging the path; "center" has no tangent. - const rotated = autoRotate && closeToPath && placement !== 'center'; - if (rotated) { + if (label.rotated) { ctx.save(); - ctx.translate(x, y); - ctx.rotate(anchor.angle); + ctx.translate(label.x, label.y); + ctx.rotate(label.angle); } - const lx = rotated ? 0 : x; - const ly = rotated ? 0 : y; - - if (opts.labelBackground ?? defaults.labelBackground) { - const radius = opts.labelBackgroundRadius ?? defaults.labelBackgroundRadius ?? 5; - ctx.fillStyle = opts.labelBackgroundFill ?? defaults.labelBackgroundFill ?? '#403C53'; + if (label.background) { + const bg = label.background; + ctx.fillStyle = bg.fill; ctx.beginPath(); - ctx.roundRect( - lx - textWidth / 2 - padding, - ly - fontSize / 2 - padding, - textWidth + 2 * padding, - fontSize + 2 * padding, - radius - ); + ctx.roundRect(bg.x, bg.y, bg.width, bg.height, bg.radius); ctx.fill(); } - ctx.fillStyle = opts.labelFill ?? defaults.labelFill ?? '#fff'; - ctx.fillText(text, lx, ly); - if (rotated) ctx.restore(); + ctx.fillStyle = label.fill; + ctx.fillText(label.text, label.lx, label.ly); + if (label.rotated) ctx.restore(); } } +/** + * Sigma's graph→viewport map as a canvas transform. It composes a uniform + * normalization with a uniform camera scale and a vertical flip — the app + * never rotates the camera, which the reprojection has always assumed — so + * two probes pin it exactly. + * + * @returns {{sx: number, sy: number, tx: number, ty: number}|null} null when + * the camera is degenerate (a zero or non-finite scale) + */ +function viewportTransform(sigma) { + const origin = sigma.graphToViewport({ x: 0, y: 0 }); + const unit = sigma.graphToViewport({ x: 1, y: 1 }); + const sx = unit.x - origin.x; + const sy = unit.y - origin.y; + if (!Number.isFinite(sx) || !Number.isFinite(sy) || sx === 0 || sy === 0) return null; + return { sx, sy, tx: origin.x, ty: origin.y }; +} + +/** Screen-space slack on the cull rect: a stroke straddles the ring and a + * label sits a standoff outside it, so a group just off-canvas can still paint + * into it. Generous on purpose — the cull is about the 49 groups that are + * nowhere near, not about shaving the boundary. */ +const CULL_MARGIN_PX = 200; + +function rectsIntersect(a, b) { + return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY; +} + +/** Graph-space bounds of a ring, for the viewport cull. */ +function ringBBox(points) { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const p of points) { + if (p.x < minX) minX = p.x; + if (p.x > maxX) maxX = p.x; + if (p.y < minY) minY = p.y; + if (p.y > maxY) maxY = p.y; + } + return { minX, minY, maxX, maxY }; +} + +/** + * The visible canvas as a GRAPH-space rect, inflated by `marginPx` of screen + * space (a stroke straddles the ring, and a label sits a standoff outside it). + */ +function visibleGraphRect(view, width, height, marginPx) { + const toGraphX = (vx) => (vx - view.tx) / view.sx; + const toGraphY = (vy) => (vy - view.ty) / view.sy; + const xs = [toGraphX(-marginPx), toGraphX(width + marginPx)]; + const ys = [toGraphY(-marginPx), toGraphY(height + marginPx)]; + return { + minX: Math.min(...xs), maxX: Math.max(...xs), + minY: Math.min(...ys), maxY: Math.max(...ys), + }; +} + +/** @returns {{x: number, y: number, angle: number, nx: number, ny: number}|null} */ +function projectAnchor(anchor, view) { + return { + ...anchor, + x: anchor.x * view.sx + view.tx, + y: anchor.y * view.sy + view.ty, + // The flip mirrors the tangent and the normal along y. + angle: view.sy < 0 ? -anchor.angle : anchor.angle, + ny: view.sy < 0 ? -anchor.ny : anchor.ny, + }; +} + +/** + * One Path2D over an outer ring and its holes (filled even-odd). Rings paint + * as Catmull-Rom curves (smoothClosedPath — the same control points the SVG + * export emits); rings too small to smooth fall back to the polyline. The + * polygon stays the geometric source of truth. + * + * @returns {Path2D|null} null for a ring too short to be a shape + */ +function ringsPath(outer, holes = []) { + if (!outer || outer.length < 2) return null; + const path = new Path2D(); + const addRing = (ring) => { + const segments = smoothClosedPath(ring); + if (!segments) { + path.moveTo(ring[0].x, ring[0].y); + for (let i = 1; i < ring.length; i++) path.lineTo(ring[i].x, ring[i].y); + path.closePath(); + return; + } + path.moveTo(segments[0].x0, segments[0].y0); + for (const s of segments) path.bezierCurveTo(s.c1x, s.c1y, s.c2x, s.c2y, s.x, s.y); + path.closePath(); + }; + addRing(outer); + for (const hole of holes) if (hole.length >= 3) addRing(hole); + return path; +} + export { BubbleSetLayer }; diff --git a/src/graph/bubble_sets.js b/src/graph/bubble_sets.js index dab86ce..02c3aa8 100644 --- a/src/graph/bubble_sets.js +++ b/src/graph/bubble_sets.js @@ -1,7 +1,100 @@ +import {bubbleGroupStyle} from "../config.js"; +import {suggestGroupGeometry} from "./bubble_tuning.js"; import {StaticUtilities} from "../utilities/static.js"; import {detectCommunities as computeCommunityAssignments} from "./communities.js"; -import {clampPopoverLeft} from "../utilities/popover_position.js"; import {Popup} from "../utilities/popup.js"; +import {renderGroupList, syncGroupRows} from "../managers/group_list.js"; + +// Community detection asks for a target group count. Two is the smallest split +// worth drawing; the upper bound keeps a typo ("500") from minting hundreds of +// bubbles nobody asked for. +// Cost of fitting a hull that routes around non-members: ~6 µs per +// (member × avoid) pair, measured 2026-08-07 — see the table in config.js +// beside AVOID_FIT_CONFIRM_MS. Rough on purpose: the price of being wrong is +// one extra confirm, never a freeze the user did not agree to. +const AVOID_FIT_MS_PER_PAIR = 0.006; + +/** + * The per-group style controls, keyed by the human-readable label that travels + * in `data-property` ("Bubble Set