commit 631f089a90df621b068edaf202a33ff641e31c96 Author: Raven Scott Date: Mon Jul 27 01:11:49 2026 -0400 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db4682f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.DS_Store +*.log +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..da3d8b8 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# BridgeSwarm Website + +Official marketing site, user guide, and developer documentation for +**BridgeSwarm** — a browser extension + native messaging host that bridges +the [Hyperswarm](https://github.com/holepunchto/hyperswarm) P2P stack into +ordinary browsers. + +Live at **[bridgeswarm.dev](https://bridgeswarm.dev)**. Source code for the +product itself lives at +[git.ssh.surf/snxraven/BridgeSwarm](https://git.ssh.surf/snxraven/BridgeSwarm). + +This repository is a static, multi-page site (Vite + vanilla HTML/CSS/JS — +no framework, no client-side router). It is owned and published by +**HoneyPeer, LLC** and licensed under **AGPL-3.0** (see [License](#license)). + +## Quick start + +Requires Node.js 18+. + +```bash +npm install # install dependencies +npm run dev # start the Vite dev server → http://localhost:5174 +npm run build # produce a static production build in dist/ +npm start # serve the production build → http://localhost:4174 +``` + +Other scripts: + +```bash +npm run preview # Vite's own preview server for a local build (port 5174) +npm run serve # alias for `npm start` +``` + +| Script | Purpose | Port | +| --- | --- | --- | +| `npm run dev` | Vite dev server with hot reload | `5174` | +| `npm run build` | Builds every page in `vite.config.js` into `dist/` | — | +| `npm start` / `npm run serve` | Serves `dist/` via `server.mjs` (static files + generated `sitemap.xml`) | `4174` | + +`server.mjs` also proxies `/api/releases` to the Gitea releases API for the +[Releases](https://bridgeswarm.dev/releases) page, and excludes noindex stub +pages (like `/legal/cookies`) from the generated sitemap. + +## Content map + +``` +index.html Marketing home page +download.html Download / install instructions +releases.html Release history (pulled from Gitea) +community.html Community & support links +learn/ "How it works" walkthrough +use-cases/ Use case gallery + +docs/ Documentation + index.html Docs overview + quickstart.html Quick start guide + user-guide.html End-user guide (extension + host) + developer-guide.html Building on top of BridgeSwarm + architecture.html System architecture (extension ↔ host ↔ Hyperswarm) + api.html API reference + data.html Data API (Hypercore/Hyperbee/Hyperdrive/Autobase/Hyperdb) + protocols.html HRPC / protocol details + capabilities.html Capability packs (bare-media, bare-ffmpeg, etc.) + examples.html Example apps / local examples server + security.html Security model + faq.html FAQ + release.html Release channels & artifacts + +legal/ Legal hub — see below + index.html Overview of all policies + terms.html Terms of Service + eula.html End User License Agreement (extension + host) + aup.html Acceptable Use Policy + privacy.html Privacy Policy (local-first, no telemetry) + cookies.html Stub → redirects to /legal/privacy#cookies (noindex) + license.html AGPL-3.0 explainer, §13 network-use, source link + copyright.html Copyright & DMCA notice procedure + disclaimer.html "As-is" disclaimer & limitation of liability + security.html Vulnerability / responsible-disclosure policy + +src/ + js/ site.js (shared chrome/nav/footer/theme), home.js, releases.js + styles/ global.css, docs.css, legal.css, home.css, theme-light.css + +public/ + assets/ + brand/ OG/Twitter card images, hero backgrounds, brand colors.css + favicons/ Favicons, site.webmanifest, browserconfig.xml + logo/ BridgeSwarm icon & wordmark, all sizes/variants + social/ Social preview assets + robots.txt + +vite.config.js Multi-page build config (all HTML entry points) + dev proxy +server.mjs Production static server + sitemap generator +``` + +Every HTML page shares the same chrome, injected at runtime by +`src/js/site.js` via `data-nav` / `data-footer` (and `data-docs-nav` / +`data-legal-nav` for sidebar sections), so navigation, footer, and +dark/light theme toggling stay consistent across the whole site without a +build-time templating layer. + +## Brand assets + +Logos, icons, wordmarks, and social card images live under +[`public/assets/`](public/assets): + +- `public/assets/logo/` — BridgeSwarm icon & wordmark (light/dark/mono/teal variants, multiple sizes) +- `public/assets/brand/` — OG/Twitter card images, hero backgrounds, and `colors.css` (brand color tokens) +- `public/assets/favicons/` — favicons and PWA manifest + +## License & ownership + +BridgeSwarm — the browser extension, the native messaging host, and this +website's source — is owned, operated, and engineered by **HoneyPeer, LLC** +(DeKalb County, Georgia, USA), and licensed under the +**GNU Affero General Public License v3.0 (AGPL-3.0)**. See +[`/legal/license`](https://bridgeswarm.dev/legal/license) for the full +open-source notice, including AGPL §13 network-use obligations, and +[`/legal/`](https://bridgeswarm.dev/legal/) for the complete set of legal +policies (Terms, EULA, AUP, Privacy, Copyright/DMCA, Disclaimer, Security). + +Legal contact: [legal@honeypeer.com](mailto:legal@honeypeer.com) diff --git a/community.html b/community.html new file mode 100644 index 0000000..44a2b2a --- /dev/null +++ b/community.html @@ -0,0 +1,228 @@ + + + + + + + + +Community & Support — BridgeSwarm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +

Source, support, and who's behind BridgeSwarm

+

BridgeSwarm is source-available under AGPL-3.0, self-hosted on Gitea, and built by a small team that also uses it daily.

+
+
+ +
+
+
+ +

Source repository

+

The full history, issues, and CI pipeline live on our self-hosted Gitea instance — not GitHub.

+
+
+

git.ssh.surf/snxraven/BridgeSwarm

+

Extension, native host, examples, branding package, and documentation — one monorepo.

+
git clone https://git.ssh.surf/snxraven/BridgeSwarm.git
+
+
+
+ +
+
+
+ +

Set up a dev environment

+
+ +
+
+

Get running

+
git clone https://git.ssh.surf/snxraven/BridgeSwarm.git
+cd BridgeSwarm
+npm run setup
+# Load unpacked: extension/
+

Run npm run examples to serve the demo gallery at 127.0.0.1:4173, and npm run sync:examples after editing anything under examples/.

+
+
+

Where changes tend to land

+
    +
  • extension/ — MV3 background, content script, Control Center UI
  • +
  • native-host/host.js, messenger.js, capability packs
  • +
  • examples/ — source of truth for every demo
  • +
  • docs/ — architecture, API, data, capabilities, Protomux, HRPC references
  • +
+
+
+

Please open an issue before a large change — especially anything touching the native messaging protocol, extension IDs, or the install scripts, since those have hard-coded coexistence checks against other tools. Build/verification commands and CI behavior are documented in the repository README.

+
+
+ +
+ +
+ +
+
+
+ +

HoneyPeer, LLC

+
+
+

BridgeSwarm is owned and engineered by HoneyPeer, LLC, based in DeKalb County, Georgia, USA. For legal, licensing, or press inquiries, reach us at legal@honeypeer.com.

+
+
+
+ +
+
+
+ +

AGPL-3.0

+
+
+
§
+
+

BridgeSwarm is released under the GNU Affero General Public License v3.0. In practice, that means:

+
    +
  • You can use, study, modify, and redistribute BridgeSwarm freely.
  • +
  • If you modify BridgeSwarm and let others interact with it over a network — the AGPL's key difference from plain GPL — you must make your modified source available to those users (AGPL §13).
  • +
  • Derivative works must remain under AGPL-3.0; there's no proprietary-relicensing path for forks.
  • +
  • Branding assets ship under the same license as the rest of the project.
  • +
+

This is a summary, not legal advice — read the full license text before shipping a modified deployment, and contact legal@honeypeer.com with licensing questions specific to your use case.

+
+
+
+
+ +
+
+
+

Join the swarm

+

Install it, break it, tell us on Discord — or send a patch.

+ +
+
+
+ +
+ +
+ + + + diff --git a/docs/api.html b/docs/api.html new file mode 100644 index 0000000..f5c5bb1 --- /dev/null +++ b/docs/api.html @@ -0,0 +1,333 @@ + + + + + + API Reference · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Build with BridgeSwarm

+

API Reference

+

+ The complete surface of window.BridgeSwarm: constructor options, instance methods, + browser-side events, and every request type the native host accepts via + BridgeSwarm.request(type, payload, options). Binary payloads are base64-encoded on the + wire in both directions. +

+ + + +

Message format

+
    +
  • Request (browser → host): { id?, type, payload }. The extension adds an id if you omit one; the host uses it to match the response.
  • +
  • Response (host → browser): { id, type: 'response', payload }. Payload is the command result: { ok: true, ... } or { ok: false, error: '...' }.
  • +
  • Event (host → browser): { type: 'event', event, payload }, forwarded to subscribed tabs and delivered as a bridge-swarm-event custom DOM event.
  • +
+
+ The Promise returned by BridgeSwarm.request() almost always resolves — + check response.ok and read response.error on failure, rather than relying on + a rejected Promise (the exception is client-side timeouts). +
+ +

Constructor options

+
const swarm = new BridgeSwarm({
+  appName: 'my-app',   // string, optional — falls back to the extension's default app name
+  maxPeers: 50,        // number, optional — 0 or omitted means no limit (or extension default)
+});
+ + + + + + +
OptionTypeNotes
appNamestringPassed to Hyperswarm on the host; if omitted, the extension's configured default app name is used.
maxPeersnumberCap on peer connections for this swarm. 0 means no limit. Falls back to the extension's default max peers.
+

+ Firewalling is not a constructor option and is never a JS predicate function — it is + host-managed state, set with swarm.setFirewall(...) after construction. +

+ +

Instance methods

+ + + + + + + + + + + + + + + + + + +
MethodReturnsDescription
BridgeSwarm.ready()Promise<constructor>Static method. Resolves once the extension has injected the API. Rejects after the configured default ready timeout (ms) if > 0; default is to wait indefinitely.
swarm.join(topic, opts?)PromiseJoins a topic (32-byte hex string). Triggers init once per swarm, then join.
swarm.leave(topic)PromiseLeaves a previously joined topic.
swarm.getPublicKey()Promise<string>Hex-encoded public key for this swarm (backed by the swarmKey request).
swarm.setFirewall({ mode, keys })Promisemode: off | allowlist | denylist; keys: public-key hex strings.
swarm.ban(publicKeyHex, banned?)PromiseBans (or unbans) a peer and updates the denylist. Also available as peerInfo.ban(banned?) on connection events.
swarm.setAutoReplicate({ enabled, coreKeyHex?, resourceId? })PromiseOpts new connections into automatic Hypercore replication; the host takes over those sockets.
swarm.connections()ArraySnapshot of current connection objects. Returns [] after destroy().
swarm.createProtomux(conn)Protomux | nullWraps a connection in a framed stream and returns a Protomux instance, or null if the bundle isn't loaded.
swarm.hrpcCall(connId, method, args?, { onChunk, timeoutMs })PromiseInvokes host HRPC: ping, notify, fetchStream, streamSum, duplex.
swarm.destroy()PromiseTears down the swarm and all its connections on the host.
BridgeSwarm.request(type, payload, opts?)Promise<response>Generic request bridge. opts.timeoutMs aborts and rejects with "Request timed out after N ms".
BridgeSwarm.capabilitiesobjectlist(), has(pack), call(pack, cmd, payload), on(event, fn).
BridgeSwarm.mediaobjectMedia pack batch helpers plus liveSession / attachLiveReceiver — see Media helpers.
+ +

Connection object

+ + + + + + + + + +
MemberNotes
conn.connIdHost-assigned identifier for this connection.
conn.write(data)Accepts a string or bytes; returns a Promise that resolves on success and rejects on host error (e.g. "Connection not found"). Also emits an error event.
conn.on('data', fn)fn(data: Uint8Array) — one event per decrypted frame.
conn.on('end', fn)Peer disconnected.
conn.on('error', fn)Transport-level error.
+ +

Browser events

+ + + + + +
Event (on window)When
bridge-swarm-host-disconnectThe native messaging port disconnected. Existing swarms/connections are stale — call destroy() and rejoin, or prompt a refresh. If the "Show notification when host disconnects" setting is on, the user also gets a browser notification.
+ +

Swarm lifecycle

+

All payloads include swarmId (chosen by the client).

+ + + + + + + + + + + + +
TypePayloadResponse
init{ swarmId, options? }appName, maxPeers{ ok }
join{ swarmId, topic, opts? }{ ok } or { ok: false, error }
leave{ swarmId, topic }{ ok }
swarmKey{ swarmId }{ ok, publicKey } (hex)
setFirewall{ swarmId, mode, keys }{ ok, mode, keys }
banPeer{ swarmId, publicKeyHex, banned? }{ ok, banned, publicKeyHex }
setAutoReplicate{ swarmId, enabled, coreKeyHex?, resourceId? }{ ok, enabled }
destroy{ swarmId }{ ok }
+ +

Connection requests & events

+

Events, host → browser:

+ + + + + + + + + + + +
EventPayload
connection{ connId, swarmId, peerInfo, autoReplicated? }peerInfo: { publicKey (hex), topics? (hex[]) }; page API adds peerInfo.ban()
data{ connId, swarmId, data } — data is base64
end{ connId, swarmId }
error{ connId, swarmId, message }
hrpc-chunk{ connId, swarmId, streamId, method, chunk }
hrpc-end{ connId, swarmId, streamId, method, result? }
hrpc-error{ connId, swarmId, streamId, method, message }
+

Requests, browser → host:

+ + + + + + +
TypePayloadResponse
write{ connId, data } — base64{ ok } or { ok: false, error }
destroyConnection{ connId }{ ok }
+ +

Connection attachment

+

+ After a connection is established, the browser can ask the host to "take over" it for Hypercore + replication or HRPC. The connection then stops forwarding to the browser. +

+ + + + + + + +
TypePayloadResponse
attachReplication{ connId, coreKeyHex?, resourceId? }{ ok } or { ok: false, error }
attachHrpc{ connId }{ ok } or { ok: false, error } — returns immediately; channel pairs once both sides attach (first RPC may wait up to 12s)
hrpcInvoke{ connId, method, args?, streamId? }ping, notify, fetchStream, streamSum, duplexUnary { ok, result } or streaming { ok, streamId, streaming: true } + events
+

Prefer swarm.hrpcCall(connId, method, args, { onChunk }) for streaming. Full detail: Protocols.

+ +

Data API

+

All commands return { ok: true, ... } on success or { ok: false, error } on failure. Binary data is base64-encoded.

+ + + + + + + + + + + + + + + + + + + + + + +
TypePayloadResponse
coreInfo{ ok, key (hex), length, writable }
coreAppend{ data } or { base64 }{ ok, length }
coreGet{ index }{ ok, data } (base64 or null)
beeGet{ key }{ ok, key?, value?, seq? } or { ok, value: null }
beePut{ key, value }{ ok }
beeDel{ key }{ ok }
driveGet{ path? } (default '/'){ ok, data }
drivePut{ path, data? } or { path, base64? }{ ok }
driveList{ path? }{ ok, entries } (array of { key, value })
driveDel{ path }{ ok }
autobaseAppend{ value? } or { data? }{ ok, length }
autobaseViewGet{ index }{ ok, data }
autobaseInfo{ ok, length, signedLength }
hyperdbGet{ collection, query }{ ok, doc }
hyperdbInsert{ collection, doc } — doc: { id, value }{ ok }
hyperdbDelete{ collection, query }{ ok }
hyperdbFindToArray{ collectionOrIndex, query?, limit?, reverse? }{ ok, docs }
hyperdbFlush{ ok }
+

Named-resource variants (coreOpen, beeOpen, etc.) and full examples: Data API.

+ +

Capabilities

+ + + + + + + + + +
TypePayloadResponse
capabilities.list{}{ ok, packs }
capabilities.has{ pack }{ ok, pack, has }
capability{ pack, cmd, payload }pack-specific
media.info / media.imageTransform / media.extractFrame / media.transcode / media.cancel / …command payloadsee Capabilities
media.encodeStart / encodePushFrame / encodePush / encodeSubscribe / encodeStoplive session payload{ ok, sessionId, … } + cap-chunk segments
+

Capability events (broadcast to all subscribed tabs, no swarmId filter):

+ + + + + + + +
EventPayload
cap-chunk{ pack, jobId/sessionId, kind?, seq?, index?, data?, progress?, bytes? }
cap-end{ pack, jobId/sessionId, … }
cap-error{ pack, jobId/sessionId, message }
+ +

Examples server

+

+ Toggled from Control Center Settings (examplesServerEnabled / examplesServerPort + in bridgeSwarmSettings). The background service worker sends these — pages typically + don't call them directly. +

+ + + + + + + +
TypePayloadResponse
examplesServer.start{ host?, port? } — default 127.0.0.1:4173{ ok, running, url, root, … }
examplesServer.stop{}{ ok, running: false, … }
examplesServer.status{}{ ok, running, url, rootFound, … }
+ +

Host snapshot

+ + + + + +
TypePayloadResponse
host.snapshot{}{ ok, version, storagePath, packs, examples, swarmCount, connectionCount, swarms: [{ swarmId, publicKey, firewall, autoReplicate, peerCount, connIds }] }
+

Read-only inventory used by the Control Center; not required by page apps.

+ +

Media helpers

+
await BridgeSwarm.media.info({ dataBase64, filename })
+await BridgeSwarm.media.imageTransform({ dataBase64, maxWidth: 640, mimetype: 'image/webp' })
+await BridgeSwarm.media.extractFrame({ dataBase64, frameIndex: 0 })
+await BridgeSwarm.media.transcode({ dataBase64, format: 'webm' }, { onProgress })
+await BridgeSwarm.media.cancel(jobId)
+
+const live = BridgeSwarm.media.liveSession(videoEl)
+await live.start({ width: 640, height: 360, fps: 10, ingest: 'frames' })
+await live.pushFrame(canvas)
+await live.subscribe({ swarm: { connIds: [conn.connId] } })
+await live.stop()
+
+BridgeSwarm.media.attachLiveReceiver(conn, videoEl)
+

Full behavior, defaults, and limits: Capabilities.

+ + +
+
+
+ +
+ + + diff --git a/docs/architecture.html b/docs/architecture.html new file mode 100644 index 0000000..9779a32 --- /dev/null +++ b/docs/architecture.html @@ -0,0 +1,292 @@ + + + + + + Architecture · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Build with BridgeSwarm

+

Architecture

+

+ BridgeSwarm is split into two processes that never trust each other implicitly: a browser + extension that only ever sees what a page is allowed to send, and a native host that runs the real + Hyperswarm stack with your user's OS-level privileges. This page documents every component, how + messages and events flow between them, the connection lifecycle, the local examples server, and the + live media encode pipeline. +

+ + + +

Layers

+
+flowchart TB
+  subgraph Browser["Desktop browser"]
+    Page["Web page: window.BridgeSwarm"]
+    CS["content.js"]
+    BG["background.js (service worker)"]
+    UI["dashboard.js Control Center - Port + autosave settings"]
+  end
+
+  subgraph Host["Native messaging host (Bare)"]
+    MSG["messenger.js - 4-byte LE + JSON"]
+    H["host.js"]
+    SW["Hyperswarm"]
+    DATA["Corestore, Hypercore, Hyperbee, Hyperdrive, Autobase, Hyperdb"]
+    MUX["Protomux + HRPC"]
+    CAP["capabilities/ - media + live encode"]
+    EX["examples-server.js - 127.0.0.1:4173"]
+  end
+
+  Page <-->|postMessage| CS
+  CS <-->|runtime.sendMessage| BG
+  UI -->|"storage.local bridgeSwarmSettings"| BG
+  BG <-->|"connectNative com.bridgeswarm"| MSG
+  MSG --> H
+  H --> SW
+  H --> DATA
+  H --> MUX
+  H --> CAP
+  H --> EX
+  SW <-->|Noise P2P| Peer["Remote peers"]
+        
+ +

Four layers, top to bottom:

+
    +
  1. Page — your application code, talking only to window.BridgeSwarm.
  2. +
  3. Extension (MV3) — content script + background service worker + Control Center UI. Owns no P2P state itself; it's a relay and settings store.
  4. +
  5. Native messaging transport — a length-prefixed JSON pipe between the browser process and a separate OS process.
  6. +
  7. Native host (Bare) — runs Hyperswarm, all Hyper* data structures, Protomux/HRPC attachment, capability packs, and the optional examples HTTP server.
  8. +
+ +

Components

+

Extension

+ + + + + + + + + +
ComponentRole
background.jsMaintains the native messaging port (chrome.runtime.connectNative('com.bridgeswarm')); tracks pending requests by id; routes swarm events to the tabs that own each swarmId; broadcasts capability events to subscribed tabs; reconnects with backoff on disconnect; starts/stops the examples server per settings.
content.jsReads settings; skips injection on file:// if configured; otherwise injects api.js, framed-stream.js, and protomux-bundle.js in order; bridges page ↔ background via window.postMessage / chrome.runtime.sendMessage.
Control Center (dashboard.html/dashboard.js)Overview · Swarms · Connections · Activity · Settings, over a live bridgeswarm-dashboard Port; settings save on change.
Options (options.html)Opens the Control Center's Settings tab in a full browser tab.
Injected scripts (page context)api.js exposes window.BridgeSwarm, BridgeSwarm.request, and BridgeSwarm.capabilities / BridgeSwarm.media; merges constructor options with injected defaults.
+ +

Native host

+ + + + + + + + +
ComponentRole
index.mjsEntry point. Loads bare-process, registers default capability packs (media/ffmpeg), starts the messenger and host.
messenger.jsChrome/Firefox native messaging framing: 4-byte little-endian length + UTF-8 JSON. Max message size host → browser is 1 MB.
host.jsSwarm lifecycle, peer firewall/ban, auto-replicate, connection attachment (Protomux/HRPC), Hyper* data API, capability dispatch, examples-server control.
examples-server.jsOptional bare-http1 static server on 127.0.0.1:4173 serving the synced examples/ tree (or BRIDGESWARM_EXAMPLES_DIR).
+ +

Wire protocol

+
    +
  • Wire format (host ↔ browser): 4-byte LE unsigned length + UTF-8 JSON. Binary I/O — no CRLF translation.
  • +
  • Message kinds: Requests { id?, type, payload } · Responses { id, type: 'response', payload } · Events { type: 'event', event, payload }.
  • +
+ +

Request/response flow

+
+sequenceDiagram
+  participant Page
+  participant ContentScript
+  participant Background
+  participant NativeHost
+
+  Page->>ContentScript: postMessage bridge-swarm-bridge
+  ContentScript->>Background: runtime.sendMessage action send
+  Background->>NativeHost: port.postMessage id, type, payload
+  NativeHost->>NativeHost: handleMessage -> reply(result)
+  NativeHost->>Background: messenger.send id, type response
+  Background->>ContentScript: sendResponse payload
+  ContentScript->>Page: dispatchEvent bridge-swarm-bridge-response
+        
+

+ BridgeSwarm.request(type, payload) builds the payload; the background service worker + assigns id and resolves the matching pending Promise once the host replies. +

+ +

Event flow

+
+sequenceDiagram
+  participant NativeHost
+  participant Background
+  participant ContentScript
+  participant Page
+
+  NativeHost->>NativeHost: emit(event, payload)
+  NativeHost->>Background: messenger.send type event
+  alt swarm-scoped event
+    Background->>ContentScript: tabs.sendMessage only tabs owning swarmId
+  else capability / global event
+    Background->>ContentScript: tabs.sendMessage all subscribed tabs
+  end
+  ContentScript->>Page: dispatchEvent bridge-swarm-event
+  Page->>Page: api.js _onEvent connection data end error cap-*
+        
+ +

Connection lifecycle

+
+stateDiagram-v2
+  [*] --> Init: new BridgeSwarm + join(topic)
+  Init --> Discovering: host Hyperswarm.join
+  Discovering --> Connected: peer Noise handshake
+  Connected --> DataForward: page conn.write / data events
+  Connected --> Attached: attachReplication or attachHrpc
+  DataForward --> Closed: end / error / destroy
+  Attached --> Closed: end / destroy
+  Closed --> [*]
+        
+
    +
  1. Init swarmjoin(topic) triggers init (once) then join. The host creates a Hyperswarm instance and joins the topic.
  2. +
  3. Connection — the host allocates a connId, stores the socket, and emits connection with connId, swarmId, peerInfo.
  4. +
  5. Dataconn.write sends a write request (base64); the host forwards raw socket bytes back as data events — unless the connection is attached.
  6. +
  7. AttachmentattachReplication / attachHrpc stops browser data forwarding; the host owns a Protomux instance on that socket instead (for Hypercore replication or HRPC). At most one connection per peer per swarm is kept.
  8. +
+ +

Examples server flow

+
+sequenceDiagram
+  participant UI as Options / Dashboard
+  participant BG as background.js
+  participant Host as Native host
+  participant Browser as Browser tab
+
+  UI->>UI: toggle examplesServerEnabled (autosave bridgeSwarmSettings)
+  UI->>BG: storage.onChanged
+  BG->>Host: examplesServer.start host 127.0.0.1 port 4173
+  Host->>Host: bare-http1 serve examples/
+  Host-->>BG: running url http://127.0.0.1:4173/
+  Browser->>Host: GET /live-encode/
+  Note over Browser: content script injects BridgeSwarm on http origin
+        
+

+ The examples server exists purely so demos share a real HTTP origin (required for content-script + injection) without you needing to stand up your own dev server. It is bound to + 127.0.0.1 only. See Examples for the full demo catalog. +

+ +

Live media encode path

+
+flowchart LR
+  Cam["Page capture - canvas / getDisplayMedia"] -->|"JPEG frames or WebM slices"| API["BridgeSwarm.media.liveSession"]
+  API -->|"encodePushFrame via NMH"| Enc["host bare-ffmpeg - VP9 / WebM"]
+  Enc -->|"cap-chunk kind: segment"| MSE["Page MSE player"]
+  Enc -->|"BSML on socket"| Peer["Remote peer conn"]
+  Enc -->|archive| File["cap-jobs/sessionId/live.webm"]
+        
+

+ The page never runs ffmpeg — it captures frames (canvas snapshot, screen share, or camera), compresses + them to JPEG client-side, and pushes them to the host under the native-messaging size limit. The host + encodes real VP9/WebM with bare-ffmpeg and can simultaneously: stream MSE segments back to + the originating page, write BSML-framed binary chunks onto peer Hyperswarm sockets for P2P fan-out, and + archive the finished file under cap-jobs/<sessionId>/live.webm. Full command + reference in Capabilities. +

+ +
+ Want the request/response shape for every message type shown above? See the + API Reference. +
+ + +
+
+
+ +
+ + + diff --git a/docs/capabilities.html b/docs/capabilities.html new file mode 100644 index 0000000..ae77d15 --- /dev/null +++ b/docs/capabilities.html @@ -0,0 +1,223 @@ + + + + + + Capabilities · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Build with BridgeSwarm

+

Capabilities

+

+ BridgeSwarm exposes selected Bare + native APIs to the page as capability packs. This is intentionally curated — not + every bare-* package, only what fits the browser + native-messaging model. The default + host includes the media pack (bare-media + bare-ffmpeg); see + FAQ for what's bundled but not yet exposed. +

+ + + +

Page API

+
await BridgeSwarm.capabilities.list()     // e.g. ['media']
+await BridgeSwarm.capabilities.has('media')
+
+// Generic dispatch
+await BridgeSwarm.capabilities.call('media', 'info', { path: '…' })
+
+// Batch wrappers
+await BridgeSwarm.media.info({ dataBase64, filename })
+await BridgeSwarm.media.imageTransform({ dataBase64, maxWidth: 640, mimetype: 'image/webp' })
+await BridgeSwarm.media.extractFrame({ dataBase64, frameIndex: 0 })
+await BridgeSwarm.media.transcode({ dataBase64, format: 'webm' }, { onProgress })
+await BridgeSwarm.media.cancel(jobId)
+
+// Live encode (host bare-ffmpeg -> MSE)
+const live = BridgeSwarm.media.liveSession(videoEl)
+await live.start({ width: 640, height: 360, fps: 10, ingest: 'frames' })
+await live.pushFrame(canvas)
+await live.subscribe({ swarm: { connIds: [conn.connId] } })
+await live.stop()
+ +

Streaming events

+

Streaming results use host events, kept under the ~1 MB native-messaging limit:

+ + + + + + + +
EventPayload
cap-chunk{ pack, jobId/sessionId, kind?, index?, data? (base64), progress?, bytes? }
cap-end{ pack, jobId/sessionId, chunks?, path?, mimetype?, … }
cap-error{ pack, jobId/sessionId, message }
+

Live sessions emit cap-chunk with kind: 'segment' (WebM fragments for MSE) or kind: 'drop' under backpressure.

+
const off = BridgeSwarm.capabilities.on('cap-chunk', (p) => console.log(p))
+ +

Security

+

Filesystem access for capability packs is allowlisted under:

+
$BRIDGE_SWARM_STORAGE/cap-jobs/
+# default: ~/.bridgeswarm/bridge-swarm-storage/cap-jobs/
+

Absolute paths outside that tree are rejected by the host.

+ +

Pack — Media (default)

+

Shipped in the standard bridge-swarm-host artifact (bare-media + bare-ffmpeg + image codecs).

+ + + + + + + + + + + + + + + +
CommandBehavior
media.infoProbe image/video metadata
media.imageTransformDecode → optional resize/crop → encode (webp/jpeg/png); stream base64
media.extractFrameExtract one video frame → encode; stream base64
media.transcodeAsync job to webm/mp4/mkv (VP9+Opus where supported); progress events
media.encodeStartStart a live VP9/WebM session (ingest: frames|segments, egress page/swarm/file)
media.encodePushFramePush a JPEG/WebP frame (base64) into the live session
media.encodePushPush a MediaRecorder timeslice, or a frame alias
media.encodeSubscribeUpdate fan-out connIds / page egress mid-session
media.encodeStopFlush the encoder, finalize the archive, emit cap-end
media.cancelCancel a batch job or a live session
media.writeInput / media.readOutputChunked upload / download under cap-jobs/
+ +

Live encode notes

+
+flowchart LR
+  In["Page ingest - JPEG frames / WebM slices"] --> Host["media.encode* - bare-ffmpeg VP9"]
+  Host -->|"cap-chunk kind: segment"| PageOut["MSE on page"]
+  Host -->|"BSML socket write"| SwarmOut["Peer connections"]
+  Host --> FileOut["cap-jobs/session/live.webm"]
+        
+
    +
  • Chrome's native messaging host protocol caps messages at ~1 MB per message → ingest compressed JPEG frames (or WebM slices), not raw RGBA.
  • +
  • Defaults for demos: 640×360 @ 10fps; soft max around 720p @ 15fps; session max duration 10 minutes.
  • +
  • Egress modes: page (MSE segments back to the sender), swarm (BSML-framed binary written directly onto Hyperswarm sockets), file (cap-jobs/<sessionId>/live.webm).
  • +
  • Peer fan-out writes happen on the host socket (efficient for higher bitrates); the receiving page still reads via native messaging conn.on('data') unless you keep processing host-side.
  • +
  • Helpers: BridgeSwarm.media.liveSession(videoEl) and BridgeSwarm.media.attachLiveReceiver(conn, videoEl).
  • +
+
+ After installing or updating the host on macOS, run npm run repair:macos so + .bare addons (including ffmpeg) are re-extracted and code-signed. Then + fully quit the browser before testing — a running process can hold a stale, unsigned + addon in memory. +
+
await BridgeSwarm.capabilities.has('media')  // true
+

Demos: live-encode, media-demo, clip-studio.

+ +

Bundled for upcoming packs

+

These modules are dependencies of the default host but not yet exposed as page packs:

+ + + + + + + +
ModulePlanned pack
bare-fs (already used internally)fs — read/write/list under allowlisted roots
bare-sqlitesqlite — local DB under storage
bare-fetchnet — CORS-free host fetch
+

UI/mobile/build Bare packages (bare-gtk, bare-ios, bare-build, …) stay out of scope. Full module list: FAQ.

+ +

Host protocol

+ + + + + + + + +
TypePayloadResponse
capabilities.list{}{ ok, packs: string[] }
capabilities.has{ pack }{ ok, pack, has }
capability{ pack, cmd, payload }pack-specific
media.<cmd>same as payloadshortcut when the pack is installed
+

Unknown pack → { ok: false, error: "capability '…' not installed" }.

+ + +
+
+
+ +
+ + + diff --git a/docs/data.html b/docs/data.html new file mode 100644 index 0000000..ff21b84 --- /dev/null +++ b/docs/data.html @@ -0,0 +1,268 @@ + + + + + + Data API · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Build with BridgeSwarm

+

Data API

+

+ The native host runs a real Corestore, Hypercore, + Hyperbee, Hyperdrive, Autobase, and + Hyperdb — the same modules used server-side in the Holepunch ecosystem. Your page + calls into them with BridgeSwarm.request(type, payload[, options]). Optional + options.timeoutMs aborts the call after that many milliseconds. +

+ + + +

Storage

+

+ Storage lives under the BRIDGE_SWARM_STORAGE environment variable, or + ./bridge-swarm-storage relative to the host process if unset. In a standard install this + resolves to: +

+
~/.bridgeswarm/bridge-swarm-storage/
+

+ The host keeps one Corestore plus a set of default Hyper* instances built on top of + it. You can also open additional named resources (separate namespaces or specific + keys) and target them with resourceId on subsequent operations. +

+
+ Corestore storage is sealed to the underlying device/inode. Copying it across filesystems (rather than + moving it) can invalidate the seal and produce "Invalid device file, was modified." See + User Guide → Troubleshooting. +
+ +

Basic usage

+
const res = await BridgeSwarm.request('beeGet', { key: 'foo' });
+if (res.ok) console.log(res.value);
+ +

Named resources & resourceIds

+ + + + + + + + + +
TypePayloadResponse
coreOpen{ name? } or { keyHex } or { namespace, name }{ ok, resourceId, key, length, writable }
beeOpen{ name?, namespace? }{ ok, resourceId, key }
driveOpen{ name?, namespace? }{ ok, resourceId, key }
autobaseOpen{ name?, namespace?, viewName? }{ ok, resourceId, length }
hyperdbOpen{ name?, namespace? }{ ok, resourceId, key }
+

Pass resourceId on any data command below to target that resource instead of the default instance.

+
const opened = await BridgeSwarm.request('coreOpen', { name: 'my-log' });
+await BridgeSwarm.request('coreAppend', { resourceId: opened.resourceId, data: 'entry-1' });
+const info = await BridgeSwarm.request('coreInfo', { resourceId: opened.resourceId });
+// { ok, key, length, writable }
+ +

Hypercore

+

Append-only log. All commands accept an optional resourceId.

+ + + + + + + +
TypePayloadResponse
coreInfo{ resourceId? }{ ok, key (hex), length, writable }
coreAppend{ data | base64, resourceId? }{ ok, length }
coreGet{ index, resourceId? }{ ok, data } (base64 or null)
+
await BridgeSwarm.request('coreAppend', { data: JSON.stringify({ n: 1 }) });
+const first = await BridgeSwarm.request('coreGet', { index: 0 });
+console.log(atob(first.data));
+ +

Hyperbee

+

Key/value B-tree store, built on a Hypercore.

+ + + + + + + +
TypePayloadResponse
beeGet{ key, resourceId? }{ ok, key, value, seq } or { ok, value: null }
beePut{ key, value, resourceId? }{ ok }
beeDel{ key, resourceId? }{ ok }
+
await BridgeSwarm.request('beePut', { key: 'name', value: 'Alice' });
+const result = await BridgeSwarm.request('beeGet', { key: 'name' });
+console.log(result.value); // 'Alice'
+ +

Hyperdrive

+

P2P file system on top of Hyperbee + Hypercore.

+ + + + + + + + +
TypePayloadResponse
driveGet{ path?, resourceId? } (default '/'){ ok, data } (base64 or null)
drivePut{ path, data | base64, resourceId? }{ ok }
driveList{ path?, resourceId? }{ ok, entries }
driveDel{ path, resourceId? }{ ok }
+
await BridgeSwarm.request('drivePut', { path: '/notes/hello.txt', data: 'Hello, drive!' });
+const list = await BridgeSwarm.request('driveList', { path: '/notes' });
+console.log(list.entries); // [{ key: '/notes/hello.txt', value: {...} }]
+ +

Autobase

+

Multi-writer linearized log — useful when several peers append independently and you need a single, deterministic ordering.

+ + + + + + + +
TypePayloadResponse
autobaseAppend{ value | data, resourceId? }{ ok, length }
autobaseViewGet{ index, resourceId? }{ ok, data }
autobaseInfo{ resourceId? }{ ok, length, signedLength }
+ +

Hyperdb

+

+ Schema-based P2P database, generated from a Hyperschema + definition (npm run build:hyperdb in the BridgeSwarm repo). The default schema ships two collections: +

+
    +
  • @bridgeswarm/records{ id, value } (strings)
  • +
  • @bridgeswarm/notes{ id, title, body } (strings)
  • +
+ + + + + + + + + +
TypePayloadResponse
hyperdbGet{ collection, query, resourceId? }{ ok, doc }
hyperdbInsert{ collection, doc, resourceId? } — doc: { id, value }{ ok }
hyperdbDelete{ collection, query, resourceId? }{ ok }
hyperdbFindToArray{ collectionOrIndex, query?, limit?, reverse?, resourceId? }{ ok, docs }
hyperdbFlush{ resourceId? }{ ok }
+
await BridgeSwarm.request('hyperdbInsert', {
+  collection: 'records',
+  doc: { id: '1', value: 'hello hyperdb' },
+});
+const rows = await BridgeSwarm.request('hyperdbFindToArray', {
+  collectionOrIndex: 'records',
+  limit: 20,
+});
+console.log(rows.docs);
+ +

Connection attachment & auto-replicate

+ + + + + + + + +
TypePayloadResponse
attachReplication{ connId, coreKeyHex?, resourceId? }{ ok } — host takes over the socket for Hypercore replication (no longer forwarded to the page)
setAutoReplicate{ swarmId, enabled, coreKeyHex?, resourceId? }{ ok, enabled } — when enabled, every new connection auto-runs attachReplication; those sockets are not usable for page chat/Protomux. Prefer swarm.setAutoReplicate(...).
attachHrpc{ connId }{ ok } — see Protocols
hrpcInvoke{ connId, method, args?, streamId? }Unary { ok, result } or streaming { ok, streamId, streaming: true } + hrpc-chunk / hrpc-end / hrpc-error
+ +

Peer control

+ + + + + + +
TypePayloadResponse
setFirewall{ swarmId, mode: 'off'|'allowlist'|'denylist', keys: string[] }{ ok, mode, keys }
banPeer{ swarmId, publicKeyHex, banned? }{ ok, banned, publicKeyHex }
+

Prefer the page helpers: swarm.setFirewall(...), swarm.ban(publicKey), peerInfo.ban().

+ +
+ See Data API demo and + Auto-replicate demo in + the Examples gallery for these APIs in a working app. +
+ + +
+
+
+ +
+ + + diff --git a/docs/developer-guide.html b/docs/developer-guide.html new file mode 100644 index 0000000..a126c30 --- /dev/null +++ b/docs/developer-guide.html @@ -0,0 +1,455 @@ + + + + + + Developer Guide · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Build with BridgeSwarm

+

Developer Guide

+

+ This guide walks through everything you need to build a real application on + window.BridgeSwarm: the injected API's lifecycle, joining topics, working with + connections, the request/response surface, named Hyper* resources, peer control, Protomux and HRPC, + the media capability, packaging pages that depend on the extension, testing, error handling, + performance limits, and your AGPL obligations if you modify BridgeSwarm itself. +

+ + + +

Architecture, for builders

+

+ Your page never touches the network directly. It calls window.BridgeSwarm, injected by + the extension's content script, which relays through the background service worker to a native + messaging host running the actual Hyperswarm stack under + Bare. +

+
Page  ⇄(postMessage)⇄  content.js  ⇄(runtime.sendMessage)⇄  background.js  ⇄(native messaging)⇄  Native host
+

+ Requests are { id?, type, payload }; the extension assigns id if you omit + it. Responses are { id, type: 'response', payload } where payload is either + { ok: true, ... } or { ok: false, error }. Events are + { type: 'event', event, payload }, forwarded to subscribed tabs and delivered to the page + as a bridge-swarm-event custom event. See Architecture + for full sequence diagrams. +

+ +

window.BridgeSwarm lifecycle

+

+ Because injection happens asynchronously, a script that runs at page load may execute before the API + exists. Use BridgeSwarm.ready() to wait for it safely: +

+
// Promise-based
+const BridgeSwarmCtor = await BridgeSwarm.ready();
+
+// or callback-style
+BridgeSwarm.ready().then((swarm) => {
+  // swarm here is actually the constructor — see note below
+});
+

+ If the extension option Default ready timeout (ms) is set above 0, + ready() rejects after that many milliseconds with + "BridgeSwarm not available within N ms" — useful for showing a "please install the + extension" message. The default is 0, meaning wait indefinitely. +

+
const swarm = new BridgeSwarm({
+  appName: 'my-app',   // string — passed to Hyperswarm on the host
+  maxPeers: 50,        // number — cap on peer connections for this swarm; 0 = unlimited
+});
+

+ Omit either option and the extension's configured defaults (from Settings) are used. Note that + firewalling is not a constructor option or a JS predicate function — it's host-managed + state you set after construction with swarm.setFirewall(...). +

+ +

Joining topics

+
await swarm.join('my-topic');       // string — hashed/normalized to a 32-byte topic
+// or with a 32-byte hex string directly:
+await swarm.join('a1b2c3...'.padEnd(64, '0'));
+
+// leave later
+await swarm.leave('my-topic');
+

+ Under the hood this sends init (once per swarm) then join with + { swarmId, topic } to the host, which creates a Hyperswarm instance and joins the DHT + topic. Both anounce and lookup happen by default, so any two peers that join the same topic will find + each other. +

+ +

Connections API

+
swarm.on('connection', (conn, peerInfo) => {
+  // peerInfo: { publicKey: hex string, topics?: hex[] }
+  // peerInfo.ban(banned?) is added by the page API for convenience
+
+  conn.on('data', (data) => {
+    // data: Uint8Array (decoded from base64 on the wire)
+  });
+  conn.on('end', () => { /* peer disconnected */ });
+  conn.on('error', (err) => { /* transport-level error */ });
+
+  // conn.write returns a Promise: resolves on success, rejects on host error
+  conn.write('hello').catch((err) => console.error(err));
+});
+
+// Snapshot of current connections (e.g. to broadcast to everyone)
+for (const conn of swarm.connections()) {
+  conn.write('broadcast message');
+}
+

+ After swarm.destroy(), swarm.connections() returns []. Each + connection is already a Noise-encrypted, framed stream — you get one decrypted frame per + data event. +

+
+ Host disconnects happen. Listen for bridge-swarm-host-disconnect on + window to detect when the native messaging port drops. After this fires, existing + swarms/connections are stale — call swarm.destroy() and rejoin, or prompt the user to + refresh. +
+
window.addEventListener('bridge-swarm-host-disconnect', () => {
+  showBanner('Disconnected from BridgeSwarm — reconnecting…');
+});
+ +

BridgeSwarm.request types, overview

+

+ Everything that isn't covered by a dedicated method goes through the generic request bridge: +

+
const res = await BridgeSwarm.request(type, payload, { timeoutMs: 5000 });
+if (!res.ok) {
+  console.error('request failed:', res.error);
+}
+

Request families, at a glance (full payload/response shapes in the API Reference):

+ + + + + + + + + + + + + + + +
FamilyExamples
Swarm lifecycleinit, join, leave, swarmKey, destroy
Peer controlsetFirewall, banPeer, setAutoReplicate
Connectionswrite, destroyConnection
AttachmentattachReplication, attachHrpc, hrpcInvoke
HypercorecoreOpen, coreInfo, coreAppend, coreGet
HyperbeebeeOpen, beeGet, beePut, beeDel
HyperdrivedriveOpen, driveGet, drivePut, driveList, driveDel
AutobaseautobaseOpen, autobaseAppend, autobaseViewGet, autobaseInfo
HyperdbhyperdbOpen, hyperdbGet, hyperdbInsert, hyperdbDelete, hyperdbFindToArray, hyperdbFlush
Capabilitiescapabilities.list, capabilities.has, capability, media.*
Host inventoryhost.snapshot (read-only, used by the Control Center)
+

+ Every response follows the same shape: success payloads include ok: true plus + command-specific fields; failures are { ok: false, error: string }. The returned Promise + always resolves — check .ok rather than relying on a rejection, except for + timeouts (see Error handling). +

+ +

Named Hyper* resources

+

+ The host keeps one Corestore and a set of default Hyper* instances, but you can also open additional + named resources — separate namespaces or specific keys — and target them on + subsequent calls with resourceId: +

+
const opened = await BridgeSwarm.request('beeOpen', { name: 'chat-log' });
+// { ok, resourceId, key }
+
+await BridgeSwarm.request('beePut', {
+  resourceId: opened.resourceId,
+  key: 'message-1',
+  value: 'hello',
+});
+
+const got = await BridgeSwarm.request('beeGet', {
+  resourceId: opened.resourceId,
+  key: 'message-1',
+});
+

+ Omit resourceId to use the default instance for that data type. See the full list of + *Open commands in Data API. +

+ +

setFirewall and ban

+
// Allowlist mode — only these public keys may connect
+await swarm.setFirewall({ mode: 'allowlist', keys: ['abc123...'] });
+
+// Denylist mode — block specific keys, allow everyone else
+await swarm.setFirewall({ mode: 'denylist', keys: ['def456...'] });
+
+// Off — no filtering (default)
+await swarm.setFirewall({ mode: 'off', keys: [] });
+
+// Ban a peer you're already connected to
+await swarm.ban('abc123...');
+// or, from a connection event:
+swarm.on('connection', (conn, peerInfo) => {
+  // peerInfo.ban() bans and closes this peer
+});
+

Banning updates the denylist and is enforced on future connection attempts as well.

+ +

setAutoReplicate

+

+ Opt a swarm into automatically taking over every new connection socket for Hypercore replication — + no manual attachReplication call needed per peer: +

+
await swarm.setAutoReplicate({
+  enabled: true,
+  coreKeyHex: myCore.key,   // omit to replicate the default core
+  resourceId: undefined,    // or target a named resource instead
+});
+
+ Auto-replicated sockets are taken over by the host — they stop being usable for page chat/Protomux. + At most one connection per peer, per swarm, is kept. Use this for sync-only swarms, not swarms that + also need bidirectional page messaging on the same connections. +
+ +

createProtomux

+

+ BridgeSwarm injects a browser bundle of Protomux, compact-encoding (c), and + b4a so you can run your own message-oriented mini-protocols directly in the page, on top + of a raw connection: +

+
swarm.on('connection', (conn, peerInfo) => {
+  const mux = swarm.createProtomux(conn);
+  if (!mux) return; // bundle not loaded — logs a warning
+
+  const ch = mux.createChannel({ protocol: 'my-app/v1' });
+  const msg = ch.addMessage({
+    encoding: window.BridgeSwarmProtomux.c.string,
+    onmessage: (value) => console.log('got:', value),
+  });
+  ch.open();
+  msg.send('hello over protomux');
+});
+

+ createProtomux wraps the connection in BridgeSwarmFramedStream.wrapRawFrames(conn) + first, since each BridgeSwarm connection already delivers one decrypted Noise frame per chunk. Full + reference: Protocols. +

+ +

hrpcCall

+

+ For typed RPC (unary calls and streaming), BridgeSwarm's supported path is host HRPC, + not in-page protomux-rpc. Attach it on a connection, then call: +

+
await BridgeSwarm.request('attachHrpc', { connId: conn.connId });
+
+const pong = await swarm.hrpcCall(conn.connId, 'ping', { value: 'hi' });
+
+const sum = await swarm.hrpcCall(conn.connId, 'fetchStream', { count: 3 }, {
+  onChunk: (chunk) => console.log('chunk:', chunk),
+  timeoutMs: 20000,
+});
+

+ Both peers must attach HRPC on the same connection; the first invoke may wait up to 12 seconds for the + channel to pair. Full command spec: Protocols → HRPC. +

+ +

BridgeSwarm.media — live encode

+

+ The default host bundles bare-media + bare-ffmpeg, exposed as the + media capability pack. Beyond batch jobs (probe, transform, extract-frame, transcode), + it supports a live VP9/WebM encode session driven from the page: +

+
const live = BridgeSwarm.media.liveSession(videoEl);
+
+await live.start({ width: 640, height: 360, fps: 10, ingest: 'frames' });
+
+// Push compressed JPEG frames captured from a canvas — not raw RGBA
+await live.pushFrame(canvas);
+
+// Fan out the host-side encode to specific peer connections
+await live.subscribe({ swarm: { connIds: [conn.connId] } });
+
+await live.stop();
+

+ On the receiving side, BridgeSwarm.media.attachLiveReceiver(conn, videoEl) wires up MSE + playback of a peer's live stream. Defaults for demos are 640×360 @ 10fps, with a soft ceiling around + 720p @ 15fps and a 10-minute session cap. See Capabilities for + the full command list, event shapes, and security constraints. +

+ +

Settings injection

+

+ The content script reads bridgeSwarmSettings from extension storage and injects a + __BRIDGESWARM_DEFAULTS__ object into the page before api.js loads. + Constructor options you pass explicitly always take precedence; anything you omit falls back to these + injected defaults (default app name, max peers, timeouts, firewall). This is also how the + "Disable on file:// URLs" setting takes effect — the content script simply skips + injection entirely on matching origins. +

+ +

Packaging pages that depend on the extension

+
    +
  • Never rely on file://. Serve your app over http:// or https:// — even locally — so injection and same-origin behavior work correctly.
  • +
  • Feature-detect, don't assume. Always gate BridgeSwarm-dependent UI behind await BridgeSwarm.ready() (with a sane timeout) so your page degrades gracefully without the extension installed.
  • +
  • Handle host disconnects. Wire up the bridge-swarm-host-disconnect listener before you create any swarms.
  • +
  • Don't hardcode :4173 in production. That port is specific to the bundled examples server; ship your own app on whatever origin makes sense for you.
  • +
  • Ship your own protomux protocol strings. Namespace your protocol (and id, if used) so you don't collide with other BridgeSwarm-enabled apps sharing a topic space.
  • +
+ +

Testing with two tabs

+

+ Because each browser tab gets its own key pair, the fastest test loop for anything P2P is two tabs on + the same machine: +

+
    +
  1. Serve your page locally (or use the bundled examples server as a template — see Examples).
  2. +
  3. Open it in two tabs (or two profiles, for a cleaner peerInfo separation).
  4. +
  5. Join the same topic in both; watch the Control Center's Connections and Activity tabs while you interact.
  6. +
  7. Enable Debug logging in Settings for verbose console output during development.
  8. +
+ +

Capability allowlist under cap-jobs

+

+ Filesystem access for capability packs is restricted to one allowlisted tree: +

+
$BRIDGE_SWARM_STORAGE/cap-jobs/
+# default: ~/.bridgeswarm/bridge-swarm-storage/cap-jobs/
+

+ Absolute paths outside that tree are rejected by the host. Chunked upload/download helpers + (media.writeInput / media.readOutput) and live-encode file archives all + live under this directory. If you extend the host with your own capability pack, keep any file I/O + scoped the same way. +

+ +

Error handling

+
    +
  • Command failures resolve (not reject) as { ok: false, error: string } — always check .ok.
  • +
  • conn.write(data) returns a Promise that does reject on host error (e.g. "Connection not found"), and the connection also emits an error event — handle both, or at least .catch() the write.
  • +
  • Timeouts — pass { timeoutMs } as the third argument to BridgeSwarm.request (or via hrpcCall's options) to abort and reject with "Request timed out after N ms". Without an explicit value, the extension's configured default request timeout applies if set above 0.
  • +
  • Unknown capability packs return { ok: false, error: "capability '…' not installed" } rather than throwing.
  • +
  • Host disconnects invalidate all live swarms/connections — listen for bridge-swarm-host-disconnect and treat existing state as stale.
  • +
+ +

Performance limits (NMH ~1MB)

+
+ Chrome's native messaging host protocol caps messages from the host to the browser at roughly + 1 MB. This shapes several API design choices you should follow in your own code. +
+
    +
  • Binary payloads are base64-encoded on the wire, which adds ~33% overhead on top of the raw byte size — budget accordingly.
  • +
  • Stream large results as chunked events (cap-chunk / hrpc-chunk) instead of one giant response.
  • +
  • For live media, ingest compressed JPEG frames (or WebM timeslices) rather than raw RGBA frames from a canvas — raw frames blow past the limit almost immediately at any real resolution.
  • +
  • For large file transfers over Hyperdrive, prefer many small drivePut calls (or chunked capability I/O) over one enormous payload.
  • +
+ +

AGPL obligations

+

+ BridgeSwarm is licensed under AGPL-3.0. + The AGPL's key difference from GPL is its network clause (§13): if you modify BridgeSwarm itself (the + extension or the native host) and let others interact with your modified version over a + network, you must offer them the corresponding modified source. +

+
    +
  • Building ordinary pages/apps that merely use window.BridgeSwarm as an API is normal usage and does not, by itself, place your application under AGPL — the copyleft is triggered by distributing or network-serving a modified BridgeSwarm, not by writing a page that talks to it.
  • +
  • If you fork the extension or native host and offer it as a service (e.g. a rebranded install flow, a hosted relay, a modified capability pack), you must make your modified source available to users of that service.
  • +
  • Review the full license text and talk to counsel for your specific situation — this page is not legal advice.
  • +
+

Questions about licensing: legal@honeypeer.com. More detail in Security → AGPL.

+ + +
+
+
+ +
+ + + diff --git a/docs/examples.html b/docs/examples.html new file mode 100644 index 0000000..64066d0 --- /dev/null +++ b/docs/examples.html @@ -0,0 +1,219 @@ + + + + + + Examples · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Reference

+

Examples

+

+ BridgeSwarm ships twelve working demos that double as reference implementations for every major + feature. Enable the examples server in Settings (or run npm run examples + from source) and open http://127.0.0.1:4173/. + Most demos are peer-to-peer — open the same URL in two tabs. +

+ +
+ Do not open examples via file://. Modern Chrome/Firefox treat each local + file as a unique opaque origin, which breaks scripts, styles, and extension injection. Always use + http://127.0.0.1:4173/…. +
+ +

Run the examples server

+

+ Recommended: in the BridgeSwarm extension, open Settings (or + Dashboard → Settings) and enable Examples server. Settings save automatically. +

+

From the BridgeSwarm repo root, the dev alternative:

+
npm run examples
+

Either way, use http://127.0.0.1:4173/. Shared chrome (theme, layout, boot script) lives in the repo's examples/shared/ and is reused by every demo.

+ +

Demo index

+ + + + + + + + + + + + + + + + + +
DemoURL
Landinghttp://127.0.0.1:4173/
Chathttp://127.0.0.1:4173/chat/
Advanced Chathttp://127.0.0.1:4173/chat-advanced/
Firewall Roomhttp://127.0.0.1:4173/firewall-room/
Whiteboardhttp://127.0.0.1:4173/whiteboard/
Screensharehttp://127.0.0.1:4173/screenshare/
Data APIhttp://127.0.0.1:4173/data-demo/
Auto-replicatehttp://127.0.0.1:4173/sync-demo/
Live Encodehttp://127.0.0.1:4173/live-encode/
Media Studiohttp://127.0.0.1:4173/media-demo/
Clip Studiohttp://127.0.0.1:4173/clip-studio/
SDK Demohttp://127.0.0.1:4173/sdk-demo/
HRPC Demohttp://127.0.0.1:4173/hrpc-demo/
+ +

Connect

+ + +

Collaborate

+ + +

Media

+ + +

Data

+ + +

Platform

+ + +

Troubleshooting

+
    +
  1. Wait a moment for content-script injection after the page loads, then retry.
  2. +
  3. Confirm the BridgeSwarm extension and native host are both installed and the browser was restarted after install.
  4. +
  5. Confirm you're on http://127.0.0.1:4173/…, not file://.
  6. +
  7. For media demos on macOS after a host update, run npm run repair:macos, then fully quit the browser.
  8. +
+ +
+ The examples/ tree in the repository is the source of truth for all demos; + npm run sync:examples copies it into extension/examples/ and + native-host/examples/ for packaging into release artifacts. +
+ + +
+
+
+ +
+ + + diff --git a/docs/faq.html b/docs/faq.html new file mode 100644 index 0000000..9e838f2 --- /dev/null +++ b/docs/faq.html @@ -0,0 +1,405 @@ + + + + + + FAQ · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Reference

+

Frequently Asked Questions

+

+ Short answers to the questions that come up most, grouped by topic. If yours isn't here, check the + User Guide's troubleshooting section or the + Security page. +

+ +

General

+ +
+ What is BridgeSwarm? +
+

+ BridgeSwarm brings the real Hyperswarm P2P stack — Hypercore, Hyperbee, Hyperdrive, Autobase, + Hyperdb, Protomux, and HRPC — into normal desktop browsers without forking Chrome or Firefox. A + Bare-based native + host runs the actual modules; a Manifest V3 extension injects window.BridgeSwarm into + pages and relays messages over native messaging. +

+
+
+ +
+ Why not just use WebRTC? +
+

+ WebRTC is peer-to-peer transport only — you still need signaling infrastructure, and it doesn't + give you durable, replicable append-only data structures. BridgeSwarm gives you Hyperswarm's DHT + for discovery/NAT traversal and the full Holepunch data stack (Hypercore, Hyperbee, + Hyperdrive, Autobase, Hyperdb) in the same page, with no server to run. That said, BridgeSwarm + doesn't replace WebRTC for everything — the screenshare demo + actually uses WebRTC for video and BridgeSwarm purely for signaling. +

+
+
+ +
+ What browsers does BridgeSwarm support? +
+

Chrome 88+, Edge, and Firefox 79+ — desktop builds only.

+
+
+ +
+ Does BridgeSwarm work on mobile? +
+

+ No. Native messaging — the mechanism the extension uses to talk to the local host process — isn't + available on mobile browsers. +

+
+
+ +
+ Do I need Node.js installed to use BridgeSwarm? +
+

+ No, not for a normal install. The release installer downloads prebuilt Bare host binaries for + darwin/linux/win32 — no Node required for end users. Node is only needed for from-source + development and CI tooling; the host itself runs on Bare (>=1.29.4 via the + bare npm dependency). +

+
+
+ +

Installation & setup

+ +
+ Do I need to restart my browser after installing? +
+

+ Yes — fully quit the browser (Cmd+Q / Quit, not just closing windows) so it picks up the + native-messaging manifest and any newly signed native addons. +

+
+
+ +
+ Can BridgeSwarm coexist with other extensions using native messaging (like holesail-browser)? +
+

+ Yes, by design. BridgeSwarm uses its own install directory (~/.bridgeswarm/), its own + native-messaging host name (com.bridgeswarm), and its own extension IDs. The + installer explicitly refuses to run if any of its identity values collide with known + holesail-browser identifiers, and it never touches holesail-browser's install directory or + manifest. +

+
+
+ +
+ Where does BridgeSwarm store data? +
+

+ Under BRIDGE_SWARM_STORAGE, or ~/.bridgeswarm/bridge-swarm-storage/ by + default. This holds your Corestore (backing Hypercore/Hyperbee/Hyperdrive/Autobase/Hyperdb) plus + capability job output under cap-jobs/. Extension settings live separately, in the + browser's own chrome.storage.local. +

+
+
+ +

Development

+ +
+ Can I use BridgeSwarm from a plain HTML page, or do I need a framework? +
+

+ Plain HTML/JS is enough — see Quick Start for a complete + working snippet with no build step. The bundled demos are also mostly framework-free. +

+
+
+ +
+ Why does my page need to run on http://, not file://? +
+

+ Modern Chrome/Firefox treat each file:// document as its own unique opaque origin, + which breaks both regular script/style loading and the content script's ability to reliably + inject window.BridgeSwarm. Serve your app over http:// or + https:// — even a trivial local static server works. +

+
+
+ +
+ How do I test peer-to-peer features without a second machine? +
+

+ Open your app in two browser tabs. Each tab gets its own cryptographic identity, so two tabs + joining the same topic is a fully realistic two-peer test. See + Developer Guide → Testing with two tabs. +

+
+
+ +
+ What's the difference between BridgeSwarm.request and the higher-level methods like swarm.join? +
+

+ swarm.join(), swarm.setFirewall(), swarm.ban(), and friends + are thin, ergonomic wrappers that call BridgeSwarm.request() under the hood with the + right type/payload shape. Anything not covered by a dedicated method + (Hyper* data commands, capability calls, host snapshot) is available directly via + BridgeSwarm.request(type, payload). See the API + Reference. +

+
+
+ +

Capabilities & modules

+ +
+ What Bare modules does the default host actually ship? +
+

Runtime/platform: bare, bare-process, bare-module, bare-fs, bare-path, bare-stream, bare-http1.

+

Holepunch data + swarm: hyperswarm, hypercore/corestore, hyperbee, hyperdrive, autobase, hyperdb/hyperschema, hrpc, protomux/compact-encoding/b4a.

+

Media (default capability): bare-media, bare-ffmpeg, and image codecs (bare-jpeg, bare-png, bare-webp, bare-gif, bare-heif, bare-bmp, bare-ico, bare-tiff, bare-svg, bare-image-resample, bare-exif).

+

Local power (bundled, not yet exposed as page packs): bare-sqlite, bare-fetch.

+

Full list: Capabilities → Bundled for upcoming packs.

+
+
+ +
+ Why aren't all ~150 bare-* packages exposed as capabilities? +
+

+ Curated packs are intentional: page-facing APIs are limited to what fits BridgeSwarm's actual + product surface (P2P networking, Hyper* data, and controlled local power like media). UI/mobile/ + build-tooling packages (bare-gtk, bare-ios, bare-android, + bare-build) are the wrong surface entirely. Open subprocess execution and raw LAN + socket listening are intentionally not exposed as page APIs — they're too easy to abuse without + much more sandboxing than the current model provides. +

+
+
+ +
+ Can I write my own capability pack? +
+

+ The host's capability dispatch (capabilities.list / capabilities.has / + capability) is designed to support additional packs beyond media. If you extend the + host with your own pack, keep any filesystem access scoped the same way the media pack is — + allowlisted under cap-jobs/ — and remember that shipping a modified host to others + over a network triggers AGPL's source-offer obligation. See + Security → AGPL-3.0. +

+
+
+ +

Performance & limits

+ +
+ What's the message size limit, and why? +
+

+ Chrome's native messaging host protocol caps messages from the host to the browser at roughly + 1 MB. This is a browser-imposed constraint, not a BridgeSwarm choice — it + shapes API design toward chunked/streaming transfers (capability cap-chunk events, + HRPC streaming) for anything larger. See + Developer Guide → Performance limits. +

+
+
+ +
+ Why does live encode want JPEG frames instead of raw video? +
+

+ Raw RGBA frames from a canvas blow past the ~1 MB native-messaging limit almost immediately + at any usable resolution. Compressed JPEG frames (or WebM timeslices) keep each message small + enough to ingest reliably. Defaults for demos are 640×360 @ 10fps, with a soft ceiling around + 720p @ 15fps and a 10-minute session cap. See Capabilities → Live encode notes. +

+
+
+ +
+ Is there a limit on peer connections? +
+

+ You can cap peers per swarm with the maxPeers constructor option (0 = + unlimited, or the extension's configured default applies). There's no hardcoded low ceiling beyond + what you configure. +

+
+
+ +

Licensing & trust

+ +
+ What license is BridgeSwarm released under? +
+

+ GNU Affero + General Public License v3.0 (AGPL-3.0). Owned and engineered by HoneyPeer, LLC + (DeKalb County, Georgia, USA). Legal contact: legal@honeypeer.com. +

+
+
+ +
+ Does building an app on top of BridgeSwarm make my app AGPL? +
+

+ Using window.BridgeSwarm as an API from your own page is ordinary usage, not + modification or distribution of BridgeSwarm itself — it doesn't automatically place your + application under AGPL. The copyleft applies if you modify BridgeSwarm's extension or native host + and let others interact with that modified version over a network. See + Security → AGPL-3.0 and + Developer Guide → AGPL obligations. This is general + information, not legal advice. +

+
+
+ +
+ Is my data sent to any BridgeSwarm-operated server? +
+

+ No. There is no backend service and no telemetry. All persistent state lives locally under your + own bridge-swarm-storage/ directory, and network traffic only ever goes directly to + peers you connect to over Hyperswarm — encrypted with Noise. See + User Guide → Privacy & local storage. +

+
+
+ +
+ Is it safe to leave the native host running all the time? +
+

+ It only does work when a BridgeSwarm-enabled page has active swarms, connections, or capability + jobs, and it only ever listens on 127.0.0.1 (the examples server) plus whatever + Hyperswarm sockets your pages open. As with any locally installed background process, only run + builds you trust. +

+
+
+ +
+ Still stuck? Check User Guide → Troubleshooting + for specific error messages, or review the full Security model. +
+ + +
+
+
+ +
+ + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..c633e15 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,243 @@ + + + + + + Documentation · BridgeSwarm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Documentation

+

BridgeSwarm Documentation

+

+ BridgeSwarm brings the real Hyperswarm P2P stack — Hypercore, Hyperbee, Hyperdrive, + Autobase, Hyperdb, Protomux, and HRPC — into normal desktop browsers, without forking Chrome or + Firefox. A Bare-based + native host runs the modules; a Manifest V3 extension injects window.BridgeSwarm into + the page and relays messages over native messaging. Everything below is organized so you can go from + zero to a working peer-to-peer page in minutes, then dig as deep as you need. +

+ +
+ New here? Start with Quick Start — install the + extension and native host, load the examples server, and join your first swarm in under five minutes. +
+ +

Browse the docs

+
+ + + +

Quick Start

+

Install the host and extension, load the demo gallery, and write your first join/write snippet.

+
+ + + +

User Guide

+

Installing BridgeSwarm, the Control Center (Overview, Swarms, Connections, Activity, Settings), running demos, troubleshooting, and privacy.

+
+ + + +

Developer Guide

+

Build pages on window.BridgeSwarm: joining topics, connections, requests, Protomux, HRPC, media, testing, and packaging.

+
+ + + +

Architecture

+

Layered diagram of extension, native host, and protocol; message flow, connection lifecycle, and the live-encode path.

+
+ + + +

API Reference

+

Constructor options, instance methods, events, and the full BridgeSwarm.request type catalog.

+
+ + + +

Data API

+

Hypercore, Hyperbee, Hyperdrive, Autobase, and Hyperdb over the bridge — named resources, auto-replicate, storage paths.

+
+ + + +

Protocols

+

Protomux and compact-encoding in the browser, plus host-side HRPC attach/call/stream methods.

+
+ + + +

Capabilities

+

Optional Bare capability packs: batch media transforms and live VP9/WebM encode, plus the security model behind them.

+
+ + + +

Examples

+

Every bundled demo — chat, whiteboard, screenshare, media studio, HRPC, and more — with what each one teaches.

+
+ + + +

Security

+

Noise encryption, firewall/ban, the cap-jobs sandbox, native-host threat model, and AGPL-3.0 obligations.

+
+ + + +

FAQ

+

Answers to the questions that come up most: browsers, mobile, performance limits, licensing, and more.

+
+ + + +

Release Notes

+

Release channels, artifact layout, versioning, and the rolling latest-main build.

+
+ +
+ +

How it works, in one picture

+

+ The page never talks to the network directly. It calls window.BridgeSwarm, which is + injected by the extension's content script. The content script relays through the background service + worker to a native messaging host built on Bare, + which runs the actual Hyperswarm stack and replies over the same channel. +

+
Page (window.BridgeSwarm)
+  ⇅ postMessage
+Content script (content.js)
+  ⇅ runtime.sendMessage
+Background service worker (background.js)
+  ⇅ native messaging: 4-byte LE length + JSON
+Native host (Bare): Hyperswarm · Hypercore/Hyperbee/Hyperdrive · Autobase · Hyperdb · Protomux · HRPC · media
+  ⇅ Noise-encrypted P2P
+Remote peers
+

See Architecture for the full component and sequence diagrams.

+ +

Building blocks

+ + + + + + + + + + + + + +
TechnologyPurpose
HyperswarmP2P networking, DHT discovery, NAT traversal
CorestoreMulti-Hypercore storage
HypercoreAppend-only log
HyperbeeKey/value B-tree
HyperdriveP2P file system
AutobaseMulti-writer linearized log
HyperdbSchema-based P2P database
ProtomuxProtocol multiplexing over a connection
HRPCTyped RPC with streaming, run on the native host
+ +
+ Trust boundary: the native host runs with your user privileges — it can open sockets + and read/write files. Only install BridgeSwarm builds you trust. See Security. +
+ + +
+
+
+ +
+ + + diff --git a/docs/protocols.html b/docs/protocols.html new file mode 100644 index 0000000..570e9e4 --- /dev/null +++ b/docs/protocols.html @@ -0,0 +1,295 @@ + + + + + + Protocols · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Build with BridgeSwarm

+

Protocols

+

+ Beyond raw conn.write / data events, BridgeSwarm gives you two ways to run + structured protocols over a connection: Protomux in the browser for your own + message-oriented mini-protocols, and HRPC on the native host for typed + request/response and streaming RPC. +

+ + + +

Protomux setup

+

+ BridgeSwarm injects a browser bundle of Protomux, + compact-encoding + (c), and b4a + so you can run message-oriented protocols over P2P connections without bundling anything yourself. No + extra <script> tags are needed — the content script injects api.js, + framed-stream.js, and protomux-bundle.js into the page, in that order. +

+ +

Globals

+ + + + + + + +
GlobalDescription
window.BridgeSwarmMain API: new BridgeSwarm(opts), swarm.join(topic), swarm.on('connection', ...), swarm.createProtomux(conn)
window.BridgeSwarmFramedStreamwrapRawFrames(conn) — wraps a connection so it presents a stream that preserves message boundaries for Protomux
window.BridgeSwarmProtomux{ Protomux, c, b4a } — the Protomux class, compact-encoding helpers, and b4a buffer utilities
+ +

createProtomux(conn)

+

Given a connection from swarm.on('connection', (conn, peerInfo) => { ... }):

+
const mux = swarm.createProtomux(conn);
+

+ This wraps the connection in a framed stream and returns a new Protomux instance. If the + Protomux bundle or framed-stream script isn't loaded, createProtomux returns + null and logs a warning — always check before using it. Note that + conn.write(data) returns a Promise that resolves on success and rejects on host error. +

+ +

compact-encoding (c)

+

Use window.BridgeSwarmProtomux.c for message encodings. Common ones:

+
    +
  • c.string — UTF-8 string
  • +
  • c.binary / c.raw — raw buffer
  • +
  • c.uint — unsigned integer
  • +
  • c.bool — boolean
  • +
  • c.json — JSON (if available in the bundle)
  • +
+
const { Protomux, c, b4a } = window.BridgeSwarmProtomux;
+
+const channel = mux.createChannel({
+  protocol: 'my-app/v1',
+  onopen() {},
+  onclose() {}
+});
+
+const stringMsg = channel.addMessage({
+  encoding: c.string,
+  onmessage(value) { console.log('Got string:', value); }
+});
+
+const binaryMsg = channel.addMessage({
+  encoding: c.binary,
+  onmessage(buf) { console.log('Got buffer:', buf); }
+});
+
+channel.open();
+stringMsg.send('hello');
+binaryMsg.send(b4a.from('data'));
+ +

b4a

+

window.BridgeSwarmProtomux.b4a provides Node-Buffer-style helpers:

+
    +
  • b4a.from(str, encoding) — create from string (e.g. 'utf8')
  • +
  • b4a.toString(buf, encoding) — buffer to string
  • +
  • b4a.allocUnsafe(n) — allocate (browsers may back this with Uint8Array)
  • +
+

Use these when encoding/decoding binary message payloads.

+ +

Full Protomux example

+
<!DOCTYPE html>
+<html>
+<body>
+  <button id="join">Join topic</button>
+  <pre id="log"></pre>
+  <script>
+    const log = (msg) => { document.getElementById('log').textContent += msg + '\n'; };
+    const swarm = new BridgeSwarm({ appName: 'protomux-example' });
+
+    document.getElementById('join').onclick = async () => {
+      await swarm.join('protomux-demo-topic');
+      log('Joined. Open this page in another tab and join the same topic.');
+    };
+
+    swarm.on('connection', (conn, peerInfo) => {
+      log('Peer connected: ' + peerInfo.publicKey.slice(0, 16) + '...');
+      const mux = swarm.createProtomux(conn);
+      if (!mux) { log('Protomux not available'); return; }
+
+      const ch = mux.createChannel({
+        protocol: 'chat',
+        onopen() { log('Channel opened'); },
+        onclose() { log('Channel closed'); }
+      });
+      ch.addMessage({
+        encoding: window.BridgeSwarmProtomux.c.string,
+        onmessage(m) { log('Peer said: ' + m); }
+      });
+      ch.open();
+      window.sendChat = (text) => ch.messages[0].send(text);
+    });
+  </script>
+</body>
+</html>
+ +

Native host: connection attachment

+

+ The native host can also attach Hypercore replication or HRPC to a + connection. Once you do this, that connection is no longer forwarded to the browser — the host takes + it over entirely. +

+ + + + + + +
RequestBehavior
BridgeSwarm.request('attachReplication', { connId, coreKeyHex? })Uses the connection identified by connId. If coreKeyHex is given, the host looks up that core from its corestore; otherwise it uses the default core (name 'default'). The host creates a Protomux from the socket and runs core.replicate(protomux) so the remote peer can replicate that core.
BridgeSwarm.request('attachHrpc', { connId })Enables HRPC on that connection over a Protomux channel named bridgeswarm-hrpc.
+

+ Storage for the host's corestore is under BRIDGE_SWARM_STORAGE or + ./bridge-swarm-storage relative to the host process. +

+
+ Each BridgeSwarm connection is already a NoiseSecretStream (framed, encrypted). The + extension forwards one decrypted frame per chunk, so wrapRawFrames(conn) presents one + chunk per frame to Protomux. Protomux pairs channels by protocol (and optional + id) — use the same values on both sides so the channel opens. For RPC-style usage, + BridgeSwarm's supported path is host HRPC; in-page + protomux-rpc + remains optional DIY if you bundle it yourself on top of createProtomux. +
+ +

HRPC on the native host

+

+ The native host runs hrpc + over a connection — schema-defined RPC on a Protomux channel. The browser doesn't run hrpc itself; it + attaches HRPC on the host and invokes methods via hrpcInvoke / swarm.hrpcCall. +

+
await BridgeSwarm.request('attachHrpc', { connId: conn.connId });
+

Both peers must attach HRPC on the same connection pair. The first invoke may wait up to 12 seconds for the channel to open.

+ +

HRPC commands (spec)

+ + + + + + + + + +
CommandModeRequestResponse
pingUnary{ value? }{ pong }
streamSumRequest-stream → responsechunks { n, label } (pass args.chunks from the browser){ sum, count } on hrpc-end
fetchStreamRequest → response-stream{ count }chunks { i, data } via hrpc-chunk
duplexDuplexargs.chunks { x, y }chunks { result, n } via hrpc-chunk
notifySend-only{ event, payload }none
+

Build the spec with npm run build:hrpcspec/hrpc/ (mirrored into native-host/spec/hrpc/).

+ +

Invoking HRPC from the browser

+

Unary / notify

+
const res = await swarm.hrpcCall(conn.connId, 'ping', { value: 'hello' });
+// or: BridgeSwarm.request('hrpcInvoke', { connId, method: 'ping', args })
+ +

Streaming

+
const res = await swarm.hrpcCall(conn.connId, 'fetchStream', { count: 3 }, {
+  onChunk: (chunk) => console.log(chunk),
+  timeoutMs: 20000,
+});
+// Host replies immediately with { streaming: true, streamId }, then emits
+// hrpc-chunk / hrpc-end / hrpc-error events to the page.
+
await swarm.hrpcCall(conn.connId, 'streamSum', {
+  chunks: [{ n: 1, label: 'a' }, { n: 2, label: 'b' }],
+});
+ +
+ Try HRPC live in the HRPC Demo + and the SDK demo's raw Protomux usage in + SDK Demo. See Examples. +
+ + +
+
+
+ +
+ + + diff --git a/docs/quickstart.html b/docs/quickstart.html new file mode 100644 index 0000000..8285507 --- /dev/null +++ b/docs/quickstart.html @@ -0,0 +1,245 @@ + + + + + + Quick Start · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Getting Started

+

Quick Start

+

+ Five steps from a clean machine to a peer-to-peer page: install the native host and extension, + load them into your browser, enable the examples server, then write a few lines of JavaScript that + join a topic and exchange messages with another tab. +

+ + + +

1. Install the host

+

+ The easy installer downloads a prebuilt native-host binary for your platform plus the packaged + extension from the rolling latest-main release. No Node.js or git clone required. +

+ +

macOS / Linux:

+
curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/web-installer.sh | bash
+ +

Windows (PowerShell):

+
irm https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/install.ps1 | iex
+ +

+ This installs the native host to ~/.bridgeswarm/ (%LOCALAPPDATA%\bridgeswarm\ + on Windows), registers the native-messaging manifest for Chrome/Edge and Firefox, and saves + BridgeSwarm-*.zip / .xpi to ~/Downloads. +

+ +
+ Prefer to build from source? git clone the repo, run npm run setup + (or ./scripts/install-from-source.sh), then load extension/ unpacked. + See the Developer Guide. +
+ +

2. Load the extension

+ +

Chrome / Edge

+
    +
  1. Open chrome://extensions
  2. +
  3. Enable Developer mode (top right)
  4. +
  5. Drag & drop ~/Downloads/BridgeSwarm-1.0.0.zip onto the page — or extract it + and click Load unpacked
  6. +
  7. Confirm the extension ID is jhmbaojjfkkpoolhkoohklbjokdmbdpm
  8. +
  9. Fully restart the browser
  10. +
+ +

Firefox

+

+ Temporary: about:debuggingThis FirefoxLoad Temporary Add-on. + Permanent (Nightly / Developer Edition only): set xpinstall.signatures.required to + false in about:config, then Install Add-on From File with the + downloaded .xpi. +

+ +
+ Restart required. The native messaging manifest is only picked up when the browser + fully restarts (Cmd+Q / Quit, not just closing windows). +
+ +

3. Enable the examples server

+

+ BridgeSwarm ships a full demo gallery. The native host can serve it locally so every demo runs on the + same origin (required for the content script to inject window.BridgeSwarm). +

+
    +
  1. Click the BridgeSwarm toolbar icon to open the Control Center, or open Options
  2. +
  3. Go to Settings → Examples server
  4. +
  5. Toggle Enable examples server on — settings save automatically, no Save button
  6. +
  7. Open http://127.0.0.1:4173/ in two tabs
  8. +
+

+ Never open demo files via file:// — each local file is treated as a unique opaque + origin by modern browsers, which breaks both scripts and extension injection. +

+
# Dev alternative from the repo (equivalent to the Settings toggle)
+npm run examples
+ +

4. Your first join/write snippet

+

+ Drop this into any page served over http:// or https:// (not file://). + Open the same page in two tabs to see peers connect. +

+
// Wait for the extension to inject the API (safe even if your script runs first)
+await BridgeSwarm.ready();
+
+// Create a swarm — appName scopes discovery
+const swarm = new BridgeSwarm({ appName: 'my-first-app' });
+
+// Join a topic to discover peers interested in the same thing
+await swarm.join('hello-bridgeswarm');
+
+// Handle incoming connections
+swarm.on('connection', (conn, peerInfo) => {
+  console.log('Peer connected:', peerInfo.publicKey);
+
+  conn.on('data', (data) => {
+    console.log('Received:', new TextDecoder().decode(data));
+  });
+
+  // conn.write returns a Promise — resolves on success, rejects on host error
+  conn.write('Hello, peer!').catch((err) => console.error('write failed:', err));
+});
+
+// Later: leave and clean up
+// await swarm.leave('hello-bridgeswarm');
+// swarm.destroy();
+ +

+ That is a complete, working P2P chat primitive: no server, no signaling infrastructure to run + yourself — Hyperswarm's DHT handles discovery and NAT traversal, and Noise handles encryption. +

+ + + + + + + + + + +
ConceptWhat it does
BridgeSwarm.ready()Resolves once the extension has injected the API — use if your script may run before injection
new BridgeSwarm(opts)Creates a swarm; appName and maxPeers are optional (extension defaults apply otherwise)
swarm.join(topic)Joins a 32-byte topic (hex string or a value the API hashes for you) to discover peers
swarm.on('connection', …)Fires per peer connection with a conn object and peerInfo
conn.write(data)Sends bytes or a string to that peer; returns a Promise
+ +

5. Next steps

+ + + +
+
+
+ +
+ + + diff --git a/docs/release.html b/docs/release.html new file mode 100644 index 0000000..ba3f9ba --- /dev/null +++ b/docs/release.html @@ -0,0 +1,203 @@ + + + + + + Release Notes · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Reference

+

Release Notes

+

+ BridgeSwarm ships two kinds of Gitea releases: a rolling latest-main build that tracks + every push to main, and versioned v*.*.* tags for stable milestones. Both + are built by the same CI pipeline and produce the same artifact layout. +

+ + + +

Release channels

+ + + + + + + + + + + + + + + + +
ChannelTriggerStabilityUse it if…
latest-mainEvery push to main (CI force-updates the tag and release)Prerelease — rolling, always reflects the tip of mainYou want the newest fixes/features and are comfortable with occasional churn. This is what the one-line installer downloads.
v*.*.*Pushing a version tag (e.g. v1.2.0)Stable — pinned to a specific commit, never overwrittenYou need a reproducible, unchanging install — for production deployments or long-lived documentation references.
+
+ The rolling release's title reflects the exact commit it was built from — e.g. "Latest build + (main @ a1b2c3d)" — and its body links the commit and message, so you can always trace an + installed build back to its exact source. +
+ +

Artifacts

+

Every release publishes the same set of files:

+ + + + + + + + + + + + +
FileContents
bridge-swarm-host-darwin-x64.zipStandalone native host binary for macOS (Intel)
bridge-swarm-host-darwin-arm64.zipStandalone native host binary for macOS (Apple Silicon)
bridge-swarm-host-linux-x64.zipStandalone native host binary for Linux (x64)
bridge-swarm-host-linux-arm64.zipStandalone native host binary for Linux (arm64)
bridge-swarm-host-win32-x64.zipStandalone native host binary for Windows
BridgeSwarm-<version>.zipPackaged Chrome/Edge extension (drag-and-drop installable), including the synced examples/ tree
BridgeSwarm-<version>.xpiPackaged Firefox extension
SHA256SUMS.txtChecksums for every .zip/.xpi in the release
+

+ The installer scripts (scripts/install.sh, scripts/install.ps1, + scripts/web-installer.sh) automatically pick the right host archive for your OS/CPU and + download the extension packages into ~/Downloads. +

+ +

Versioning

+

+ BridgeSwarm's extension and native host share a version number (currently 1.0.0), + declared in extension/manifest.json and native-host/package.json. Tagged + releases follow semantic versioning: + breaking changes to the page API or wire protocol bump the major version; new request types, + capabilities, or backward-compatible features bump the minor version; fixes bump the patch version. +

+ +

Building artifacts locally

+
npm run build                 # host launcher + protomux + hrpc
+npm run pack                  # package extension (includes examples) -> releases/
+npm run build:dist            # standalone host binary (current platform)
+npm run build:dist:package    # all platforms + zip archives (best on CI)
+

CI runs the equivalent of node scripts/build-distributable.js --all --package to produce every platform archive in one pass, then generates SHA256SUMS.txt before publishing.

+ +

Verifying downloads

+
# macOS / Linux
+shasum -a 256 -c SHA256SUMS.txt
+
+# or, per file
+sha256sum bridge-swarm-host-darwin-arm64.zip
+

Compare the output against the matching line in SHA256SUMS.txt from the same release.

+ +

Compatibility

+
    +
  • Chrome 88+, Edge, Firefox 79+
  • +
  • Desktop only — native messaging is not available on mobile
  • +
  • Release install: prebuilt Bare host binaries (darwin/linux/win32) — no Node.js required for end users
  • +
  • From-source / CI: Node.js for tooling; Bare >=1.29.4 via the bare npm dependency
  • +
+ +

Current release

+ + + + + + + + +
Version1.0.0
LicenseAGPL-3.0
PublisherHoneyPeer, LLC
Rolling channellatest-main
+

Install the current rolling build with the one-liner from Quick Start, or browse all releases and diffs directly on Gitea.

+ + +
+
+
+ +
+ + + diff --git a/docs/security.html b/docs/security.html new file mode 100644 index 0000000..ff76b83 --- /dev/null +++ b/docs/security.html @@ -0,0 +1,230 @@ + + + + + + Security · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Reference

+

Security

+

+ BridgeSwarm gives web pages real peer-to-peer networking and local storage through a native host + that runs with your full user privileges. That's powerful, and it's also a real trust boundary you + should understand before installing it or building on it. +

+ +
+ The native host runs with your user privileges and can make network connections, + read/write files (within its allowlisted paths), and access system resources. Only install + BridgeSwarm builds — and only use BridgeSwarm-enabled pages — that you trust. +
+ + + +

Trust model

+

There are three separate trust relationships in play, and it helps to reason about them separately:

+ + + + + + + +
RelationshipWhat's at riskMitigation
You ↔ the BridgeSwarm build you installThe native host runs with your user privilegesOnly install from sources you trust; prefer the official signed latest-main release or building from the public repo yourself
You ↔ a page that uses BridgeSwarmAny page with BridgeSwarm injected can open P2P connections and touch local storage under allowlisted pathsOnly visit BridgeSwarm-enabled pages you trust; use "Disable on file:// URLs" and review a page's source if unsure
You ↔ remote peersMalicious or noisy peers on a shared topicNoise encryption, setFirewall, ban, maxPeers
+ +

Noise encryption

+

+ Every Hyperswarm connection is automatically end-to-end encrypted with the + Noise Protocol Framework — + there is no plaintext mode and nothing for you to configure. Each connection is a + NoiseSecretStream; the extension forwards one decrypted frame per data + event to the page. +

+ +

Firewall & ban

+
// Allowlist — only these public keys may connect
+await swarm.setFirewall({ mode: 'allowlist', keys: ['abc123...'] });
+
+// Denylist — block specific keys, allow everyone else
+await swarm.setFirewall({ mode: 'denylist', keys: ['def456...'] });
+
+// Ban a peer (updates the denylist too)
+await swarm.ban('abc123...');
+swarm.on('connection', (conn, peerInfo) => {
+  // peerInfo.ban() is also available directly on the event
+});
+

+ You can also configure a global default firewall from extension Settings — mode plus + a list of keys — applied automatically whenever any swarm initializes on your machine, independent of + what a given page requests. See User Guide → Firewall defaults. +

+ +

Built-in protections

+ + + + + + + + + +
ThreatProtection
Connection floodingmaxPeers option, per swarm
Malicious peersswarm.setFirewall({ mode, keys }), swarm.ban(publicKey), peerInfo.ban()
Data interceptionNoise encryption (automatic, always on)
Identity spoofingCryptographic key pairs — each tab has its own identity, and public keys are verifiable
DHT attacksRate limiting built into Hyperswarm / the DHT layer
+ +

The cap-jobs sandbox

+

+ Capability packs (media transforms, live encode) that need to touch the filesystem are restricted to + one allowlisted directory tree: +

+
$BRIDGE_SWARM_STORAGE/cap-jobs/
+# default: ~/.bridgeswarm/bridge-swarm-storage/cap-jobs/
+

+ Absolute paths outside that tree are rejected by the host — a page cannot ask a capability pack to + read or write arbitrary files on your machine. Chunked upload/download helpers + (media.writeInput / media.readOutput) and live-encode archives all resolve + under this directory. +

+ +

Native-host threat model

+

+ Native messaging is Chrome/Firefox's sanctioned way for an extension to talk to a local process, and + it has known, well-understood properties that shape BridgeSwarm's design: +

+
    +
  • The host is only reachable by an installed, matching extension. The native-messaging manifest whitelists a specific extension ID (allowed_origins / allowed_extensions); a random page cannot spawn or talk to the host directly.
  • +
  • The host process itself is trusted code you installed — treat it like any other local binary with your user's privileges. Get it from a source you trust.
  • +
  • Message size is capped (~1 MB) host → browser, which bounds worst-case memory/IPC pressure from any single message and shapes the API toward chunked/streaming transfers for large payloads.
  • +
  • Capability packs are curated, not "run anything." Only specific, reviewed commands are exposed per pack (see Capabilities); there is intentionally no generic "execute shell command" or open filesystem API exposed to pages.
  • +
  • Firewall/ban are host-enforced, not just client-side hints. A denied peer's connection attempts are rejected by the host's Hyperswarm instance itself.
  • +
+
+ BridgeSwarm does not run a backend service and collects no telemetry. All state is local to your + machine unless you explicitly replicate it to peers you connect to. See + User Guide → Privacy & local storage. +
+ +

Identity & keys

+

+ Each browser tab gets its own cryptographic key pair for Hyperswarm, generated fresh unless you + persist and reuse one yourself. Hypercore/Hyperbee/Hyperdrive/Autobase/Hyperdb resources each have + their own public key, independent of swarm identity — see + User Guide → Topics vs. keys for how discovery + (topics) and addressing (keys) relate. +

+ +

AGPL-3.0

+

+ BridgeSwarm is licensed under the + GNU Affero General + Public License v3.0. AGPL extends GPL's copyleft to cover network use (§13): if you modify + BridgeSwarm and let others interact with your modified version over a network — not just by + distributing binaries — you must offer them the corresponding source for your modified version. +

+
    +
  • Writing a normal web app that uses window.BridgeSwarm as an API is not, by itself, what triggers the network clause — that's ordinary usage, not modification/distribution of BridgeSwarm.
  • +
  • Forking the extension or native host and offering a modified build as a hosted service, a rebranded installer, or a modified capability pack does trigger it — you must make that modified source available to users of the service.
  • +
  • This is a summary, not legal advice. Review the full license text and consult counsel for your specific situation.
  • +
+

+ Licensing questions: legal@honeypeer.com. BridgeSwarm is + owned and engineered by HoneyPeer, LLC (DeKalb County, Georgia, USA). +

+ + +
+
+
+ +
+ + + diff --git a/docs/user-guide.html b/docs/user-guide.html new file mode 100644 index 0000000..b1b8be6 --- /dev/null +++ b/docs/user-guide.html @@ -0,0 +1,338 @@ + + + + + + User Guide · BridgeSwarm Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

Getting Started

+

User Guide

+

+ This guide is for people running BridgeSwarm day to day: installing it, understanding the + Control Center, trying the bundled demos, and fixing the handful of things that can + go wrong. If you're writing code against window.BridgeSwarm, see the + Developer Guide instead. +

+ + + +

Installing BridgeSwarm

+

+ BridgeSwarm has two parts that must both be installed: a native host (a small + background program built on the Bare + runtime that actually runs Hyperswarm) and a browser extension (Manifest V3) that + injects the page API and talks to the host over Chrome/Firefox native messaging. +

+

The one-line installer handles both — see Quick Start for the exact commands. In short:

+
    +
  1. Run the installer script for your OS. It downloads a prebuilt host binary and writes the native-messaging manifest to the right OS-specific directory.
  2. +
  3. Load the downloaded extension package into Chrome/Edge (Developer mode → drag the zip, or Load unpacked) or Firefox (temporary add-on or signed install on Nightly/Developer Edition).
  4. +
  5. Fully restart the browser so it picks up the native-messaging manifest.
  6. +
+

+ The installer writes the host to ~/.bridgeswarm/ on macOS/Linux + (%LOCALAPPDATA%\bridgeswarm\ on Windows) and is careful to coexist with unrelated + extensions on your system — it only ever touches its own install directory and its own native + messaging manifest name, com.bridgeswarm. +

+ +

The Control Center

+

+ Click the BridgeSwarm toolbar icon to open the Control Center — a live dashboard over + a persistent port (bridgeswarm-dashboard) into the native host. It has five tabs: +

+ +

Overview

+

+ Host health (connected / disconnected), the examples server status and toggle, and a live snapshot of + swarm and connection counts. This is the fastest way to confirm the extension is actually talking to + the native host. +

+ +

Swarms

+

+ Every active Hyperswarm instance created by any tab: its public key, app name, peer + count, and firewall mode. Useful for seeing exactly what a page has joined without opening DevTools. +

+ +

Connections

+

+ Live peer sockets across all swarms — who's connected, which swarm they belong to, and whether a + connection has been "attached" for Hypercore replication or HRPC (and is therefore no longer + forwarded to the page). +

+ +

Activity

+

+ A structured event stream from both the host and the extension: connections opening and closing, + errors, capability job progress, and examples-server lifecycle events. This is the first place to + look when something isn't behaving as expected. +

+ +

Settings

+

Changes here save automatically — there is no Save button.

+ + + + + + + + + + + + + + + +
GroupSettingPurpose
GeneralDebug loggingVerbose logs in the Activity tab and browser console
Disable on file:// URLsSkip injecting BridgeSwarm on local files
Swarm defaultsDefault app nameUsed when a page omits appName
Default max peers0 = unlimited
Request timeout (ms)Default BridgeSwarm.request timeout; 0 = none
Ready timeout (ms)BridgeSwarm.ready() timeout; 0 = wait forever
Default peer firewallModeOff / Allowlist / Denylist, applied automatically on swarm init
Peer keysOne public-key hex per line
Examples serverEnable examples serverNative host serves the demo gallery on 127.0.0.1 only
PortBound to 127.0.0.1 only (default 4173)
PanelRefresh interval (ms)How often Overview polls host state (1000–10000ms)
+

+ Options (the browser's native extension options page) simply opens the Control + Center's Settings tab in a full tab — they are the same UI. +

+ +

The examples server

+

+ The bundled demo gallery needs to be served over http://, not opened as local files, so + the content script can inject the page API and so relative fetches behave normally. Toggle + Examples server on in Settings (or Dashboard → Settings) and the native host starts + a local static server: +

+
http://127.0.0.1:4173/
+

+ It only binds to 127.0.0.1 — nothing outside your machine can reach it. Developers + running from source can start the same server with npm run examples instead of using the + toggle. +

+ +

Using the demos

+

+ Open http://127.0.0.1:4173/ + and pick a demo from the landing page. Most demos are P2P, so open the same URL in a + second tab to see two peers connect to each other. See the full catalog with + descriptions in Examples. +

+
+ Each browser tab gets its own cryptographic identity, so two tabs on the same machine are a perfectly + realistic two-peer test — you don't need a second computer. +
+ +

Firewall defaults

+

+ By default, a new swarm accepts connections from any peer that discovers the same topic (firewall + mode off). You can lock this down two ways: +

+
    +
  • Per-page, at runtime — the page itself calls swarm.setFirewall({ mode, keys }). This is what the Firewall Room demo shows.
  • +
  • Globally, from Settings — set a Default peer firewall mode (allowlist or denylist) and a list of public keys; it is applied automatically whenever any swarm on your machine initializes.
  • +
+

Whichever a page sets explicitly at runtime takes effect for that swarm going forward. See Security for the full trust model.

+ +

Troubleshooting

+ +

"Native host has exited"

+

+ The launcher script starts the Bare runtime; if paths are stale after a manual reinstall, re-run the + install script for your platform, or (from source) run npm install in + native-host/ followed by npm run build:host. +

+ +

"Access to the specified native messaging host is forbidden"

+

+ The extension ID in the native-messaging manifest doesn't match your installed extension's actual ID + (this can happen if you load an unpacked dev build after installing the packaged release). From + source, run ./scripts/update-native-manifest-extension-id.sh YOUR_EXTENSION_ID. +

+ +

Device-file / storage errors ("Invalid device file, was modified")

+

+ BridgeSwarm's Corestore seals its on-disk storage to the underlying inode. If storage is + copied across filesystems (rather than moved), the seal can be invalidated and the + host will refuse to open it with an error like "Invalid device file, was modified." To avoid + this: +

+
    +
  • Let the installer manage storage — it preserves bridge-swarm-storage/ across + reinstalls by moving it on the same filesystem whenever possible, and only falls back to a copy + (which reseals) when moving across devices.
  • +
  • Don't manually cp -a the storage directory between machines or drives if you want to + preserve existing Hypercore keys — treat it as a move, not a copy, or expect a reseal.
  • +
  • If you do hit this error, the safest recovery is to let the host create fresh storage (effectively + new local keys) rather than fight the seal.
  • +
+ +

Users not showing up in each other's lists

+
    +
  • Confirm both tabs joined the exact same topic string
  • +
  • Enable Debug logging in Settings and check the Activity tab for join/connection events
  • +
  • Confirm both tabs are on the same origin (e.g. both on http://127.0.0.1:4173/...) — a page on file:// may not have BridgeSwarm injected at all
  • +
+ +

Media demos stop working after an update (macOS)

+

+ Native ffmpeg/image-codec addons are code-signed on install. After updating the host, run + npm run repair:macos (or re-run the installer) and then fully quit the + browser (Cmd+Q) before testing again — a running browser process can hold a stale, unsigned addon in + memory. +

+ +

Updating

+

+ Run the exact same install command you used the first time (see Quick + Start). It downloads the current latest-main artifacts, replaces the host binary, and + rewrites the native-messaging manifest — while preserving your existing + bridge-swarm-storage/ directory (your Hypercore/Hyperbee/Hyperdrive/Autobase/Hyperdb + data and keys). Reload the extension package the same way you loaded it initially. +

+ +

Uninstalling

+
    +
  1. Remove the BridgeSwarm extension from chrome://extensions or Firefox's Add-ons manager.
  2. +
  3. Delete the install directory: ~/.bridgeswarm/ (macOS/Linux) or %LOCALAPPDATA%\bridgeswarm\ (Windows). This removes the host binary and your local bridge-swarm-storage/.
  4. +
  5. Delete the native-messaging manifest named com.bridgeswarm.json from your browser's native-messaging-hosts directory (the installer prints this path when it runs, and it's under your browser's application-support / config directory).
  6. +
+
+ Deleting ~/.bridgeswarm/ deletes local storage permanently, including any Hypercore keys + and Hyperbee/Hyperdrive/Hyperdb data you created. Back it up first if you need it. +
+ +

Privacy & local storage

+

+ BridgeSwarm does not run a backend service and does not collect telemetry. Everything the native host + persists lives locally under BRIDGE_SWARM_STORAGE (default + ~/.bridgeswarm/bridge-swarm-storage/), including: +

+
    +
  • Your Corestore — the underlying storage for Hypercore, Hyperbee, Hyperdrive, Autobase, and Hyperdb
  • +
  • Capability job output under cap-jobs/ (media transforms, live-encode archives)
  • +
  • Extension settings, which live in the browser's own chrome.storage.local, not on the host
  • +
+

+ Data you replicate or send only reaches peers you connect to — over an encrypted Noise channel — and + only for topics or keys you explicitly join or open. There is no central server that ever sees your + traffic. See Security for the full threat model, and remember that + any page you grant BridgeSwarm access to can, by design, open P2P connections and read/write local + storage under the allowlisted paths — only use BridgeSwarm-enabled pages you trust. +

+ +

Topics vs. keys — which do I use?

+

+ BridgeSwarm gives you two different ways to find and identify things, and it's easy to conflate them: +

+ + + + + + + + +
TopicPublic key
What it isA 32-byte value (hex string) used with swarm.join(topic)The identity of a specific Hypercore/Hyperbee/Hyperdrive/Autobase/Hyperdb resource, or of a peer
PurposeDiscovery — find peers interested in the same thing, via the DHTAddressing / replication — reference one specific resource or peer, e.g. with coreKeyHex
Use it forChat rooms, shared sessions, "anyone doing X can find each other"Replicating a specific append-only log, allowlisting/denylisting one peer, resuming a known dataset
Exampleswarm.join('my-app-lobby')swarm.setAutoReplicate({ enabled: true, coreKeyHex })
+

+ Rule of thumb: use a topic when you want strangers with the same interest to find each + other; use a key when you already know exactly which resource or peer you want to talk + to. Many apps do both — join a topic to discover peers, then exchange or announce Hypercore keys over + that connection to start replicating specific data. +

+ + +
+
+
+ +
+ + + diff --git a/download.html b/download.html new file mode 100644 index 0000000..609d11e --- /dev/null +++ b/download.html @@ -0,0 +1,286 @@ + + + + + + + + +Download & Install BridgeSwarm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +

Install BridgeSwarm

+

Two pieces: a small native host binary and a browser extension. The installer wires them together and registers native messaging so Chrome, Edge, and Firefox can find the host. No Node.js or git clone required for a normal install.

+
+
+ +
+
+
+ +

Run the installer

+

Downloads prebuilt native-host binaries and the extension from the rolling Gitea release latest-main.

+
+ +
+
+ macOS / Linux +
curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/web-installer.sh | bash
+

Installs the host to ~/.bridgeswarm/ and saves BridgeSwarm-1.0.0.zip / .xpi to ~/Downloads.

+
+
+ Windows (PowerShell) +
irm https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/install.ps1 | iex
+

Run in a normal PowerShell window (no admin required). Installs to %LOCALAPPDATA%\bridgeswarm\ and saves archives to %USERPROFILE%\Downloads.

+
+
+ +
+
i
+
+ Coexists cleanly with holesail-browser if you have it installed — different install directory, different native messaging host name, different extension IDs. Neither installer touches the other's files. +
+
+
+
+ +
+
+
+ +

What the installer actually does

+

Nothing hidden — read scripts/install.sh yourself. In order:

+
+ +
+
    +
  1. Stops any running bridge-swarm-host process (path-scoped, never touches other native hosts).
  2. +
  3. Preserves your existing bridge-swarm-storage directory (moved aside, not copied, so the Corestore device-file seal survives a reinstall).
  4. +
  5. Downloads and unzips the platform binary for your OS/arch from the latest-main Gitea release.
  6. +
  7. On macOS: clears the quarantine attribute, ad-hoc code-signs the host binary and its extracted .bare/.dylib addons, and writes an entitlements plist so library validation doesn't block unsigned native addons.
  8. +
  9. Writes a small launcher script that sets BRIDGE_SWARM_STORAGE and BRIDGESWARM_EXAMPLES_DIR before starting the binary.
  10. +
  11. Writes native messaging manifests named com.bridgeswarm.json into the Chrome, Chromium, and Firefox native-messaging-hosts directories for your OS — never overwriting any other vendor's manifest.
  12. +
  13. Restores your preserved storage directory.
  14. +
  15. Downloads the extension .zip (Chrome/Edge) and .xpi (Firefox) into your Downloads folder, removing any stale BridgeSwarm-* archives first.
  16. +
+
+
+
+ +
+
+
+ +

Load the extension

+
+ +
+
+

Chrome / Edge

+
    +
  1. Open chrome://extensions.
  2. +
  3. Enable Developer mode (top right).
  4. +
  5. Drag & drop ~/Downloads/BridgeSwarm-1.0.0.zip onto the page — or extract it and use Load unpacked.
  6. +
  7. Confirm the extension ID reads exactly jhmbaojjfkkpoolhkoohklbjokdmbdpm. If it doesn't match, re-download rather than editing IDs by hand.
  8. +
  9. Fully restart the browser (quit, don't just close the window).
  10. +
+
+
+

Firefox

+
    +
  1. Temporary (any Firefox): about:debuggingThis FirefoxLoad Temporary Add-on… → select the downloaded .zip.
  2. +
  3. Permanent (Nightly / Developer Edition only): set xpinstall.signatures.required to false in about:config, then about:addons → gear → Install Add-on From File → select the .xpi.
  4. +
  5. Restart Firefox after installing.
  6. +
+
+
+

Both browsers require a full restart after the first install — native messaging hosts are only re-discovered on browser startup.

+
+
+ +
+
+
+ +

Building from source

+

For contributors, or if you'd rather build the host yourself instead of trusting a prebuilt binary.

+
+
+
git clone https://git.ssh.surf/snxraven/BridgeSwarm.git
+cd BridgeSwarm
+npm run setup          # or ./scripts/install-from-source.sh
+# Then in the browser: Load unpacked → the extension/ directory
+

Requires Node.js for tooling; the host itself runs on Bare >=1.29.4 pulled in as an npm dependency.

+
npm run pack                    # extension zip + xpi
+npm run build:dist:package      # all-platform host zips (best run on CI with bare-build)
+
+
+
+ +
+ +
+ +
+
+
+ +

Common issues

+
+ +
+
+ "Native host has exited" +

The launcher runs Bare via Node. Re-run the installer, or from a source checkout run npm install in native-host/ followed by npm run build:host, so the local bare dependency and launcher paths are correct.

+
+
+ "Access to the specified native messaging host is forbidden" +

Your extension ID doesn't match the native messaging manifest's allowed_origins. From source: ./scripts/update-native-manifest-extension-id.sh YOUR_EXTENSION_ID. For a release install, re-run the installer instead of hand-editing the manifest.

+
+
+ "hrpc not available" +

The HRPC spec hasn't been generated for this build. From a source checkout, run npm run build:hrpc.

+
+
+ macOS: media capability missing after an update +

Run npm run repair:macos so the .bare addons (including bare-ffmpeg) are re-extracted and codesigned, then fully quit the browser (Cmd+Q) and relaunch.

+
+
+ Peers aren't showing up in each other's lists +

Confirm every tab is joined to the exact same topic string, enable debug logging in Settings, and check the Dashboard's Activity tab for connection events.

+
+
+ +

Still stuck? See community & support, or read the full user guide for how the pieces fit together.

+
+
+ +
+
+
+

Ready to write your first P2P app?

+

The user guide walks through Hyperswarm topics, Noise, Hyper* storage, and the Control Center from first principles.

+ +
+
+
+ +
+ +
+ + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..e82eab9 --- /dev/null +++ b/index.html @@ -0,0 +1,479 @@ + + + + + + + + +BridgeSwarm — Hyperswarm P2P in your browser + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + +
+

Peer-to-peer for the ordinary browser

+
+ BridgeSwarm — three peer nodes linked by two bridges +
+

Hyperswarm in the browser.
No server. No fork.

+

BridgeSwarm runs a real Hyperswarm node next to Chrome, Edge, or Firefox and hands your page a plain window.BridgeSwarm API for encrypted, NAT-punching P2P connections and Hyper* storage.

+ +
+
+ 0 + Hyper* storage modules +
+
+ 0 + signaling servers required +
+
+ 0+ + runnable examples +
+
+ 0 + supported browsers +
+
+
+ +
+ +
+
+ Hyperswarm + Noise + Hypercore + Hyperbee + Hyperdrive + Autobase + Hyperdb + Protomux + HRPC + Bare +
+
+ +
+
+
+ +

Stop routing every byte through a server you have to pay for and trust

+

The client–server model puts one machine in the middle of every interaction. BridgeSwarm puts your users directly in touch with each other, with the server relegated to an optional convenience instead of a hard dependency.

+
+ +
+
+

Traditional client–server

+
    +
  • Single point of failure — the server goes down, everyone goes down
  • +
  • Every message pays a round trip through infrastructure you operate
  • +
  • Bandwidth costs scale with your user count, not with peer proximity
  • +
  • All traffic and metadata is visible to whoever runs the server
  • +
  • Shipping a live feature means shipping a backend
  • +
+
+
+

BridgeSwarm P2P

+
    +
  • Peers discover and connect directly — no host to keep online
  • +
  • Data moves point-to-point over Noise-encrypted sockets
  • +
  • Bandwidth is distributed across the swarm, not billed to you
  • +
  • Only participants who join a topic ever see that topic's traffic
  • +
  • Hyper* storage, replication, and RPC ship in the host — for free
  • +
+
+
+
+
+ +
+
+
+ +

What you can build once every tab is a peer

+

Eight shapes of app that get simpler the moment browsers can talk to each other directly.

+
+
+ + +
+ +
+
+
+ +

A full P2P stack, not a wrapper around WebRTC

+

The native host is a real Bare runtime running the same modules that power Holepunch's Pear and Keet — reachable from any page through the extension.

+
+ +
+
+ +

Peer-to-peer, secured by default

+

Every connection is discovered through the Hyperswarm DHT and encrypted before your first byte moves — no certificates, no key exchange to write.

+
    +
  • Noise encryption — every socket is a Noise-protocol secret stream, automatically.
  • +
  • NAT traversal — UDP hole-punching gets peers behind home routers and firewalls talking directly.
  • +
  • Unique identities — each tab gets its own cryptographic key pair.
  • +
+
+
+
const swarm = new BridgeSwarm({ appName: 'my-app' });
+await swarm.join('my-topic');
+
+swarm.on('connection', (conn, peerInfo) => {
+  console.log('peer:', peerInfo.publicKey);
+  conn.write('hello, peer');
+});
+
+
+ +
+
+ +

A real data stack, not a wrapper

+

Hypercore, Hyperbee, Hyperdrive, Autobase, and Hyperdb run in the host and are callable straight from the page.

+
    +
  • Hyper* storage — append-only logs, key/value stores, a P2P filesystem, multi-writer logs, and a schema-based database.
  • +
  • Protomux — multiplex your own message protocols over one connection, in-page.
  • +
  • HRPC — typed, streaming RPC attached on the host with attachHrpc.
  • +
+
+
+
await BridgeSwarm.request('beePut', { key: 'title', value: 'Q3 plan' });
+const doc = await BridgeSwarm.request('beeGet', { key: 'title' });
+
+await BridgeSwarm.request('attachHrpc', { connId });
+const res = await swarm.hrpcCall(connId, 'ping', { value: 'hi' });
+
+
+ +
+
+ +

Live encode that never touches the page

+

bare-ffmpeg runs on the host: probe, transform, batch transcode, and live VP9/WebM encode straight to MSE or a peer connection.

+
    +
  • Live sessions — push captured frames in, stream encoded segments out.
  • +
  • Batch jobs — image/video probing, resizing, and format conversion.
  • +
  • Flexible egress — page playback, swarm fan-out, or archive to disk, in any combination.
  • +
+
+
+
const live = BridgeSwarm.media.liveSession(videoEl);
+await live.start({ width: 640, height: 360, fps: 10 });
+await live.pushFrame(canvas);
+await live.subscribe({ swarm: { connIds: [conn.connId] } });
+
+
+ +
+
+ +

Control Center & examples, out of the box

+

Everything autosaves. There is no config file to hand-edit and no separate deploy step for the demo gallery.

+
    +
  • Control Center — live Overview, Swarms, Connections, and Activity views, plus Settings.
  • +
  • Examples server — one toggle serves a dozen demos at 127.0.0.1:4173.
  • +
  • Debug logging — flip it on in Settings when a connection misbehaves.
  • +
+
+
+
// extension -> host, on settings change
+examplesServer.start({ host: '127.0.0.1', port: 4173 })
+// host responds
+{ running: true, url: 'http://127.0.0.1:4173/' }
+
+
+
+
+ +
+
+
+ +

Three layers: page, extension, Bare host

+

The page never talks to the operating system directly. Everything crosses a narrow, auditable boundary.

+
+ +
+
+ +

Web page

+

window.BridgeSwarm

+
+ +
+ +

Extension (MV3)

+

background + content script

+
+ +
+ +

Bare host

+

Hyperswarm · Hyper* · HRPC

+
+ +
+ +

Remote peers

+

other browsers running BridgeSwarm

+
+
+ +
+
+

postMessage bridge

+

The content script injects api.js; the page and extension talk only through window.postMessage.

+
+
+

Native messaging

+

4-byte little-endian length + UTF-8 JSON between the extension and com.bridgeswarm, ~1 MB per message.

+
+
+

Noise encryption

+

Every peer connection is a Noise secret stream, encrypted and authenticated before data moves.

+
+
+ +
+
+sequenceDiagram
+  participant Page
+  participant ContentScript
+  participant Background
+  participant NativeHost
+  Page->>ContentScript: postMessage (bridge-swarm-bridge)
+  ContentScript->>Background: runtime.sendMessage (send)
+  Background->>NativeHost: port.postMessage {id, type, payload}
+  NativeHost->>NativeHost: handleMessage -> reply(result)
+  NativeHost->>Background: messenger.send {id, type: response}
+  Background->>ContentScript: sendResponse(payload)
+  ContentScript->>Page: dispatchEvent (bridge-swarm-bridge-response)
+        
+
+
+
+ +
+
+
+ +

Install, load the extension, join a topic

+

The full guide covers Windows, verification, and troubleshooting. This is the fast path.

+
+ +
+ + +
+ +
+
+
curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/web-installer.sh | bash
+
+
+
irm https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/install.ps1 | iex
+
+
+ +
+
+
1
+

Install the host

+

Installs to ~/.bridgeswarm and drops BridgeSwarm-1.0.0.zip / .xpi into ~/Downloads.

+
+
+
2
+

Load the extension

+

Open chrome://extensions, enable Developer mode, drag the zip in. Confirm the ID reads jhmbaojjfkkpoolhkoohklbjokdmbdpm, then restart.

+
+
+
3
+

Join a topic

+

await swarm.join('my-topic') and handle the connection event. That's the whole API surface for chat.

+
+
+ + +
+
+ +
+
+
+ +

Give your page a real P2P stack

+

Free, source-available under AGPL-3.0, and running on the same modules that power Holepunch's Pear runtime.

+ +
+
+
+ +
+ +
+ + + + + diff --git a/learn/index.html b/learn/index.html new file mode 100644 index 0000000..cdf820f --- /dev/null +++ b/learn/index.html @@ -0,0 +1,324 @@ + + + + + + + + +How BridgeSwarm Works — Learn + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +

How BridgeSwarm works

+

Everything between window.BridgeSwarm in your page and a live Noise-encrypted socket to a peer's machine — the long version.

+
+
+ +
+
+ + + +
+

Native messaging

+

Browser extensions cannot open sockets, spawn processes, or touch the filesystem directly — that sandbox is exactly what keeps a random web page from doing the same. Native messaging is the narrow, browser-mediated exception: an extension's background script can launch one specific, pre-registered executable and exchange length-prefixed JSON with it over stdin/stdout.

+

BridgeSwarm registers a host named com.bridgeswarm. When you install, the installer writes a manifest — com.bridgeswarm.json — into your browser's native-messaging-hosts directory, pointing at the launcher script for the host binary and listing the extension IDs allowed to connect to it (allowed_origins for Chrome, allowed_extensions for Firefox). The browser enforces that allowlist; nothing else can attach to your BridgeSwarm host.

+

The wire format is deliberately boring: a 4-byte little-endian length prefix followed by UTF-8 JSON, in both directions. Binary payloads (media chunks, Hyperdrive file contents) are base64-encoded inside that JSON, which caps a single host→browser message at roughly 1 MB — one reason live media encode streams segments rather than raw frames.

+
// Message shapes on the wire
+{ id, type, payload }              // request (page -> host)
+{ id, type: 'response', payload }  // response (host -> page)
+{ type: 'event', event, payload }  // event (host -> page, unsolicited)
+
+ +
+

The Bare host

+

Bare is a small JavaScript runtime built by Holepunch specifically to run their P2P stack outside the browser — the same runtime underneath Pear and Keet. BridgeSwarm's native host is a Bare process, not a Node process pretending to be one: a release build ships a self-contained binary, while a from-source build runs Bare via a local bare npm dependency.

+

On startup the host wires up, in order:

+
    +
  • bare-process and default capability packs (media / bare-ffmpeg).
  • +
  • messenger.js — the native messaging framing described above.
  • +
  • host.js — swarm lifecycle, peer firewall/ban, Hyper* data API, Protomux/HRPC attachment, and the optional examples server.
  • +
+

Because it's a real runtime with real native module support, the host can load bare-ffmpeg, bare-fs, and similar packages that would be meaningless inside a browser sandbox — while the page still only ever sees the plain JS object window.BridgeSwarm.

+
+
!
+
The native host runs with your full user privileges — it can open sockets and read/write files. Only install BridgeSwarm builds you trust, the same way you'd treat any other native application.
+
+
+ +
+

Hyperswarm topics as rendezvous

+

There is no BridgeSwarm server that brokers connections. Instead, Hyperswarm uses a public, distributed hash table (DHT) purely as a rendezvous mechanism: peers announce and lookup a 32-byte topic — usually the hash of a string like "my-app-room-42" — and the DHT tells them about each other's network addresses. The actual connection, once addresses are known, is established directly between peers with UDP hole-punching for NAT traversal.

+

From the page, this collapses into one call:

+
const swarm = new BridgeSwarm({ appName: 'my-app' });
+await swarm.join('my-topic');
+swarm.on('connection', (conn, peerInfo) => {
+  console.log('peer:', peerInfo.publicKey);
+});
+

Because the topic is just a shared secret string (or its hash), you can think of it as an address peers "dial" — see Live domains below for where that idea leads. Each browser tab gets its own cryptographic identity, so peerInfo.publicKey is a stable way to recognize a specific peer across reconnects within a session.

+
+ +
+

Noise encryption

+

Once two peers find each other, the connection they get is already a Noise protocol secret stream — encrypted and authenticated before a single application byte crosses the wire. You do not configure TLS certificates, manage a CA, or think about key exchange; it's the default and only mode of a Hyperswarm connection.

+

Each connection the page receives via conn.write(data) / conn.on('data', ...) is one decrypted frame per host-side chunk. If you attach Protomux (see below), wrapRawFrames(conn) preserves that one-chunk-per-frame boundary so message-oriented protocols built on top don't need their own length-prefixing.

+
+ +
+

Hyper* storage

+

The host also runs Holepunch's storage stack on a single Corestore, reachable from the page through BridgeSwarm.request(type, payload). You can use the default instance of each, or open named/keyed resources and pass a resourceId on subsequent calls.

+
+ + + + + + + + + +
ModuleShapeTypical use
HypercoreAppend-only logEvent logs, audit trails, replicated feeds
HyperbeeKey/value B-tree over a HypercoreStructured state, lookups (beeGet/beePut)
HyperdriveP2P filesystemServing files, drag-and-drop sync (driveGet/drivePut/driveList)
AutobaseMulti-writer linearized logMultiple peers appending to one causal history
HyperdbSchema-based P2P databaseTyped collections with query/find (hyperdbInsert/hyperdbFindToArray)
+
+

Two connection-attachment commands turn a live swarm connection into a replication channel instead of a raw data pipe: attachReplication (Hypercore replicates over that socket; the page stops receiving raw data events on it) and swarm.setAutoReplicate(...), which does the same automatically for every new connection on a swarm — see the data-sync use case for a worked example.

+
+ +
+

Protomux & HRPC

+

Two different problems, two different layers:

+
    +
  • Protomux runs in the page. BridgeSwarm injects a bundle of Protomux, compact-encoding, and b4a so you can multiplex several message-oriented protocols over one connection: swarm.createProtomux(conn) gives you a Protomux instance you open channels on with your own protocol name and message encodings.
  • +
  • HRPC runs on the host. Call BridgeSwarm.request('attachHrpc', { connId }) on both ends of a connection, then invoke schema-defined methods with swarm.hrpcCall(connId, method, args) — unary, request-stream, response-stream, and duplex modes are all supported, surfaced to the page as hrpc-chunk / hrpc-end / hrpc-error events.
  • +
+
// Protomux, in the page
+const mux = swarm.createProtomux(conn);
+const ch = mux.createChannel({ protocol: 'my-app/v1' });
+const msg = ch.addMessage({ encoding: c.string, onmessage: console.log });
+ch.open();
+msg.send('hello');
+
+// HRPC, via the host
+await BridgeSwarm.request('attachHrpc', { connId: conn.connId });
+const res = await swarm.hrpcCall(conn.connId, 'ping', { value: 'hi' });
+

Use Protomux when you're happy hand-rolling a small message protocol in the page. Reach for HRPC when you want typed methods and streaming without writing your own dispatcher.

+
+ +
+

Capability packs

+

Not every Bare module the host depends on is exposed to the page — that's intentional. Capability packs are the curated, explicit surface: BridgeSwarm.capabilities.list(), .has(name), and .call(pack, cmd, payload), plus convenience wrappers like BridgeSwarm.media.*.

+

The default host ships the media pack (bare-media + bare-ffmpeg): image/video probing, transforms, batch transcoding, and live VP9/WebM encode from a capture source straight through to MSE playback or a peer connection. Streaming results arrive as cap-chunk / cap-end / cap-error events to stay under the native-messaging size limit. Filesystem access for packs is allowlisted under $BRIDGE_SWARM_STORAGE/cap-jobs/ — absolute paths outside that tree are rejected by the host.

+

See live media for the full flow from camera capture to encoded output.

+
+ +
+

Control Center

+

The extension's dashboard is a live view into the background service worker's state over a dedicated port (bridgeswarm-dashboard): Overview (host connection health), Swarms, Connections, Activity (a log viewer), and Settings. Every setting — default app name, max peers, request/ready timeouts, whether to skip injection on file://, disconnect notifications, the examples-server toggle, debug logging — autosaves on change. There is no Save button and no separate config file to hand-edit.

+
+ +
+

Mental model: live domains & self-hosted apps

+

Two ideas that make more sense once the pieces above click into place:

+

A topic as a live domain. A DNS name resolves to an IP once, statically, through infrastructure you don't control. A Hyperswarm topic is closer to an address you dial directly: whoever currently has the host running and has joined that topic is reachable, with no registrar, no hosting bill, and no server between you and them. You can treat a topic string as a lightweight, ephemeral "domain" for a specific room, session, or shared document — it exists exactly as long as at least one peer is announcing it.

+

Self-hosted web apps, for free. The native host's optional examples server is a small bare-http1 static server bound to 127.0.0.1:4173. It's a working demonstration of a broader pattern: once BridgeSwarm is installed, the host can serve real HTML/CSS/JS on localhost, and the extension's content script still injects window.BridgeSwarm into pages loaded from it (deliberately not into file:// pages, which the browser treats as opaque, unrelated origins). That means a fully self-hosted, offline-capable app — with P2P networking built in — can live entirely on a user's machine with no cloud hosting step at all. See self-hosted web apps.

+
+ +
+

Message flows

+

A request from the page to the host and back:

+
+sequenceDiagram
+  participant Page
+  participant ContentScript
+  participant Background
+  participant NativeHost
+  Page->>ContentScript: postMessage (bridge-swarm-bridge)
+  ContentScript->>Background: runtime.sendMessage (action: send)
+  Background->>NativeHost: port.postMessage {id, type, payload}
+  NativeHost->>NativeHost: handleMessage -> reply(result)
+  NativeHost->>Background: messenger.send {id, type: response}
+  Background->>ContentScript: sendResponse(payload)
+  ContentScript->>Page: dispatchEvent (bridge-swarm-bridge-response)
+        
+

An unsolicited event from the host (a new connection, a capability chunk) reaching the page:

+
+sequenceDiagram
+  participant NativeHost
+  participant Background
+  participant ContentScript
+  participant Page
+  NativeHost->>NativeHost: emit(event, payload)
+  NativeHost->>Background: messenger.send {type: event}
+  alt swarm-scoped event
+    Background->>ContentScript: tabs.sendMessage (only tabs owning swarmId)
+  else capability / global event
+    Background->>ContentScript: tabs.sendMessage (all subscribed tabs)
+  end
+  ContentScript->>Page: dispatchEvent (bridge-swarm-event)
+  Page->>Page: api.js _onEvent (connection / data / end / error / cap-*)
+        
+

And the connection lifecycle underneath every swarm:

+
+stateDiagram-v2
+  [*] --> Init: new BridgeSwarm + join(topic)
+  Init --> Discovering: host Hyperswarm.join
+  Discovering --> Connected: peer Noise handshake
+  Connected --> DataForward: page conn.write / data events
+  Connected --> Attached: attachReplication or attachHrpc
+  DataForward --> Closed: end / error / destroy
+  Attached --> Closed: end / destroy
+  Closed --> [*]
+        
+
+ +
+

Glossary

+
+ + + + + + + + + + + + + + + + + +
BareA small JavaScript runtime for native modules, built by Holepunch to run Pear, Keet, and BridgeSwarm's native host outside the browser sandbox.
HyperswarmPeer discovery and connection library built on a DHT; peers announce/lookup a topic and connect directly, with NAT traversal handled automatically.
TopicA 32-byte value (often a hash of a human-readable string) that peers join to discover each other. No central registration required.
Noise protocolThe handshake and encryption framework used to secure every Hyperswarm connection by default.
CorestoreA factory/manager for multiple named Hypercores backed by one storage directory.
HypercoreAn append-only, cryptographically verifiable log — the base primitive under Hyperbee, Hyperdrive, and Autobase.
HyperbeeA key/value B-tree store built on a Hypercore.
HyperdriveA P2P filesystem built on Hypercore, with paths, directory listings, and file content.
AutobaseA multi-writer linearized log that merges several Hypercores from different peers into one causal history.
HyperdbA schema-based (Hyperschema-generated) P2P database with typed collections and queries.
ProtomuxA library for multiplexing multiple named protocols/channels over one connection, used in-page by BridgeSwarm.
HRPCSchema-defined RPC (unary and streaming) that BridgeSwarm runs on the host side of an attached connection.
Native messagingThe browser API that lets an extension exchange length-prefixed JSON with one specific, registered local executable.
Capability packA curated set of host-side commands (e.g. media) exposed to the page through BridgeSwarm.capabilities.
+
+
+ + + +
+
+ +
+
+
+

See it running

+

Every concept above has a runnable demo on the examples server once BridgeSwarm is installed.

+ +
+
+
+ +
+ +
+ + + + diff --git a/legal/aup.html b/legal/aup.html new file mode 100644 index 0000000..23bd259 --- /dev/null +++ b/legal/aup.html @@ -0,0 +1,185 @@ + + + + + + + + + Acceptable Use Policy · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/legal/cookies.html b/legal/cookies.html new file mode 100644 index 0000000..6d757f5 --- /dev/null +++ b/legal/cookies.html @@ -0,0 +1,106 @@ + + + + + + + + + Cookie Policy → Privacy Policy · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Cookie information is included in the + Privacy Policy. +

+ + diff --git a/legal/copyright.html b/legal/copyright.html new file mode 100644 index 0000000..f111db8 --- /dev/null +++ b/legal/copyright.html @@ -0,0 +1,187 @@ + + + + + + + + + Copyright & DMCA · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/legal/disclaimer.html b/legal/disclaimer.html new file mode 100644 index 0000000..20e6172 --- /dev/null +++ b/legal/disclaimer.html @@ -0,0 +1,214 @@ + + + + + + + + + Disclaimer & Liability · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/legal/eula.html b/legal/eula.html new file mode 100644 index 0000000..c95b88c --- /dev/null +++ b/legal/eula.html @@ -0,0 +1,281 @@ + + + + + + + + + End User License Agreement · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/legal/index.html b/legal/index.html new file mode 100644 index 0000000..09a8859 --- /dev/null +++ b/legal/index.html @@ -0,0 +1,205 @@ + + + + + + + + + Legal · BridgeSwarm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/legal/license.html b/legal/license.html new file mode 100644 index 0000000..f0126b8 --- /dev/null +++ b/legal/license.html @@ -0,0 +1,224 @@ + + + + + + + + + Open Source Notices · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/legal/privacy.html b/legal/privacy.html new file mode 100644 index 0000000..0967ac8 --- /dev/null +++ b/legal/privacy.html @@ -0,0 +1,359 @@ + + + + + + + + + Privacy Policy · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/legal/security.html b/legal/security.html new file mode 100644 index 0000000..5c9db63 --- /dev/null +++ b/legal/security.html @@ -0,0 +1,217 @@ + + + + + + + + + Security Policy · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/legal/terms.html b/legal/terms.html new file mode 100644 index 0000000..54b2f51 --- /dev/null +++ b/legal/terms.html @@ -0,0 +1,287 @@ + + + + + + + + + Terms of Service · BridgeSwarm Legal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8465fa3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2202 @@ +{ + "name": "bridgeswarm-website", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bridgeswarm-website", + "version": "1.0.0", + "license": "AGPL-3.0", + "dependencies": { + "mermaid": "^11.16.0" + }, + "devDependencies": { + "vite": "^6.3.5" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mermaid": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6282fc5 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "bridgeswarm-website", + "version": "1.0.0", + "private": true, + "description": "Official BridgeSwarm marketing site, user guide, and developer documentation", + "type": "module", + "license": "AGPL-3.0", + "author": "HoneyPeer, LLC", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "start": "node server.mjs", + "serve": "node server.mjs" + }, + "devDependencies": { + "vite": "^6.3.5" + }, + "dependencies": { + "mermaid": "^11.16.0" + } +} diff --git a/public/assets/brand/colors.css b/public/assets/brand/colors.css new file mode 100644 index 0000000..ad2badf --- /dev/null +++ b/public/assets/brand/colors.css @@ -0,0 +1,37 @@ +/* BridgeSwarm Brand Color Tokens + Source of truth — synced into assets/brand/colors.css + Keep in sync with extension/panel.css Control Center +*/ +:root { + --bs-bg-primary: #0c0c10; + --bs-bg-secondary: #141418; + --bs-bg-tertiary: #1a1a20; + --bs-bg-elevated: #1a1a20; + --bs-bg-hover: #22222a; + + --bs-accent-primary: #2dd4bf; + --bs-accent-secondary: #14b8a6; + --bs-accent-success: #34d399; + --bs-accent-warning: #fbbf24; + --bs-accent-danger: #fb7185; + --bs-accent-info: #60a5fa; + + --bs-text-primary: #ececf1; + --bs-text-secondary: #c8c8d4; + --bs-text-muted: #8b8b9a; + --bs-text-faint: #5c5c6c; + + --bs-border: rgba(255, 255, 255, 0.1); + --bs-border-strong: #3a3a46; + + --bs-logo-node: #2dd4bf; + --bs-logo-node-alt: #38bdf8; + --bs-logo-bridge: #2dd4bf; + --bs-logo-plate: #0c0c10; + + --bs-radius: 10px; + --bs-radius-sm: 7px; + + --bs-font: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif; + --bs-mono: "Cascadia Code", "SF Mono", Consolas, monospace; +} diff --git a/public/assets/brand/header-banner.png b/public/assets/brand/header-banner.png new file mode 100644 index 0000000..5971f44 Binary files /dev/null and b/public/assets/brand/header-banner.png differ diff --git a/public/assets/brand/hero-bg-1280.png b/public/assets/brand/hero-bg-1280.png new file mode 100644 index 0000000..0709d26 Binary files /dev/null and b/public/assets/brand/hero-bg-1280.png differ diff --git a/public/assets/brand/hero-bg.png b/public/assets/brand/hero-bg.png new file mode 100644 index 0000000..0709d26 Binary files /dev/null and b/public/assets/brand/hero-bg.png differ diff --git a/public/assets/brand/logo-header.png b/public/assets/brand/logo-header.png new file mode 100644 index 0000000..ea1d2bb Binary files /dev/null and b/public/assets/brand/logo-header.png differ diff --git a/public/assets/brand/og-image.png b/public/assets/brand/og-image.png new file mode 100644 index 0000000..e0958df Binary files /dev/null and b/public/assets/brand/og-image.png differ diff --git a/public/assets/brand/twitter-card.png b/public/assets/brand/twitter-card.png new file mode 100644 index 0000000..814bede Binary files /dev/null and b/public/assets/brand/twitter-card.png differ diff --git a/public/assets/favicons/android-chrome-192x192.png b/public/assets/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..e37aca9 Binary files /dev/null and b/public/assets/favicons/android-chrome-192x192.png differ diff --git a/public/assets/favicons/android-chrome-512x512.png b/public/assets/favicons/android-chrome-512x512.png new file mode 100644 index 0000000..f253554 Binary files /dev/null and b/public/assets/favicons/android-chrome-512x512.png differ diff --git a/public/assets/favicons/apple-touch-icon.png b/public/assets/favicons/apple-touch-icon.png new file mode 100644 index 0000000..2ecdafa Binary files /dev/null and b/public/assets/favicons/apple-touch-icon.png differ diff --git a/public/assets/favicons/browserconfig.xml b/public/assets/favicons/browserconfig.xml new file mode 100644 index 0000000..cc1fdb2 --- /dev/null +++ b/public/assets/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #0c0c10 + + + diff --git a/public/assets/favicons/favicon-16x16.png b/public/assets/favicons/favicon-16x16.png new file mode 100644 index 0000000..a1580a0 Binary files /dev/null and b/public/assets/favicons/favicon-16x16.png differ diff --git a/public/assets/favicons/favicon-32x32.png b/public/assets/favicons/favicon-32x32.png new file mode 100644 index 0000000..e1e0ce3 Binary files /dev/null and b/public/assets/favicons/favicon-32x32.png differ diff --git a/public/assets/favicons/favicon-48x48.png b/public/assets/favicons/favicon-48x48.png new file mode 100644 index 0000000..8326694 Binary files /dev/null and b/public/assets/favicons/favicon-48x48.png differ diff --git a/public/assets/favicons/favicon.ico b/public/assets/favicons/favicon.ico new file mode 100644 index 0000000..3c0b543 Binary files /dev/null and b/public/assets/favicons/favicon.ico differ diff --git a/public/assets/favicons/mstile-144x144.png b/public/assets/favicons/mstile-144x144.png new file mode 100644 index 0000000..2c441cf Binary files /dev/null and b/public/assets/favicons/mstile-144x144.png differ diff --git a/public/assets/favicons/mstile-150x150.png b/public/assets/favicons/mstile-150x150.png new file mode 100644 index 0000000..b999852 Binary files /dev/null and b/public/assets/favicons/mstile-150x150.png differ diff --git a/public/assets/favicons/site.webmanifest b/public/assets/favicons/site.webmanifest new file mode 100644 index 0000000..c8eafbe --- /dev/null +++ b/public/assets/favicons/site.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "BridgeSwarm", + "short_name": "BridgeSwarm", + "description": "P2P Hyperswarm in the browser via native messaging host", + "icons": [ + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#2dd4bf", + "background_color": "#0c0c10", + "display": "standalone" +} diff --git a/public/assets/logo/bridgeswarm-icon-1024.png b/public/assets/logo/bridgeswarm-icon-1024.png new file mode 100644 index 0000000..d21d214 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-1024.png differ diff --git a/public/assets/logo/bridgeswarm-icon-128.png b/public/assets/logo/bridgeswarm-icon-128.png new file mode 100644 index 0000000..ad18944 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-128.png differ diff --git a/public/assets/logo/bridgeswarm-icon-16.png b/public/assets/logo/bridgeswarm-icon-16.png new file mode 100644 index 0000000..a1580a0 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-16.png differ diff --git a/public/assets/logo/bridgeswarm-icon-2048.png b/public/assets/logo/bridgeswarm-icon-2048.png new file mode 100644 index 0000000..06b2f25 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-2048.png differ diff --git a/public/assets/logo/bridgeswarm-icon-256.png b/public/assets/logo/bridgeswarm-icon-256.png new file mode 100644 index 0000000..ea1d2bb Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-256.png differ diff --git a/public/assets/logo/bridgeswarm-icon-32.png b/public/assets/logo/bridgeswarm-icon-32.png new file mode 100644 index 0000000..e1e0ce3 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-32.png differ diff --git a/public/assets/logo/bridgeswarm-icon-48.png b/public/assets/logo/bridgeswarm-icon-48.png new file mode 100644 index 0000000..8326694 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-48.png differ diff --git a/public/assets/logo/bridgeswarm-icon-512.png b/public/assets/logo/bridgeswarm-icon-512.png new file mode 100644 index 0000000..f253554 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-512.png differ diff --git a/public/assets/logo/bridgeswarm-icon-64.png b/public/assets/logo/bridgeswarm-icon-64.png new file mode 100644 index 0000000..accc60c Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-64.png differ diff --git a/public/assets/logo/bridgeswarm-icon-dark.png b/public/assets/logo/bridgeswarm-icon-dark.png new file mode 100644 index 0000000..6569ee9 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-dark.png differ diff --git a/public/assets/logo/bridgeswarm-icon-illustrated.png b/public/assets/logo/bridgeswarm-icon-illustrated.png new file mode 100644 index 0000000..5f46bd2 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-illustrated.png differ diff --git a/public/assets/logo/bridgeswarm-icon-light.png b/public/assets/logo/bridgeswarm-icon-light.png new file mode 100644 index 0000000..3fd7142 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-light.png differ diff --git a/public/assets/logo/bridgeswarm-icon-master-padded.png b/public/assets/logo/bridgeswarm-icon-master-padded.png new file mode 100644 index 0000000..06b2f25 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-master-padded.png differ diff --git a/public/assets/logo/bridgeswarm-icon-master.png b/public/assets/logo/bridgeswarm-icon-master.png new file mode 100644 index 0000000..06b2f25 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-master.png differ diff --git a/public/assets/logo/bridgeswarm-icon-mono.png b/public/assets/logo/bridgeswarm-icon-mono.png new file mode 100644 index 0000000..96cc645 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-mono.png differ diff --git a/public/assets/logo/bridgeswarm-icon-teal.png b/public/assets/logo/bridgeswarm-icon-teal.png new file mode 100644 index 0000000..d21d214 Binary files /dev/null and b/public/assets/logo/bridgeswarm-icon-teal.png differ diff --git a/public/assets/logo/bridgeswarm-logo-dark.png b/public/assets/logo/bridgeswarm-logo-dark.png new file mode 100644 index 0000000..4d76063 Binary files /dev/null and b/public/assets/logo/bridgeswarm-logo-dark.png differ diff --git a/public/assets/logo/bridgeswarm-logo-full-1200.png b/public/assets/logo/bridgeswarm-logo-full-1200.png new file mode 100644 index 0000000..e3eecb9 Binary files /dev/null and b/public/assets/logo/bridgeswarm-logo-full-1200.png differ diff --git a/public/assets/logo/bridgeswarm-logo-full-800.png b/public/assets/logo/bridgeswarm-logo-full-800.png new file mode 100644 index 0000000..aa57e74 Binary files /dev/null and b/public/assets/logo/bridgeswarm-logo-full-800.png differ diff --git a/public/assets/logo/bridgeswarm-logo-full-transparent.png b/public/assets/logo/bridgeswarm-logo-full-transparent.png new file mode 100644 index 0000000..c3afe87 Binary files /dev/null and b/public/assets/logo/bridgeswarm-logo-full-transparent.png differ diff --git a/public/assets/logo/bridgeswarm-logo-light.png b/public/assets/logo/bridgeswarm-logo-light.png new file mode 100644 index 0000000..3d7bc34 Binary files /dev/null and b/public/assets/logo/bridgeswarm-logo-light.png differ diff --git a/public/assets/logo/bridgeswarm-wordmark-dark.svg b/public/assets/logo/bridgeswarm-wordmark-dark.svg new file mode 100644 index 0000000..307ce9b --- /dev/null +++ b/public/assets/logo/bridgeswarm-wordmark-dark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + Bridge + Swarm + diff --git a/public/assets/logo/bridgeswarm-wordmark.svg b/public/assets/logo/bridgeswarm-wordmark.svg new file mode 100644 index 0000000..08e277a --- /dev/null +++ b/public/assets/logo/bridgeswarm-wordmark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + Bridge + Swarm + diff --git a/public/assets/logo/source-original.png b/public/assets/logo/source-original.png new file mode 100644 index 0000000..79cea29 Binary files /dev/null and b/public/assets/logo/source-original.png differ diff --git a/public/assets/social/avatar-400.png b/public/assets/social/avatar-400.png new file mode 100644 index 0000000..48c1337 Binary files /dev/null and b/public/assets/social/avatar-400.png differ diff --git a/public/assets/social/avatar-800.png b/public/assets/social/avatar-800.png new file mode 100644 index 0000000..c07e549 Binary files /dev/null and b/public/assets/social/avatar-800.png differ diff --git a/public/assets/social/avatar-circle-dark.png b/public/assets/social/avatar-circle-dark.png new file mode 100644 index 0000000..a9fd231 Binary files /dev/null and b/public/assets/social/avatar-circle-dark.png differ diff --git a/public/assets/social/social-banner.png b/public/assets/social/social-banner.png new file mode 100644 index 0000000..ec96d8e Binary files /dev/null and b/public/assets/social/social-banner.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..7e926ca --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://bridgeswarm.dev/sitemap.xml diff --git a/releases.html b/releases.html new file mode 100644 index 0000000..81c984a --- /dev/null +++ b/releases.html @@ -0,0 +1,170 @@ + + + + + + + + +Releases — BridgeSwarm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +

Release history

+

Every build is published to our self-hosted Gitea. latest-main is a rolling prerelease rebuilt on every push to main; tags like v1.0.0 are stable, versioned releases.

+
+
+ +
+
+
+ +

Release channels

+
+ +
+
+ Rolling +

latest-main

+

Rebuilt automatically on every push to main. This is what web-installer.sh and install.ps1 pull from — always current, not guaranteed stable.

+

View on Gitea →

+
+
+ Stable +

Tagged versions

+

Pushing a v*.*.* tag cuts a versioned Gitea release with the same platform artifacts as the rolling build.

+

Browse all releases →

+
+
+
+
+ +
+
+
+
+ +

Latest builds

+
+ All releases on Gitea → +
+ +
+ +
+ + +
+
+ +
+
+
+

Most people don't need this page

+

The one-line installer always grabs the current latest-main build for you.

+ +
+
+
+ +
+ +
+ + + + + diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..fd33460 --- /dev/null +++ b/server.mjs @@ -0,0 +1,428 @@ +/** + * BridgeSwarm website server: static dist/ + Gitea releases API proxy + dynamic sitemap. + * + * Routes: + * GET /sitemap.xml → Google-compatible sitemap (generated from dist/) + * GET /api/releases → list releases + * GET /api/releases/tags/:tag → single release by tag + * GET /api/releases/latest → latest non-draft (or rolling if present) + * + * Env: + * PORT (default 4174) + * DIST (default ./dist) + * SITE_ORIGIN (default https://bridgeswarm.dev) + * GITEA_API (default https://git.ssh.surf/api/v1/repos/snxraven/BridgeSwarm) + */ +import http from 'node:http' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const PORT = Number(process.env.PORT || 4174) +const DIST = path.resolve(process.env.DIST || path.join(__dirname, 'dist')) +const SITE_ORIGIN = String(process.env.SITE_ORIGIN || 'https://bridgeswarm.dev').replace( + /\/+$/, + '' +) +const GITEA_API = + process.env.GITEA_API || 'https://git.ssh.surf/api/v1/repos/snxraven/BridgeSwarm' +const GITEA_WEB = + process.env.GITEA_WEB || 'https://git.ssh.surf/snxraven/BridgeSwarm' + +/** Paths that must not appear in the public sitemap (noindex / redirect stubs). */ +const SITEMAP_EXCLUDE = new Set(['/legal/cookies']) + +const MIME = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.map': 'application/json', + '.txt': 'text/plain; charset=utf-8', + '.xml': 'application/xml; charset=utf-8', + '.webmanifest': 'application/manifest+json', +} + +function send(res, status, body, headers = {}) { + const buf = Buffer.isBuffer(body) ? body : Buffer.from(body ?? '') + res.writeHead(status, { + 'Content-Length': buf.length, + ...headers, + }) + res.end(buf) +} + +function sendJson(res, status, obj) { + send(res, status, JSON.stringify(obj), { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'public, max-age=60', + }) +} + +async function proxyGitea(apiPath, search = '') { + const url = `${GITEA_API}${apiPath}${search || ''}` + const r = await fetch(url, { + headers: { + Accept: 'application/json', + 'User-Agent': 'bridgeswarm-website-releases/1.0', + }, + }) + const text = await r.text() + let data + try { + data = text ? JSON.parse(text) : null + } catch { + data = { message: text || 'Invalid JSON from Gitea' } + } + return { status: r.status, data, url } +} + +async function handleApi(req, res, url) { + if (req.method === 'OPTIONS') { + send(res, 204, '', { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }) + return + } + if (req.method !== 'GET' && req.method !== 'HEAD') { + sendJson(res, 405, { error: 'Method not allowed' }) + return + } + + try { + // GET /api/releases + if (url.pathname === '/api/releases' || url.pathname === '/api/releases/') { + const qs = new URLSearchParams(url.search) + if (!qs.has('limit')) qs.set('limit', '25') + const { status, data } = await proxyGitea('/releases', `?${qs}`) + // Annotate with canonical web links + if (Array.isArray(data)) { + for (const rel of data) { + if (rel && !rel.html_url && rel.tag_name) { + rel.html_url = `${GITEA_WEB}/releases/tag/${encodeURIComponent(rel.tag_name)}` + } + } + } + sendJson(res, status, { + source: GITEA_WEB + '/releases', + api: GITEA_API + '/releases', + releases: data, + }) + return + } + + // GET /api/releases/latest + if (url.pathname === '/api/releases/latest') { + // Prefer rolling tag if present, else first non-draft + const list = await proxyGitea('/releases', '?limit=50') + if (!Array.isArray(list.data)) { + sendJson(res, list.status, list.data) + return + } + const rolling = list.data.find( + (r) => (r.tag_name === 'rolling' || r.tag_name === 'latest-main') && !r.draft + ) + const first = list.data.find((r) => !r.draft) || list.data[0] + const pick = rolling || first + if (!pick) { + sendJson(res, 404, { error: 'No releases found' }) + return + } + sendJson(res, 200, { + source: GITEA_WEB + '/releases', + release: pick, + }) + return + } + + // GET /api/releases/tags/:tag + const tagMatch = url.pathname.match(/^\/api\/releases\/tags\/([^/]+)\/?$/) + if (tagMatch) { + const tag = decodeURIComponent(tagMatch[1]) + const { status, data } = await proxyGitea( + `/releases/tags/${encodeURIComponent(tag)}` + ) + sendJson(res, status, { + source: `${GITEA_WEB}/releases/tag/${encodeURIComponent(tag)}`, + release: data, + }) + return + } + + sendJson(res, 404, { + error: 'Unknown API route', + routes: [ + 'GET /api/releases', + 'GET /api/releases/latest', + 'GET /api/releases/tags/:tag', + ], + }) + } catch (err) { + console.error('[api]', err) + sendJson(res, 502, { + error: 'Failed to reach Gitea releases API', + message: err?.message || String(err), + upstream: GITEA_API, + }) + } +} + +function safeJoin(root, reqPath) { + const decoded = decodeURIComponent(reqPath.split('?')[0]) + const cleaned = path.normalize(decoded).replace(/^(\.\.(\/|\\|$))+/, '') + const full = path.join(root, cleaned) + if (!full.startsWith(root)) return null + return full +} + +function escapeXml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +/** dist-relative path → public URL path (clean URLs used by the site). */ +function distHtmlToUrlPath(relPosix) { + const rel = relPosix.replace(/\\/g, '/').replace(/^\//, '') + if (rel === 'index.html') return '/' + if (rel.endsWith('/index.html')) { + return `/${rel.slice(0, -'index.html'.length)}` + } + if (rel.endsWith('.html')) return `/${rel.slice(0, -'.html'.length)}` + return `/${rel}` +} + +function walkHtmlFiles(dir, baseRel = '') { + /** @type {{ rel: string, abs: string, mtime: Date }[]} */ + const out = [] + let entries + try { + entries = fs.readdirSync(dir, { withFileTypes: true }) + } catch { + return out + } + for (const ent of entries) { + if (ent.name.startsWith('.')) continue + if (ent.name === 'assets' || ent.name === 'node_modules') continue + const abs = path.join(dir, ent.name) + const rel = baseRel ? `${baseRel}/${ent.name}` : ent.name + if (ent.isDirectory()) { + out.push(...walkHtmlFiles(abs, rel)) + continue + } + if (ent.isFile() && ent.name.endsWith('.html')) { + let mtime = new Date() + try { + mtime = fs.statSync(abs).mtime + } catch { + /* keep now */ + } + out.push({ rel: rel.replace(/\\/g, '/'), abs, mtime }) + } + } + return out +} + +function sitemapPriority(urlPath) { + if (urlPath === '/') return '1.0' + if (urlPath === '/download' || urlPath === '/releases') return '0.95' + if (urlPath === '/learn/' || urlPath === '/docs/' || urlPath === '/use-cases/') return '0.9' + if (urlPath === '/community') return '0.8' + if (urlPath.startsWith('/docs/')) return '0.85' + if (urlPath === '/legal/') return '0.5' + if (urlPath.startsWith('/legal/')) return '0.4' + return '0.7' +} + +function sitemapChangefreq(urlPath) { + if (urlPath === '/releases') return 'daily' + if (urlPath === '/' || urlPath === '/download' || urlPath.startsWith('/docs')) return 'weekly' + if (urlPath.startsWith('/legal')) return 'yearly' + return 'monthly' +} + +/** + * Build a Google Search-compatible sitemap 0.9 document from dist HTML pages. + * Strict sitemaps.org protocol only (loc, lastmod, changefreq, priority). + * @see https://www.sitemaps.org/protocol.html + * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap + */ +function buildSitemapXml() { + const pages = walkHtmlFiles(DIST) + /** @type {Map} */ + const byPath = new Map() + + for (const page of pages) { + const urlPath = distHtmlToUrlPath(page.rel) + const excludeKey = urlPath.replace(/\/+$/, '') || '/' + if (SITEMAP_EXCLUDE.has(excludeKey) || SITEMAP_EXCLUDE.has(urlPath)) continue + + // W3C Datetime (date only is accepted by Google) + const lastmod = page.mtime.toISOString().slice(0, 10) + const existing = byPath.get(urlPath) + if (existing && existing.lastmod >= lastmod) continue + + // Absolute HTTPS loc only · required by the protocol + const loc = SITE_ORIGIN + (urlPath === '/' ? '/' : urlPath) + byPath.set(urlPath, { + loc, + lastmod, + changefreq: sitemapChangefreq(urlPath), + priority: sitemapPriority(urlPath), + }) + } + + const entries = [...byPath.values()].sort((a, b) => { + if (a.loc === `${SITE_ORIGIN}/`) return -1 + if (b.loc === `${SITE_ORIGIN}/`) return 1 + return a.loc.localeCompare(b.loc) + }) + + // One declaration + default namespace only (no xhtml extras · single-language site) + const parts = [ + '', + '', + ] + + for (const e of entries) { + parts.push( + ' ', + ` ${escapeXml(e.loc)}`, + ` ${escapeXml(e.lastmod)}`, + ` ${escapeXml(e.changefreq)}`, + ` ${escapeXml(e.priority)}`, + ' ' + ) + } + + parts.push('') + // Trailing newline for POSIX-friendly files + return parts.join('\n') + '\n' +} + +function serveSitemap(req, res) { + try { + const xml = buildSitemapXml() + // text/xml is what Google documents for sitemaps; charset for clarity + const headers = { + 'Content-Type': 'text/xml; charset=utf-8', + 'Cache-Control': 'public, max-age=300', + } + if (req.method === 'HEAD') { + res.writeHead(200, { + ...headers, + 'Content-Length': Buffer.byteLength(xml, 'utf8'), + }) + res.end() + return + } + send(res, 200, xml, headers) + } catch (err) { + console.error('[sitemap]', err) + send(res, 500, 'Sitemap generation failed', { + 'Content-Type': 'text/plain; charset=utf-8', + }) + } +} + +function resolveStatic(urlPath) { + let p = urlPath + if (p.endsWith('/')) p += 'index.html' + let file = safeJoin(DIST, p) + if (!file) return null + + if (existsFile(file)) return file + + // clean URLs: /releases → releases.html, /docs/foo → docs/foo.html + if (!path.extname(file)) { + const asHtml = file + '.html' + if (existsFile(asHtml)) return asHtml + const asIndex = path.join(file, 'index.html') + if (existsFile(asIndex)) return asIndex + } + + return null +} + +function existsFile(f) { + try { + return fs.statSync(f).isFile() + } catch { + return false + } +} + +function serveStatic(req, res, url) { + const file = resolveStatic(url.pathname === '/' ? '/index.html' : url.pathname) + if (!file) { + send(res, 404, 'Not found', { 'Content-Type': 'text/plain; charset=utf-8' }) + return + } + const ext = path.extname(file).toLowerCase() + const type = MIME[ext] || 'application/octet-stream' + const stream = fs.createReadStream(file) + res.writeHead(200, { + 'Content-Type': type, + 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600', + }) + if (req.method === 'HEAD') { + res.end() + return + } + stream.pipe(res) + stream.on('error', () => { + if (!res.headersSent) send(res, 500, 'Read error') + else res.end() + }) +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`) + + // Health + if (url.pathname === '/api/health') { + sendJson(res, 200, { ok: true, gitea: GITEA_API, site: SITE_ORIGIN }) + return + } + + if (url.pathname.startsWith('/api/')) { + await handleApi(req, res, url) + return + } + + if (req.method !== 'GET' && req.method !== 'HEAD') { + send(res, 405, 'Method not allowed', { 'Content-Type': 'text/plain' }) + return + } + + // Dynamic sitemap (overrides any static dist/sitemap.xml) + if (url.pathname === '/sitemap.xml' || url.pathname === '/sitemap.xml/') { + serveSitemap(req, res) + return + } + + serveStatic(req, res, url) +}) + +server.listen(PORT, () => { + console.log(`[bridgeswarm-website] http://127.0.0.1:${PORT}`) + console.log(`[bridgeswarm-website] dist=${DIST}`) + console.log(`[bridgeswarm-website] origin=${SITE_ORIGIN}`) + console.log(`[bridgeswarm-website] gitea=${GITEA_API}`) + console.log(`[bridgeswarm-website] sitemap=/sitemap.xml releases=/releases api=/api/releases`) +}) diff --git a/src/js/home.js b/src/js/home.js new file mode 100644 index 0000000..70d2026 --- /dev/null +++ b/src/js/home.js @@ -0,0 +1,270 @@ +/** + * BridgeSwarm home page: hero swarm-mesh animation, install tabs, stat count-up. + * Kept intentionally tasteful — a handful of purposeful motions, nothing busy. + */ + +const REDUCED_MOTION = window.matchMedia('(prefers-reduced-motion: reduce)').matches + +/** + * Read a CSS custom property from :root, with fallback. + * @param {string} name + * @param {string} fallback + */ +function cssVar(name, fallback) { + const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim() + return v || fallback +} + +/** + * Convert a hex color (#rrggbb) to an "r, g, b" string for use in rgba(). + * @param {string} hex + */ +function hexToRgb(hex) { + const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim()) + if (!m) return '45, 212, 191' + return `${parseInt(m[1], 16)}, ${parseInt(m[2], 16)}, ${parseInt(m[3], 16)}` +} + +/** + * Subtle animated swarm mesh: a field of drifting nodes that connect with + * faint lines when close together, occasionally pulsing — evokes P2P peers + * discovering one another. Pauses off-screen and on tab blur; respects + * prefers-reduced-motion (renders one static frame instead of animating). + */ +function initHeroMesh() { + const canvas = document.querySelector('.hero-canvas') + if (!canvas || !(canvas instanceof HTMLCanvasElement)) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + let width = 0 + let height = 0 + let dpr = Math.min(window.devicePixelRatio || 1, 2) + /** @type {{x:number,y:number,vx:number,vy:number,r:number,pulse:number,pulseSpeed:number}[]} */ + let nodes = [] + let accentRgb = hexToRgb(cssVar('--accent', '#2dd4bf')) + let infoRgb = hexToRgb(cssVar('--info', '#60a5fa')) + let running = false + let rafId = 0 + + const NODE_COUNT_PER_100K_PX = 1.1 + const MAX_LINK_DIST = 150 + const MAX_NODES = 90 + + function syncColors() { + accentRgb = hexToRgb(cssVar('--accent', '#2dd4bf')) + infoRgb = hexToRgb(cssVar('--info', '#60a5fa')) + } + + function makeNodes() { + const area = width * height + const count = Math.max(24, Math.min(MAX_NODES, Math.round((area / 100000) * NODE_COUNT_PER_100K_PX))) + nodes = Array.from({ length: count }, () => ({ + x: Math.random() * width, + y: Math.random() * height, + vx: (Math.random() - 0.5) * 0.18, + vy: (Math.random() - 0.5) * 0.18, + r: 1.1 + Math.random() * 1.6, + pulse: Math.random() * Math.PI * 2, + pulseSpeed: 0.006 + Math.random() * 0.01, + })) + } + + function resize() { + const rect = canvas.getBoundingClientRect() + width = Math.max(1, Math.round(rect.width)) + height = Math.max(1, Math.round(rect.height)) + dpr = Math.min(window.devicePixelRatio || 1, 2) + canvas.width = Math.round(width * dpr) + canvas.height = Math.round(height * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + makeNodes() + } + + function step() { + ctx.clearRect(0, 0, width, height) + + // Update + draw links first (so nodes sit on top) + for (let i = 0; i < nodes.length; i++) { + const a = nodes[i] + a.x += a.vx + a.y += a.vy + a.pulse += a.pulseSpeed + + if (a.x < -20) a.x = width + 20 + if (a.x > width + 20) a.x = -20 + if (a.y < -20) a.y = height + 20 + if (a.y > height + 20) a.y = -20 + + for (let j = i + 1; j < nodes.length; j++) { + const b = nodes[j] + const dx = a.x - b.x + const dy = a.y - b.y + const dist = Math.hypot(dx, dy) + if (dist < MAX_LINK_DIST) { + const alpha = (1 - dist / MAX_LINK_DIST) * 0.22 + ctx.strokeStyle = `rgba(${accentRgb}, ${alpha.toFixed(3)})` + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(a.x, a.y) + ctx.lineTo(b.x, b.y) + ctx.stroke() + } + } + } + + // Nodes on top, gently pulsing; every 5th node tinted with the info color + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i] + const pulseAlpha = 0.35 + Math.sin(n.pulse) * 0.25 + const rgb = i % 5 === 0 ? infoRgb : accentRgb + ctx.beginPath() + ctx.fillStyle = `rgba(${rgb}, ${Math.max(0.12, pulseAlpha).toFixed(3)})` + ctx.arc(n.x, n.y, n.r + Math.sin(n.pulse) * 0.4, 0, Math.PI * 2) + ctx.fill() + } + + rafId = window.requestAnimationFrame(step) + } + + function start() { + if (running) return + running = true + rafId = window.requestAnimationFrame(step) + } + + function stop() { + running = false + if (rafId) window.cancelAnimationFrame(rafId) + rafId = 0 + } + + resize() + + if (REDUCED_MOTION) { + // Draw a single static frame — no animation loop. + step() + stop() + window.addEventListener('resize', () => { + resize() + step() + stop() + }) + return + } + + start() + + let resizeTimer = 0 + window.addEventListener('resize', () => { + window.clearTimeout(resizeTimer) + resizeTimer = window.setTimeout(resize, 120) + }) + + document.addEventListener('visibilitychange', () => { + if (document.hidden) stop() + else start() + }) + + if ('IntersectionObserver' in window) { + const io = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting && !document.hidden) start() + else stop() + } + }, + { threshold: 0.05 } + ) + io.observe(canvas) + } + + window.addEventListener('bridgeswarm:theme', syncColors) +} + +/** + * Install command tabs (macOS/Linux, Windows, from source, …). + * Buttons: [data-install-tab="key"] · Panels: [data-install-panel="key"] + */ +function initInstallTabs() { + const tabs = [...document.querySelectorAll('[data-install-tab]')] + const panels = [...document.querySelectorAll('[data-install-panel]')] + if (!tabs.length || !panels.length) return + + const activate = (key) => { + tabs.forEach((t) => t.classList.toggle('is-active', t.getAttribute('data-install-tab') === key)) + panels.forEach((p) => + p.classList.toggle('is-active', p.getAttribute('data-install-panel') === key) + ) + } + + tabs.forEach((tab) => { + tab.addEventListener('click', () => activate(tab.getAttribute('data-install-tab'))) + }) + + const initial = tabs.find((t) => t.classList.contains('is-active')) || tabs[0] + if (initial) activate(initial.getAttribute('data-install-tab')) +} + +/** + * Count-up animation for hero / stat numbers. + * Usage: 0 + * Triggers once, when the element scrolls into view. + */ +function initCountUp() { + const els = [...document.querySelectorAll('[data-count-to]')] + if (!els.length) return + + const animate = (el) => { + const to = Number(el.getAttribute('data-count-to') || '0') + const suffix = el.getAttribute('data-count-suffix') || '' + const decimals = Number(el.getAttribute('data-count-decimals') || '0') + + if (REDUCED_MOTION || !Number.isFinite(to)) { + el.textContent = `${to.toFixed(decimals)}${suffix}` + return + } + + const duration = 1100 + const start = performance.now() + const from = 0 + + const tick = (now) => { + const t = Math.min(1, (now - start) / duration) + // easeOutCubic + const eased = 1 - Math.pow(1 - t, 3) + const value = from + (to - from) * eased + el.textContent = `${value.toFixed(decimals)}${suffix}` + if (t < 1) window.requestAnimationFrame(tick) + } + window.requestAnimationFrame(tick) + } + + if (!('IntersectionObserver' in window)) { + els.forEach(animate) + return + } + + const io = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + animate(entry.target) + io.unobserve(entry.target) + } + } + }, + { threshold: 0.5 } + ) + els.forEach((el) => io.observe(el)) +} + +function init() { + initHeroMesh() + initInstallTabs() + initCountUp() +} + +export { init } + +document.addEventListener('DOMContentLoaded', init) diff --git a/src/js/releases.js b/src/js/releases.js new file mode 100644 index 0000000..3c2c165 --- /dev/null +++ b/src/js/releases.js @@ -0,0 +1,128 @@ +// Fetches release metadata for the /releases.html page and renders release +// cards into [data-release-list]. Talks to a same-origin /api/releases +// endpoint (a thin proxy in front of the Gitea releases API) so the page +// never needs a Gitea token in the browser. +// +// Expected response shape (array, newest first): +// [{ +// tag_name: 'latest-main' | 'v1.0.0', +// name: 'latest-main' | 'v1.0.0', +// prerelease: boolean, +// published_at: '2026-07-20T12:00:00Z', +// html_url: 'https://git.ssh.surf/snxraven/BridgeSwarm/releases/tag/...', +// body: 'markdown changelog text', +// assets: [{ name, browser_download_url, size }] +// }] + +const REPO_RELEASES_URL = 'https://git.ssh.surf/snxraven/BridgeSwarm/releases'; +const API_ENDPOINT = '/api/releases'; + +function formatDate(iso) { + if (!iso) return ''; + try { + return new Date(iso).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + } catch (err) { + return iso; + } +} + +function formatBytes(bytes) { + if (!Number.isFinite(bytes) || bytes <= 0) return ''; + const units = ['B', 'KB', 'MB', 'GB']; + let value = bytes; + let unit = 0; + while (value >= 1024 && unit < units.length - 1) { + value /= 1024; + unit += 1; + } + return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`; +} + +function assetIcon(name) { + if (/\.xpi$/i.test(name)) return 'Firefox'; + if (/\.zip$/i.test(name)) return 'Zip'; + if (/win32|\.exe$/i.test(name)) return 'Windows'; + if (/darwin/i.test(name)) return 'macOS'; + if (/linux/i.test(name)) return 'Linux'; + return 'File'; +} + +function releaseCardHTML(release) { + const tag = release.tag_name || release.name || 'unknown'; + const isRolling = tag === 'latest-main'; + const title = isRolling ? 'latest-main (rolling)' : tag; + const dateLabel = formatDate(release.published_at || release.created_at); + const assets = Array.isArray(release.assets) ? release.assets : []; + + const assetsHTML = assets.length + ? assets + .map( + (asset) => ` + + ${assetIcon(asset.name)} + ${asset.name} + ${asset.size ? `${formatBytes(asset.size)}` : ''} + ` + ) + .join('') + : '

No downloadable assets on this release.

'; + + return ` +
+
+

${title}

+ ${isRolling ? 'rolling' : 'stable'} +
+ ${dateLabel ? `

${dateLabel}

` : ''} +
${assetsHTML}
+ View on Gitea → +
`; +} + +function renderError(container) { + container.innerHTML = ` +
+

Couldn't load releases from the API.

+

Go straight to the source instead:

+ Open releases on Gitea → +
`; +} + +function renderEmpty(container) { + container.innerHTML = ` +
+

No releases published yet.

+ Open releases on Gitea → +
`; +} + +async function loadReleases() { + const container = document.querySelector('[data-release-list]'); + if (!container) return; + + try { + const res = await fetch(API_ENDPOINT, { headers: { Accept: 'application/json' } }); + if (!res.ok) throw new Error(`releases API responded ${res.status}`); + const releases = await res.json(); + + if (!Array.isArray(releases) || releases.length === 0) { + renderEmpty(container); + return; + } + + container.innerHTML = releases.map(releaseCardHTML).join(''); + } catch (err) { + console.warn('[bridgeswarm] failed to load /api/releases:', err); + renderError(container); + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', loadReleases); +} else { + loadReleases(); +} diff --git a/src/js/site.js b/src/js/site.js new file mode 100644 index 0000000..d8f20a5 --- /dev/null +++ b/src/js/site.js @@ -0,0 +1,751 @@ +/** + * BridgeSwarm site: shared chrome, nav, theme, reveal, code copy, mermaid, docs + */ + +const REPO = 'https://git.ssh.surf/snxraven/BridgeSwarm' +const ROLLING_RELEASE = 'https://git.ssh.surf/snxraven/BridgeSwarm/releases/tag/latest-main' +const RELEASES_PAGE = '/releases' +const COMMUNITY_PAGE = '/community' +const USE_CASES_PAGE = '/use-cases/' +const LEARN_PAGE = '/learn/' + +/** localStorage key for site color theme (dark | light) */ +const THEME_STORAGE_KEY = 'bridgeswarm.site.theme' + +/** + * @returns {'dark' | 'light'} + */ +function getStoredTheme() { + try { + const raw = localStorage.getItem(THEME_STORAGE_KEY) + if (raw === 'light' || raw === 'dark') return raw + } catch { + // ignore + } + return 'dark' +} + +/** + * @param {'dark' | 'light'} theme + */ +function applyTheme(theme) { + const t = theme === 'light' ? 'light' : 'dark' + document.documentElement.setAttribute('data-theme', t) + try { + localStorage.setItem(THEME_STORAGE_KEY, t) + } catch { + // ignore + } + // Browser chrome / PWA status bar + const meta = document.querySelector('meta[name="theme-color"]') + if (meta) { + meta.setAttribute('content', t === 'light' ? '#f4f5f7' : '#0c0c10') + } + const tile = document.querySelector('meta[name="msapplication-TileColor"]') + if (tile) { + tile.setAttribute('content', t === 'light' ? '#f4f5f7' : '#141418') + } + // Sync toggle labels + document.querySelectorAll('.theme-toggle').forEach((btn) => { + const next = t === 'light' ? 'dark' : 'light' + btn.setAttribute('aria-label', `Switch to ${next} mode`) + btn.setAttribute('title', `Switch to ${next} mode`) + btn.setAttribute('aria-pressed', t === 'light' ? 'true' : 'false') + }) + // Notify optional listeners (mermaid re-theme, hero canvas, …) + try { + window.dispatchEvent(new CustomEvent('bridgeswarm:theme', { detail: { theme: t } })) + } catch { + // ignore + } +} + +function toggleTheme() { + const cur = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark' + applyTheme(cur === 'light' ? 'dark' : 'light') +} + +function initTheme() { + // FOUC script may have already set data-theme; re-apply for meta + a11y + applyTheme(getStoredTheme()) + document.querySelectorAll('.theme-toggle').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.preventDefault() + toggleTheme() + }) + }) +} + +const THEME_TOGGLE_HTML = ` +` + +function injectChrome() { + const navHost = document.querySelector('[data-nav]') + if (navHost) { + navHost.outerHTML = ` +` + } + + const footHost = document.querySelector('[data-footer]') + if (footHost) { + footHost.outerHTML = ` +` + } + + const sideHost = document.querySelector('[data-docs-nav]') + if (sideHost) { + // Always root-absolute so /docs (no trailing slash) does not break relative links + sideHost.innerHTML = ` + + + BridgeSwarm + Documentation + + +

Guides

+ +

Develop

+ +

Project

+` + } + + const legalHost = document.querySelector('[data-legal-nav]') + if (legalHost) { + legalHost.innerHTML = ` + + + HoneyPeer, LLC + Legal + + +

Agreements

+ +

Privacy & data

+ +

IP & open source

+ +

Risk & safety

+ +

Product

+` + } +} + +function ensureBackdrop(className) { + let el = document.querySelector(`.${className}`) + if (!el) { + el = document.createElement('button') + el.type = 'button' + el.className = className + el.setAttribute('aria-label', 'Close menu') + document.body.appendChild(el) + } + return el +} + +function setNavOpen(nav, toggle, open) { + nav.classList.toggle('open', open) + toggle?.setAttribute('aria-expanded', open ? 'true' : 'false') + document.body.classList.toggle('nav-open', open) + const backdrop = ensureBackdrop('nav-backdrop') + backdrop.classList.toggle('is-visible', open) +} + +function initNav() { + const nav = document.querySelector('.site-nav') + const toggle = document.querySelector('.nav-toggle') + if (!nav) return + + const onScroll = () => { + nav.classList.toggle('scrolled', window.scrollY > 12 || nav.classList.contains('open')) + } + onScroll() + window.addEventListener('scroll', onScroll, { passive: true }) + + const backdrop = ensureBackdrop('nav-backdrop') + backdrop.addEventListener('click', () => setNavOpen(nav, toggle, false)) + + if (toggle) { + toggle.addEventListener('click', () => { + const open = !nav.classList.contains('open') + setNavOpen(nav, toggle, open) + onScroll() + }) + } + + // Close mobile menu when a link is followed + nav.querySelectorAll('a').forEach((a) => { + a.addEventListener('click', () => setNavOpen(nav, toggle, false)) + }) + + window.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && nav.classList.contains('open')) { + setNavOpen(nav, toggle, false) + } + }) + + // Active link by path (+ hash for homepage section links) + const normalize = (p) => { + let s = (p || '/').replace(/\/index\.html$/i, '').replace(/\.html$/i, '') + if (s.length > 1) s = s.replace(/\/$/, '') + return s || '/' + } + + const syncActiveNav = () => { + const path = normalize(location.pathname) + const hash = location.hash || '' + + document.querySelectorAll('.nav-links a, .docs-nav a, .docs-sidebar-brand').forEach((a) => { + a.classList.remove('active') + try { + const url = new URL(a.href, location.origin) + const target = normalize(url.pathname) + const linkHash = url.hash || '' + + // Docs sidebar / brand: exact path only + if (a.closest('.docs-nav') || a.classList.contains('docs-sidebar-brand')) { + if (path === target) a.classList.add('active') + return + } + + // Top nav section links (e.g. /#features): only when hash matches + if (linkHash) { + if (path === target && hash === linkHash) a.classList.add('active') + return + } + + // Full-page links (Docs, Download, Releases, Legal, …) + if (path === target) { + a.classList.add('active') + } + } catch { + // ignore + } + }) + } + + syncActiveNav() + window.addEventListener('hashchange', syncActiveNav) +} + +function initReveal() { + const els = document.querySelectorAll('.reveal') + if (!els.length || !('IntersectionObserver' in window)) { + els.forEach((el) => el.classList.add('visible')) + return + } + const io = new IntersectionObserver( + (entries) => { + for (const e of entries) { + if (e.isIntersecting) { + e.target.classList.add('visible') + io.unobserve(e.target) + } + } + }, + { threshold: 0.12, rootMargin: '0px 0px -40px 0px' } + ) + els.forEach((el) => io.observe(el)) +} + +function isMermaidBlock(el) { + if (!el) return false + if (el.classList?.contains('mermaid')) return true + if (el.closest?.('.mermaid-wrap, .mermaid')) return true + const code = el.tagName === 'CODE' ? el : el.querySelector?.('code') + if (code?.classList?.contains('language-mermaid') || code?.classList?.contains('mermaid')) return true + return false +} + +/** + * Collect mermaid diagram sources from the page and normalize to .mermaid nodes. + * Supports: + *
+ *
+ *
+ */ +function collectMermaidNodes() { + /** @type {HTMLElement[]} */ + const targets = [] + + document.querySelectorAll('pre > code.language-mermaid, pre > code.mermaid').forEach((code) => { + const pre = code.parentElement + if (!pre || pre.tagName !== 'PRE') return + const div = document.createElement('div') + div.className = 'mermaid' + div.textContent = code.textContent || '' + const wrap = document.createElement('div') + wrap.className = 'mermaid-wrap' + wrap.setAttribute('role', 'img') + wrap.setAttribute('aria-label', 'Diagram') + pre.replaceWith(wrap) + wrap.appendChild(div) + targets.push(div) + }) + + document.querySelectorAll('pre.mermaid').forEach((pre) => { + const div = document.createElement('div') + div.className = 'mermaid' + div.textContent = pre.textContent || '' + const wrap = document.createElement('div') + wrap.className = 'mermaid-wrap' + wrap.setAttribute('role', 'img') + wrap.setAttribute('aria-label', 'Diagram') + pre.replaceWith(wrap) + wrap.appendChild(div) + targets.push(div) + }) + + document.querySelectorAll('div.mermaid').forEach((div) => { + if (div.closest('.mermaid-wrap')) { + if (!targets.includes(div)) targets.push(div) + return + } + if (!div.parentElement?.classList.contains('mermaid-wrap')) { + const wrap = document.createElement('div') + wrap.className = 'mermaid-wrap' + wrap.setAttribute('role', 'img') + wrap.setAttribute('aria-label', 'Diagram') + div.parentNode.insertBefore(wrap, div) + wrap.appendChild(div) + } + if (!targets.includes(div)) targets.push(div) + }) + + return targets +} + +function mermaidThemeVariables(isLight) { + if (isLight) { + return { + darkMode: false, + background: '#ffffff', + primaryColor: '#ccfbf1', + primaryTextColor: '#101014', + primaryBorderColor: '#0f766e', + secondaryColor: '#f1f1f4', + secondaryTextColor: '#35353f', + secondaryBorderColor: '#d8d8de', + tertiaryColor: '#e9eaee', + tertiaryTextColor: '#35353f', + tertiaryBorderColor: '#0f766e55', + lineColor: '#0d9488', + textColor: '#101014', + mainBkg: '#f7f8fa', + nodeBorder: '#0f766e', + clusterBkg: '#f1f1f4', + clusterBorder: '#0f766e55', + titleColor: '#101014', + edgeLabelBackground: '#ffffff', + actorBkg: '#f7f8fa', + actorBorder: '#0f766e', + actorTextColor: '#101014', + signalColor: '#0d9488', + signalTextColor: '#101014', + labelBoxBkgColor: '#f7f8fa', + labelBoxBorderColor: '#0f766e', + labelTextColor: '#101014', + loopTextColor: '#35353f', + noteBkgColor: '#ccfbf1', + noteTextColor: '#101014', + noteBorderColor: '#0f766e', + activationBkgColor: '#99f6e4', + sequenceNumberColor: '#04211e', + } + } + return { + darkMode: true, + background: '#141418', + primaryColor: '#134e4a', + primaryTextColor: '#ececf1', + primaryBorderColor: '#2dd4bf', + secondaryColor: '#1e1e25', + secondaryTextColor: '#c7c7d1', + secondaryBorderColor: '#33333c', + tertiaryColor: '#1a1a20', + tertiaryTextColor: '#c7c7d1', + tertiaryBorderColor: '#2dd4bf55', + lineColor: '#5eead4', + textColor: '#ececf1', + mainBkg: '#1e1e25', + nodeBorder: '#2dd4bf', + clusterBkg: '#141418', + clusterBorder: '#2dd4bf55', + titleColor: '#ececf1', + edgeLabelBackground: '#141418', + actorBkg: '#1e1e25', + actorBorder: '#2dd4bf', + actorTextColor: '#ececf1', + signalColor: '#5eead4', + signalTextColor: '#ececf1', + labelBoxBkgColor: '#1e1e25', + labelBoxBorderColor: '#2dd4bf', + labelTextColor: '#ececf1', + loopTextColor: '#c7c7d1', + noteBkgColor: '#134e4a', + noteTextColor: '#ececf1', + noteBorderColor: '#2dd4bf', + activationBkgColor: '#0f766e', + sequenceNumberColor: '#04211e', + } +} + +/** + * Lazy-load mermaid and render diagrams (theme matched to site dark/light). + */ +async function initMermaid() { + const nodes = collectMermaidNodes() + if (!nodes.length) return + + const isLight = document.documentElement.getAttribute('data-theme') === 'light' + + try { + const { default: mermaid } = await import('mermaid') + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'strict', + theme: isLight ? 'base' : 'dark', + fontFamily: 'DM Sans, system-ui, sans-serif', + themeVariables: mermaidThemeVariables(isLight), + flowchart: { + curve: 'basis', + padding: 16, + htmlLabels: true, + useMaxWidth: true, + }, + sequence: { + useMaxWidth: true, + mirrorActors: false, + bottomMarginAdj: 8, + }, + }) + + // Stash diagram source so theme switches can re-render + nodes.forEach((n) => { + if (!n.getAttribute('data-mermaid-src')) { + n.setAttribute('data-mermaid-src', n.textContent || '') + } + }) + + await mermaid.run({ nodes, suppressErrors: false }) + nodes.forEach((n) => { + const wrap = n.closest('.mermaid-wrap') + if (wrap) wrap.classList.add('mermaid-wrap--ready') + }) + } catch (err) { + console.warn('[bridgeswarm] mermaid render failed', err) + nodes.forEach((n) => { + const wrap = n.closest('.mermaid-wrap') || n + wrap.classList?.add('mermaid-wrap--error') + if (!wrap.querySelector?.('.mermaid-error-msg')) { + const msg = document.createElement('p') + msg.className = 'mermaid-error-msg' + msg.textContent = 'Diagram failed to render. Check Mermaid syntax.' + wrap.appendChild?.(msg) + } + }) + } +} + +/** Soft reload of mermaid when theme flips (preserves source from data attribute). */ +function bindMermaidThemeReload() { + window.addEventListener('bridgeswarm:theme', () => { + document.querySelectorAll('.mermaid-wrap .mermaid, div.mermaid').forEach((node) => { + const src = node.getAttribute('data-mermaid-src') + if (!src || !String(src).trim()) return + node.removeAttribute('data-processed') + node.removeAttribute('data-mermaid-id') + node.innerHTML = '' + node.textContent = src + const wrap = node.closest('.mermaid-wrap') + wrap?.classList.remove('mermaid-wrap--ready', 'mermaid-wrap--error') + wrap?.querySelector('.mermaid-error-msg')?.remove() + }) + initMermaid().catch(() => {}) + }) +} + +function initCodeCopy() { + document.querySelectorAll('pre').forEach((pre) => { + if (pre.closest('.code-block')) return + if (isMermaidBlock(pre)) return + const wrap = document.createElement('div') + wrap.className = 'code-block' + pre.parentNode.insertBefore(wrap, pre) + wrap.appendChild(pre) + + const btn = document.createElement('button') + btn.type = 'button' + btn.className = 'copy-btn' + btn.textContent = 'Copy' + btn.addEventListener('click', async () => { + const text = pre.innerText + try { + await navigator.clipboard.writeText(text) + btn.textContent = 'Copied' + btn.classList.add('copied') + setTimeout(() => { + btn.textContent = 'Copy' + btn.classList.remove('copied') + }, 1600) + } catch { + btn.textContent = 'Failed' + } + }) + wrap.appendChild(btn) + }) +} + +function setDocsSidebarOpen(sidebar, open) { + sidebar.classList.toggle('open', open) + document.body.classList.toggle('docs-sidebar-open', open) + const backdrop = ensureBackdrop('docs-sidebar-backdrop') + backdrop.classList.toggle('is-visible', open) +} + +function initDocsSidebar() { + const sidebar = document.querySelector('.docs-sidebar') + const toggle = document.querySelector('.docs-sidebar-toggle') + if (!sidebar || !toggle) return + + const backdrop = ensureBackdrop('docs-sidebar-backdrop') + backdrop.addEventListener('click', () => setDocsSidebarOpen(sidebar, false)) + + toggle.addEventListener('click', () => { + setDocsSidebarOpen(sidebar, !sidebar.classList.contains('open')) + }) + + sidebar.querySelectorAll('a').forEach((a) => { + a.addEventListener('click', () => setDocsSidebarOpen(sidebar, false)) + }) + + window.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && sidebar.classList.contains('open')) { + setDocsSidebarOpen(sidebar, false) + } + }) +} + +/** Mark active legal / docs nav items (exact path). */ +function initLegalActiveNav() { + const normalize = (p) => { + let s = (p || '/').replace(/\/index\.html$/i, '').replace(/\.html$/i, '') + if (s.length > 1) s = s.replace(/\/$/, '') + return s || '/' + } + const path = normalize(location.pathname) + document.querySelectorAll('.docs-nav a').forEach((a) => { + try { + const target = normalize(new URL(a.href, location.origin).pathname) + if (path === target) a.classList.add('active') + } catch { + // ignore + } + }) +} + +/** + * Docs / legal page polish: body class + breadcrumb. + */ +function initDocsPage() { + const main = document.querySelector('.docs-main') + const layout = document.querySelector('.docs-layout') + if (!main || !layout) return + + document.body.classList.add('is-docs') + const isLegal = Boolean(layout.classList.contains('legal-layout') || location.pathname.includes('/legal')) + if (isLegal) document.body.classList.add('is-legal') + + if (main.querySelector('.docs-breadcrumb')) return + + const h1 = main.querySelector(':scope > h1') + if (!h1) return + + const title = (h1.textContent || '').trim() || (isLegal ? 'Legal' : 'Docs') + const path = location.pathname.replace(/\/$/, '') || '/' + const isOverview = isLegal + ? /\/legal\/?(index\.html)?$/.test(path) || title === 'Legal' + : /\/docs\/?(index\.html)?$/.test(path) || title === 'Documentation' + + const crumb = document.createElement('nav') + crumb.className = 'docs-breadcrumb' + crumb.setAttribute('aria-label', 'Breadcrumb') + + const escape = (s) => + String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + + if (isLegal) { + if (isOverview) { + crumb.innerHTML = `Legal` + } else { + crumb.innerHTML = ` + Legal + + ${escape(title)}` + } + } else if (isOverview) { + crumb.innerHTML = `Documentation` + } else { + crumb.innerHTML = ` + Docs + + ${escape(title)}` + } + + main.insertBefore(crumb, main.firstChild) +} + +function setYear() { + document.querySelectorAll('[data-year]').forEach((el) => { + el.textContent = String(new Date().getFullYear()) + }) +} + +function init() { + injectChrome() + initTheme() + initNav() + initReveal() + // Mermaid before code-copy so diagram blocks are not wrapped as copyable code + initMermaid() + bindMermaidThemeReload() + initCodeCopy() + initDocsSidebar() + initDocsPage() + initLegalActiveNav() + setYear() +} + +export { init, THEME_STORAGE_KEY } + +document.addEventListener('DOMContentLoaded', init) diff --git a/src/styles/community.css b/src/styles/community.css new file mode 100644 index 0000000..1fa7a02 --- /dev/null +++ b/src/styles/community.css @@ -0,0 +1,21 @@ +/* Download page */ +.install-tabs { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 1rem 0 0.75rem; } +.install-tab { + border: 1px solid var(--border); background: var(--bg-elevated); color: var(--text-muted); + border-radius: var(--radius-full); padding: 0.4rem 0.85rem; font-size: 0.8rem; font-weight: 600; cursor: pointer; +} +.install-tab[aria-selected="true"], .install-tab.active { + background: var(--accent-dim); border-color: var(--accent-border); color: var(--accent); +} +.install-panel[hidden] { display: none !important; } +.download-steps { counter-reset: step; list-style: none; padding: 0; margin: 1.25rem 0; } +.download-steps li { + position: relative; padding: 0.85rem 0 0.85rem 3rem; border-bottom: 1px solid var(--border); +} +.download-steps li::before { + counter-increment: step; content: counter(step); + position: absolute; left: 0; top: 0.85rem; + width: 2rem; height: 2rem; border-radius: 50%; + display: grid; place-items: center; + background: var(--accent-dim); color: var(--accent); font-family: var(--mono); font-weight: 600; font-size: 0.85rem; +} diff --git a/src/styles/docs.css b/src/styles/docs.css new file mode 100644 index 0000000..29249c8 --- /dev/null +++ b/src/styles/docs.css @@ -0,0 +1,1109 @@ +/* ═══════════════════════════════════════════════════════════ + BridgeSwarm Docs · sidebar + main + pager + prose + tables + Balanced gutters · sticky nav · refined prose · dark chrome + ═══════════════════════════════════════════════════════════ */ + +/* ─── Page shell ─── */ + +body.is-docs { + background: var(--bg-primary); + overflow-x: clip; + overflow-y: visible; +} + +body.is-docs .bg-grid { + opacity: 0.3; +} + +.docs-layout { + --docs-side: 260px; + --docs-content: min(58rem, calc(100vw - var(--docs-side) - 3rem)); + display: grid; + grid-template-columns: + minmax(0.75rem, 0.55fr) + var(--docs-side) + minmax(0, var(--docs-content)) + minmax(0.75rem, 1.15fr); + grid-template-rows: auto; + column-gap: 0; + min-height: calc(100vh - var(--nav-h)); + margin-top: var(--nav-h); + max-width: 100%; + overflow: visible; + border-top: 1px solid var(--border); +} + +/* ─── Sidebar ─── */ + +.docs-sidebar { + grid-column: 2; + position: sticky; + top: var(--nav-h); + max-height: calc(100vh - var(--nav-h)); + height: auto; + align-self: start; + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior: contain; + padding: 1.5rem 0.85rem 2.5rem 0.35rem; + border-right: 1px solid var(--border); + background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%); + z-index: 2; + scrollbar-width: thin; + scrollbar-color: rgba(45, 212, 191, 0.35) transparent; +} + +.docs-sidebar::-webkit-scrollbar { + width: 6px; +} + +.docs-sidebar::-webkit-scrollbar-thumb { + background: rgba(45, 212, 191, 0.3); + border-radius: 999px; + border: 1px solid transparent; + background-clip: padding-box; +} + +.docs-sidebar::-webkit-scrollbar-thumb:hover { + background: rgba(45, 212, 191, 0.55); + border: 1px solid transparent; + background-clip: padding-box; +} + +.docs-sidebar-brand { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.35rem 0.75rem 1.15rem; + margin-bottom: 0.35rem; + border-bottom: 1px solid var(--border); + text-decoration: none; + color: var(--text); +} + +.docs-sidebar-brand:hover { + color: var(--text); +} + +.docs-sidebar-brand-text { + display: flex; + flex-direction: column; + gap: 0.1rem; + min-width: 0; +} + +.docs-sidebar-brand-text strong { + font-family: var(--font-display); + font-size: 0.94rem; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.docs-sidebar-brand-text span { + font-size: 0.7rem; + color: var(--text-faint); + font-weight: 500; + letter-spacing: 0.02em; +} + +.docs-sidebar-title { + font-family: var(--mono); + font-size: 0.66rem; + font-weight: 650; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); + margin: 1.15rem 0 0.45rem; + padding: 0 0.85rem; +} + +.docs-sidebar-title:first-of-type { + margin-top: 0.85rem; +} + +.docs-nav { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.08rem; + margin: 0 0 0.25rem; + padding: 0 0.35rem; +} + +.docs-nav a { + position: relative; + display: flex; + align-items: center; + gap: 0.55rem; + padding: 0.48rem 0.75rem 0.48rem 0.7rem; + border-radius: 9px; + color: var(--text-muted); + font-size: 0.875rem; + font-weight: 500; + letter-spacing: -0.01em; + transition: color 0.15s, background 0.15s; + border: none; + line-height: 1.3; +} + +.docs-nav a:hover { + color: var(--text); + background: rgba(255, 255, 255, 0.045); +} + +.docs-nav a.active { + color: #5eead4; + background: rgba(45, 212, 191, 0.1); + font-weight: 600; +} + +.docs-nav a.active::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 1.1rem; + border-radius: 0 3px 3px 0; + background: var(--accent); + box-shadow: 0 0 10px rgba(45, 212, 191, 0.45); +} + +.docs-nav a .nav-ico { + width: 1.1rem; + flex-shrink: 0; + opacity: 0.55; + text-align: center; + font-size: 0.78rem; + line-height: 1; + font-family: var(--mono); +} + +.docs-nav a.active .nav-ico { + opacity: 0.95; + color: var(--accent); +} + +/* ─── Main article column ─── */ + +.docs-main { + grid-column: 3; + min-width: 0; + width: 100%; + max-width: none; + box-sizing: border-box; + overflow: visible; + padding: 2.25rem clamp(1.25rem, 2.5vw, 2.5rem) 5.5rem clamp(1.5rem, 2.5vw, 2.75rem); + background: + linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 55%, transparent) 0%, transparent 12rem), + transparent; +} + +.docs-main > * { + width: 100%; + max-width: none; + box-sizing: border-box; +} + +/* Breadcrumb */ +.docs-breadcrumb { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.4rem; + font-size: 0.8rem; + font-weight: 500; + color: var(--text-faint); + margin: 0 0 1.15rem; + letter-spacing: -0.01em; +} + +.docs-breadcrumb a { + color: var(--text-muted); + text-decoration: none; +} + +.docs-breadcrumb a:hover { + color: var(--accent); +} + +.docs-bc-sep { + opacity: 0.45; + user-select: none; +} + +.docs-bc-current { + color: var(--text-secondary); +} + +/* Title block */ +.docs-main > h1 { + font-family: var(--font-display); + font-size: clamp(1.85rem, 3.5vw, 2.4rem); + font-weight: 750; + letter-spacing: -0.03em; + line-height: 1.14; + margin: 0 0 0.75rem; + color: var(--text); +} + +.docs-main > .docs-lead { + font-size: 1.08rem; + line-height: 1.65; + color: var(--text-muted); + margin: 0 0 1.75rem; + padding: 0 0 1.5rem; + border-bottom: 1px solid var(--border); + max-width: none; + font-weight: 400; +} + +/* Section headings */ +.docs-main h2 { + font-size: 1.28rem; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.3; + margin: 2.5rem 0 0.85rem; + padding: 0.15rem 0 0; + color: var(--text); + scroll-margin-top: calc(var(--nav-h) + 1.25rem); + display: flex; + align-items: baseline; + gap: 0.55rem; +} + +.docs-main h2::before { + content: ''; + flex-shrink: 0; + width: 0.35rem; + height: 0.35rem; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.15); + transform: translateY(-0.1em); + opacity: 0.9; +} + +.docs-main h3 { + font-size: 1.05rem; + font-weight: 650; + letter-spacing: -0.015em; + margin: 1.65rem 0 0.55rem; + color: var(--text); + scroll-margin-top: calc(var(--nav-h) + 1.25rem); +} + +/* Body text */ +.docs-main p { + margin: 0 0 1rem; + line-height: 1.72; + font-size: 0.975rem; + color: var(--text-secondary); + max-width: none; +} + +.docs-main strong { + color: var(--text); + font-weight: 600; +} + +.docs-main a:not(.docs-card):not(.docs-pager a) { + color: #5eead4; + text-decoration: underline; + text-decoration-color: rgba(45, 212, 191, 0.35); + text-underline-offset: 0.18em; + transition: text-decoration-color 0.15s, color 0.15s; +} + +.docs-main a:not(.docs-card):not(.docs-pager a):hover { + color: #99f6e4; + text-decoration-color: rgba(94, 234, 212, 0.7); +} + +.docs-main ul, +.docs-main ol { + margin: 0 0 1.15rem; + padding-left: 1.2rem; + color: var(--text-secondary); + max-width: none; + font-size: 0.975rem; +} + +.docs-main li { + margin-bottom: 0.4rem; + line-height: 1.65; + padding-left: 0.15rem; +} + +.docs-main li::marker { + color: rgba(45, 212, 191, 0.75); +} + +.docs-main li p { + margin-bottom: 0.35rem; +} + +/* Inline code */ +.docs-main :not(pre) > code { + font-size: 0.84em; + padding: 0.12em 0.4em; + border-radius: 5px; + background: rgba(45, 212, 191, 0.08); + border: 1px solid rgba(45, 212, 191, 0.18); + color: #99f6e4; + font-weight: 500; +} + +/* Code blocks */ +.docs-main pre, +.docs-main .code-block, +.docs-main .table-wrap, +.docs-main .card, +.docs-main .callout, +.docs-main .install-snippet { + max-width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.docs-main .code-block { + position: relative; + margin: 1rem 0 1.35rem; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--bg-code); + box-shadow: var(--shadow-sm); + overflow: hidden; +} + +.docs-main .code-block::before { + content: ''; + display: block; + height: 2px; + background: linear-gradient(90deg, var(--accent), transparent 70%); + opacity: 0.55; +} + +.docs-main .code-block pre { + margin: 0; + border: none; + border-radius: 0; + background: transparent; + box-shadow: none; + padding: 1rem 1.15rem 1.1rem; + padding-right: 4.75rem; +} + +.docs-main pre { + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1rem 1.15rem; + margin: 1rem 0 1.35rem; + font-size: 0.8125rem; + line-height: 1.65; + color: var(--text-secondary); + white-space: pre-wrap !important; + overflow-x: clip !important; + word-break: break-word !important; + overflow-wrap: anywhere !important; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22); +} + +.docs-main pre code { + white-space: pre-wrap !important; + word-break: break-word !important; + overflow-wrap: anywhere !important; + background: none; + border: none; + padding: 0; + color: inherit; + font-size: inherit; +} + +.docs-main .code-block .copy-btn { + top: 0.7rem; + right: 0.65rem; + font-size: 0.72rem; + padding: 0.3rem 0.6rem; + border-radius: 7px; + background: var(--bg-elevated); + border: 1px solid var(--border); + color: var(--text-muted); +} + +.docs-main .code-block .copy-btn:hover { + color: var(--accent); + border-color: var(--accent-border); +} + +.docs-main .install-snippet { + margin: 1rem 0 1.35rem; +} + +.docs-main .install-snippet pre { + font-size: 0.84rem; + margin: 0; +} + +/* Tables */ +.docs-main .table-wrap { + margin: 1.15rem 0 1.5rem; + border-radius: 12px; + border: 1px solid var(--border); + overflow: hidden; + background: var(--bg-elevated); + box-shadow: var(--shadow-sm); +} + +.docs-main table { + width: 100%; + margin: 0; + border: none; + font-size: 0.9rem; +} + +.docs-main th { + background: var(--bg-tertiary); + color: var(--text-muted); + font-size: 0.72rem; + font-weight: 650; + letter-spacing: 0.05em; + text-transform: uppercase; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); +} + +.docs-main td { + padding: 0.8rem 1rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + background: transparent; + vertical-align: top; +} + +.docs-main tr:last-child td { + border-bottom: none; +} + +.docs-main tr:hover td { + background: rgba(45, 212, 191, 0.04); +} + +/* Callouts */ +.callout { + display: flex; + gap: 0.9rem; + padding: 1rem 1.15rem; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--bg-elevated); + margin: 1.25rem 0 1.5rem; + align-items: flex-start; + box-shadow: var(--shadow-sm); +} + +.callout-icon { + flex-shrink: 0; + width: 1.65rem; + height: 1.65rem; + border-radius: 8px; + display: grid; + place-items: center; + font-size: 0.78rem; + font-weight: 700; + margin-top: 0.05rem; +} + +.callout.info { + border-color: color-mix(in srgb, var(--info) 30%, var(--border)); + background: linear-gradient(135deg, color-mix(in srgb, var(--info) 10%, transparent), var(--bg-elevated)); +} + +.callout.info .callout-icon { + background: color-mix(in srgb, var(--info) 18%, transparent); + color: var(--info); +} + +.callout.warn { + border-color: color-mix(in srgb, var(--warning) 30%, var(--border)); + background: linear-gradient(135deg, color-mix(in srgb, var(--warning) 10%, transparent), var(--bg-elevated)); +} + +.callout.warn .callout-icon { + background: color-mix(in srgb, var(--warning) 16%, transparent); + color: var(--warning); +} + +.callout.success { + border-color: color-mix(in srgb, var(--success) 30%, var(--border)); + background: linear-gradient(135deg, color-mix(in srgb, var(--success) 10%, transparent), var(--bg-elevated)); +} + +.callout.success .callout-icon { + background: color-mix(in srgb, var(--success) 16%, transparent); + color: var(--success); +} + +.callout-body { + font-size: 0.9rem; + line-height: 1.6; + color: var(--text-secondary); + min-width: 0; +} + +.callout-body strong { + color: var(--text); + font-weight: 650; +} + +/* Blockquotes */ +.docs-main blockquote { + margin: 1.25rem 0 1.5rem; + padding: 0.9rem 1.15rem; + border-left: 3px solid var(--accent); + background: rgba(45, 212, 191, 0.06); + border-radius: 0 10px 10px 0; + color: var(--text-secondary); +} + +.docs-main blockquote p:last-child { + margin-bottom: 0; +} + +/* ─── TOC ─── */ + +.toc { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.15rem 1.25rem; + align-items: start; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1rem 1.15rem 1.05rem; + margin: 0 0 2rem; + box-shadow: var(--shadow-sm); +} + +.toc strong { + grid-column: 1 / -1; + display: block; + font-family: var(--mono); + font-size: 0.66rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); + margin: 0 0 0.55rem; + font-weight: 650; +} + +.toc ol { + grid-column: 1 / -1; + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 0.15rem; + font-size: 0.875rem; + max-width: none; + counter-reset: toc; +} + +.toc li { + margin: 0; + padding: 0; + counter-increment: toc; +} + +.toc li::marker { + content: none; +} + +.toc a { + display: flex; + align-items: baseline; + gap: 0.65rem; + padding: 0.4rem 0.55rem; + margin: 0 -0.55rem; + border-radius: 8px; + color: var(--text-secondary); + text-decoration: none !important; + transition: background 0.15s, color 0.15s; +} + +.toc a::before { + content: counter(toc, decimal-leading-zero); + font-family: var(--mono); + font-size: 0.72rem; + font-weight: 600; + color: var(--text-faint); + min-width: 1.4rem; + letter-spacing: 0.02em; +} + +.toc a:hover { + color: var(--accent) !important; + background: rgba(45, 212, 191, 0.07); + text-decoration: none !important; +} + +/* ─── Docs home / hub cards ─── */ + +.docs-cards { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; + margin: 0.5rem 0 1.75rem; + width: 100%; +} + +.docs-cards > .docs-card:last-child:nth-child(odd) { + grid-column: 1 / -1; + width: 100%; + max-width: calc((100% - 0.85rem) / 2); + justify-self: center; +} + +.docs-card { + display: flex; + flex-direction: column; + padding: 1.2rem 1.25rem 1.25rem; + border-radius: 14px; + border: 1px solid var(--border); + background: var(--bg-card); + box-shadow: var(--shadow-sm); + color: inherit; + text-decoration: none !important; + transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; + min-width: 0; + position: relative; + overflow: hidden; +} + +.docs-card::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(ellipse 80% 60% at 100% 0%, rgba(45, 212, 191, 0.08), transparent 55%); + opacity: 0; + transition: opacity 0.25s; + pointer-events: none; +} + +.docs-card:hover { + border-color: var(--accent-border); + transform: translateY(-2px); + box-shadow: var(--shadow-md), 0 0 0 1px var(--accent-dim); + color: inherit; + text-decoration: none !important; +} + +.docs-card:hover::after { + opacity: 1; +} + +.docs-card .card-tag { + display: inline-flex; + align-self: flex-start; + font-family: var(--mono); + font-size: 0.63rem; + font-weight: 650; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent); + background: var(--accent-dim); + border: 1px solid var(--accent-border); + border-radius: 999px; + padding: 0.2rem 0.55rem; + margin-bottom: 0.65rem; +} + +.docs-card h3 { + font-size: 1.02rem; + font-weight: 650; + margin: 0 0 0.4rem; + color: var(--text); + letter-spacing: -0.02em; +} + +.docs-card p { + font-size: 0.875rem; + line-height: 1.55; + color: var(--text-muted); + margin: 0; + flex: 1; +} + +/* ─── Pager ─── */ + +.docs-pager { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.85rem; + margin-top: 3rem; + padding-top: 1.75rem; + border-top: 1px solid var(--border); + width: 100%; +} + +.docs-pager > span:empty { + display: none; +} + +.docs-pager:has(> span:empty) { + grid-template-columns: 1fr; +} + +.docs-pager:has(> span:empty) a.next { + justify-self: end; + max-width: min(20rem, 100%); + width: 100%; +} + +.docs-pager a { + display: flex; + flex-direction: column; + gap: 0.3rem; + padding: 1rem 1.15rem; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--bg-elevated); + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 500; + letter-spacing: 0.02em; + text-decoration: none !important; + max-width: none; + flex: none; + box-shadow: var(--shadow-sm); + transition: border-color 0.2s, background 0.2s, transform 0.2s; +} + +.docs-pager a:hover { + border-color: var(--accent-border); + background: var(--accent-dim); + color: var(--accent); + transform: translateY(-1px); + text-decoration: none !important; +} + +.docs-pager a strong { + color: var(--text); + font-size: 0.98rem; + font-weight: 650; + letter-spacing: -0.02em; +} + +.docs-pager a:hover strong { + color: var(--link-hover); +} + +.docs-pager a.next { + margin-left: 0; + text-align: right; + align-items: flex-end; +} + +/* ─── Shared wide-container helper (download / community / install pages) ─── */ + +.container.install-wide { + max-width: min(900px, 100% - 2rem) !important; + width: min(900px, 100% - 2rem); + box-sizing: border-box; +} + +.container.install-wide .card { + max-width: 100% !important; + width: 100%; + box-sizing: border-box; + overflow-x: clip; + min-width: 0; +} + +.container.install-wide pre, +.container.install-wide .code-block { + overflow-x: clip !important; + white-space: pre-wrap !important; + word-break: break-word !important; + overflow-wrap: anywhere !important; + max-width: 100%; + min-width: 0; + font-size: clamp(0.78rem, 1.3vw, 0.88rem); +} + +.page-hero { + padding: calc(var(--nav-h) + 4rem) 0 3rem; + text-align: center; + max-width: 100%; + overflow-x: hidden; +} + +.page-hero h1 { + margin-bottom: 0.85rem; +} + +.page-hero .lead { + margin: 0 auto 2rem; + max-width: min(62ch, 100%); +} + +/* ─── Mobile / tablet ─── */ + +.docs-sidebar-toggle { + display: none; + position: fixed; + bottom: 1.25rem; + right: 1.25rem; + z-index: 50; + width: 52px; + height: 52px; + border-radius: 50%; + border: 1px solid var(--accent-border); + background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); + color: #04211e; + font-size: 1.25rem; + font-weight: 700; + box-shadow: 0 8px 28px rgba(45, 212, 191, 0.35); + cursor: pointer; +} + +@media (max-width: 1100px) { + .docs-layout { + --docs-side: 230px; + --docs-content: min(48rem, calc(100vw - var(--docs-side) - 2rem)); + } + + .docs-main { + padding-left: 1.5rem; + padding-right: 1.5rem; + } +} + +@media (max-width: 900px) { + .docs-layout { + display: block; + min-height: calc(100vh - var(--nav-h)); + overflow: visible; + --docs-content: 100%; + } + + .docs-sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: min(300px, 86vw); + height: 100%; + max-height: none; + z-index: 950; + transform: translateX(-105%); + transition: transform 0.3s var(--ease); + box-shadow: var(--shadow-lg); + padding: calc(var(--nav-h) + 0.75rem) 0.75rem max(2rem, env(safe-area-inset-bottom)); + padding-left: max(0.75rem, env(safe-area-inset-left)); + border-right: 1px solid var(--border); + overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + } + + .docs-sidebar.open { + transform: translateX(0); + } + + .docs-sidebar-toggle { + display: grid; + place-items: center; + bottom: max(1.25rem, env(safe-area-inset-bottom)); + right: max(1.25rem, env(safe-area-inset-right)); + min-width: 52px; + min-height: 52px; + touch-action: manipulation; + } + + .docs-main { + display: block; + width: 100%; + max-width: 100%; + padding: 1.25rem max(1rem, env(safe-area-inset-right)) 6rem max(1rem, env(safe-area-inset-left)); + background: transparent; + } + + .docs-main > h1 { + font-size: clamp(1.65rem, 7vw, 2.1rem); + } + + .docs-main > .docs-lead { + font-size: 1rem; + } + + .docs-cards { + grid-template-columns: 1fr; + } + + .docs-cards > .docs-card:last-child:nth-child(odd) { + max-width: none; + } + + .docs-card { + padding: 1.15rem 1.1rem; + } + + .docs-pager { + grid-template-columns: 1fr; + } + + .docs-pager:has(> span:empty) a.next { + justify-self: stretch; + max-width: 100%; + } + + .docs-pager a { + min-height: 48px; + } + + .docs-pager a.next { + align-items: flex-start; + text-align: left; + } + + .docs-main .table-wrap { + margin-inline: 0; + border-radius: 10px; + } + + .docs-main table { + min-width: 26rem; + } + + .docs-main th, + .docs-main td { + padding: 0.6rem 0.7rem; + font-size: 0.82rem; + } + + .container.install-wide { + max-width: min(100% - 1.25rem, 900px) !important; + width: min(100% - 1.25rem, 900px) !important; + } + + .page-hero { + padding: calc(var(--nav-h) + 2rem) max(1rem, env(safe-area-inset-right)) 2rem max(1rem, env(safe-area-inset-left)); + } + + .page-hero .lead { + max-width: 100%; + } + + .toc { + padding: 0.95rem 1rem; + } + + .mermaid-wrap { + padding: 0.75rem 0.5rem; + margin-inline: 0; + } + + .docs-breadcrumb { + font-size: 0.75rem; + gap: 0.3rem; + } +} + +@media (min-width: 1400px) { + .docs-layout { + --docs-side: 268px; + --docs-content: min(64rem, calc(100vw - var(--docs-side) - 4rem)); + } + + .docs-main { + padding-left: 2.5rem; + padding-right: 2.75rem; + } +} + +@media (min-width: 1700px) { + .docs-layout { + --docs-content: min(70rem, calc(100vw - var(--docs-side) - 5rem)); + } +} + +/* ─── Mermaid diagrams ─── */ + +.docs-main > .mermaid-wrap, +.legal-main > .mermaid-wrap { + max-width: 100%; +} + +.mermaid-wrap { + margin: 1.25rem 0 1.75rem; + padding: 1.15rem 1rem 1rem; + border-radius: 14px; + border: 1px solid var(--border); + background: + radial-gradient(ellipse 70% 80% at 50% 0%, var(--accent-dim), transparent 55%), + var(--bg-elevated); + box-shadow: var(--shadow-sm); + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-x: contain; + text-align: center; + min-width: 0; + max-width: 100%; + scrollbar-width: thin; + scrollbar-color: rgba(45, 212, 191, 0.35) transparent; +} + +.mermaid-wrap::-webkit-scrollbar { + height: 6px; + width: 6px; +} + +.mermaid-wrap::-webkit-scrollbar-thumb { + background: rgba(45, 212, 191, 0.35); + border-radius: 999px; +} + +.mermaid-wrap--ready { + border-color: rgba(45, 212, 191, 0.18); +} + +.mermaid-wrap--error { + border-color: rgba(248, 113, 113, 0.35); + text-align: left; +} + +.mermaid-wrap .mermaid { + display: flex; + justify-content: center; + margin: 0 auto; + overflow: visible; + background: transparent !important; +} + +.mermaid-wrap .mermaid svg { + max-width: 100%; + height: auto; +} + +.mermaid-wrap .mermaid-error-msg, +.mermaid-error-msg { + margin: 0.5rem 0 0; + font-size: 0.88rem; + color: #fca5a5; +} + +.mermaid-wrap:not(.mermaid-wrap--ready):not(.mermaid-wrap--error) .mermaid { + color: transparent; + min-height: 4rem; +} + +pre:has(> code.language-mermaid), +pre.mermaid { + color: var(--text-muted); + font-size: 0.8rem; +} diff --git a/src/styles/download.css b/src/styles/download.css new file mode 100644 index 0000000..1fa7a02 --- /dev/null +++ b/src/styles/download.css @@ -0,0 +1,21 @@ +/* Download page */ +.install-tabs { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 1rem 0 0.75rem; } +.install-tab { + border: 1px solid var(--border); background: var(--bg-elevated); color: var(--text-muted); + border-radius: var(--radius-full); padding: 0.4rem 0.85rem; font-size: 0.8rem; font-weight: 600; cursor: pointer; +} +.install-tab[aria-selected="true"], .install-tab.active { + background: var(--accent-dim); border-color: var(--accent-border); color: var(--accent); +} +.install-panel[hidden] { display: none !important; } +.download-steps { counter-reset: step; list-style: none; padding: 0; margin: 1.25rem 0; } +.download-steps li { + position: relative; padding: 0.85rem 0 0.85rem 3rem; border-bottom: 1px solid var(--border); +} +.download-steps li::before { + counter-increment: step; content: counter(step); + position: absolute; left: 0; top: 0.85rem; + width: 2rem; height: 2rem; border-radius: 50%; + display: grid; place-items: center; + background: var(--accent-dim); color: var(--accent); font-family: var(--mono); font-weight: 600; font-size: 0.85rem; +} diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..d707397 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,1281 @@ +/* ═══════════════════════════════════════════════════════════ + BridgeSwarm Website · Design System + Brand tokens: #0c0c10 · #141418 · #1a1a20 · accent #2dd4bf · info #60a5fa + Display: Syne · Body: DM Sans · Code: IBM Plex Mono + ═══════════════════════════════════════════════════════════ */ + +:root { + color-scheme: dark; + + --bg-primary: #0c0c10; + --bg-secondary: #141418; + --bg-tertiary: #1a1a20; + --bg-elevated: #1e1e25; + --bg-hover: #24242c; + --bg-glass: rgba(18, 18, 22, 0.78); + --bg-code: #08080b; + --bg-card: linear-gradient(165deg, rgba(30, 30, 37, 0.92) 0%, rgba(15, 15, 19, 0.96) 100%); + + --accent: #2dd4bf; + --accent-secondary: #14b8a6; + --accent-dim: rgba(45, 212, 191, 0.12); + --accent-glow: rgba(45, 212, 191, 0.32); + --accent-border: rgba(45, 212, 191, 0.28); + --accent-on: #04211e; + + --info: #60a5fa; + --info-dim: rgba(96, 165, 250, 0.12); + --info-border: rgba(96, 165, 250, 0.28); + + --success: #4ade80; + --warning: #fbbf24; + --danger: #f87171; + + --text: #ececf1; + --text-secondary: #c7c7d1; + --text-muted: #8b8b9a; + --text-faint: #6c6c7a; + + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.14); + + --radius: 14px; + --radius-sm: 10px; + --radius-lg: 22px; + --radius-full: 999px; + + --font-display: 'Syne', 'DM Sans', system-ui, -apple-system, sans-serif; + --font: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + --mono: 'IBM Plex Mono', 'SF Mono', 'Fira Code', ui-monospace, monospace; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.35); + --shadow-lg: 0 24px 64px rgba(0, 0, 0, 0.45); + --shadow-glow: 0 0 80px rgba(45, 212, 191, 0.14); + + --nav-h: 64px; + --brand-logo: 36px; + --max: 1200px; + --max-wide: 1400px; + --docs-max: 58rem; + --install-max: 720px; + --ease: cubic-bezier(0.22, 1, 0.36, 1); + + --theme-color-meta: #0c0c10; + --link-hover: #5eead4; + + --sb-size: 8px; + --sb-radius: 999px; + --sb-track: transparent; + --sb-thumb: rgba(148, 163, 184, 0.24); + --sb-thumb-hover: rgba(45, 212, 191, 0.55); + --sb-thumb-active: rgba(45, 212, 191, 0.75); +} + +/* ─── Light theme tokens (entire site) ─── */ +html[data-theme='light'] { + color-scheme: light; + + --bg-primary: #f4f5f7; + --bg-secondary: #ffffff; + --bg-tertiary: #e9eaee; + --bg-elevated: #ffffff; + --bg-hover: #e4e6eb; + --bg-glass: rgba(255, 255, 255, 0.86); + --bg-code: #f1f1f4; + --bg-card: linear-gradient(165deg, #ffffff 0%, #f7f8fa 100%); + + --accent: #0f766e; + --accent-secondary: #0d9488; + --accent-dim: rgba(15, 118, 110, 0.1); + --accent-glow: rgba(15, 118, 110, 0.18); + --accent-border: rgba(15, 118, 110, 0.28); + --accent-on: #ffffff; + + --info: #2563eb; + --info-dim: rgba(37, 99, 235, 0.1); + --info-border: rgba(37, 99, 235, 0.25); + + --success: #15803d; + --warning: #b45309; + --danger: #b91c1c; + + --text: #101014; + --text-secondary: #35353f; + --text-muted: #63636f; + --text-faint: #8d8d99; + + --border: rgba(16, 16, 20, 0.1); + --border-strong: rgba(16, 16, 20, 0.16); + + --shadow-sm: 0 1px 2px rgba(16, 16, 20, 0.05); + --shadow-md: 0 8px 24px rgba(16, 16, 20, 0.08); + --shadow-lg: 0 24px 64px rgba(16, 16, 20, 0.12); + --shadow-glow: 0 0 80px rgba(15, 118, 110, 0.1); + + --sb-thumb: rgba(99, 99, 111, 0.3); + --sb-thumb-hover: rgba(15, 118, 110, 0.5); + --sb-thumb-active: rgba(15, 118, 110, 0.68); + + --theme-color-meta: #f4f5f7; + --link-hover: #0d9488; +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + overflow-x: clip; + overflow-y: scroll; + max-width: 100%; + height: 100%; +} + +body { + font-family: var(--font); + font-size: 16px; + line-height: 1.65; + color: var(--text); + background: var(--bg-primary); + min-height: 100%; + overflow-x: clip; + overflow-y: visible; + max-width: 100%; + position: relative; +} + +/* ─── Slim themed scrollbars (site-wide) ─── */ +* { + scrollbar-width: thin; + scrollbar-color: var(--sb-thumb) var(--sb-track); +} + +*::-webkit-scrollbar { + width: var(--sb-size); + height: var(--sb-size); +} + +*::-webkit-scrollbar-track { + background: var(--sb-track); + border-radius: var(--sb-radius); +} + +*::-webkit-scrollbar-thumb { + background: var(--sb-thumb); + border-radius: var(--sb-radius); + border: 2px solid transparent; + background-clip: padding-box; +} + +*::-webkit-scrollbar-thumb:hover { + background: var(--sb-thumb-hover); + border: 2px solid transparent; + background-clip: padding-box; +} + +*::-webkit-scrollbar-thumb:active { + background: var(--sb-thumb-active); + border: 2px solid transparent; + background-clip: padding-box; +} + +*::-webkit-scrollbar-corner { + background: transparent; +} + +.hidden, +[hidden] { + display: none !important; +} + +main, +.site-nav, +.site-footer, +.docs-layout, +.section, +.container, +.container-wide { + max-width: 100%; + min-width: 0; +} + +img { + max-width: 100%; + height: auto; + display: block; +} + +video, +iframe, +svg { + max-width: 100%; +} + +a { + color: var(--accent); + text-decoration: none; + transition: color 0.2s var(--ease); +} + +a:hover { + color: var(--link-hover); +} + +/* ─── Typography ─── */ + +h1, h2, h3, h4 { + font-family: var(--font-display); + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.12; + color: var(--text); +} + +h1 { font-size: clamp(2.5rem, 5.5vw, 3.9rem); font-weight: 750; } +h2 { font-size: clamp(1.8rem, 3.5vw, 2.55rem); } +h3 { font-size: 1.28rem; } +h4 { font-size: 1.05rem; } + +p { color: var(--text-secondary); } + +.lead { + font-size: clamp(1.05rem, 2vw, 1.25rem); + color: var(--text-muted); + line-height: 1.7; + max-width: 52ch; +} + +.eyebrow { + font-family: var(--mono); + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-faint); +} + +.gradient-text { + background: linear-gradient(135deg, #5eead4 0%, var(--accent) 45%, var(--info) 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.accent-text { + color: var(--accent); +} + +/* ─── Layout ─── */ + +.container { + width: min(100% - 2rem, var(--max)); + max-width: 100%; + margin-inline: auto; + padding-inline: 0; + box-sizing: border-box; +} + +.container-wide { + width: min(100% - 2rem, var(--max-wide)); + max-width: 100%; + margin-inline: auto; + box-sizing: border-box; +} + +.section { + padding: 6rem 0; + position: relative; +} + +.section-sm { + padding: 4rem 0; +} + +.section-label { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-family: var(--mono); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 1rem; +} + +.section-label::before { + content: ''; + width: 18px; + height: 2px; + background: var(--accent); + border-radius: 2px; +} + +.section-head { + max-width: 640px; + margin-bottom: 3rem; +} + +.section-head.center { + margin-inline: auto; + text-align: center; +} + +.section-head.center .section-label { + justify-content: center; +} + +/* ─── Buttons ─── */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.55rem; + font-family: var(--font); + font-size: 0.95rem; + font-weight: 600; + letter-spacing: -0.01em; + padding: 0.8rem 1.4rem; + border-radius: var(--radius-full); + border: 1px solid transparent; + cursor: pointer; + transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), + background 0.2s, border-color 0.2s, color 0.2s; + text-decoration: none; + white-space: nowrap; + line-height: 1.2; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn:active { + transform: translateY(0); +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-secondary) 100%); + color: var(--accent-on); + box-shadow: 0 0 0 1px var(--accent-border), 0 8px 28px var(--accent-glow); +} + +.btn-primary:hover { + color: var(--accent-on); + box-shadow: 0 0 0 1px var(--accent-border), 0 12px 36px var(--accent-glow); +} + +.btn-secondary { + background: var(--bg-elevated); + color: var(--text); + border-color: var(--border-strong); +} + +.btn-secondary:hover { + color: var(--text); + border-color: var(--accent-border); + background: var(--bg-hover); +} + +.btn-ghost { + background: transparent; + color: var(--text-secondary); + border-color: var(--border); +} + +.btn-ghost:hover { + color: var(--text); + border-color: var(--border-strong); + background: var(--bg-hover); +} + +.btn-lg { + padding: 1rem 1.75rem; + font-size: 1.02rem; +} + +.btn-sm { + padding: 0.5rem 0.95rem; + font-size: 0.85rem; +} + +/* ─── Cards ─── */ + +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + transition: border-color 0.3s var(--ease), transform 0.3s var(--ease), + box-shadow 0.3s var(--ease); + max-width: 100%; + min-width: 0; + box-sizing: border-box; + overflow-x: clip; +} + +.card:hover { + border-color: var(--accent-border); + transform: translateY(-3px); + box-shadow: var(--shadow-md), var(--shadow-glow); +} + +.card-icon { + width: 44px; + height: 44px; + border-radius: 12px; + display: grid; + place-items: center; + background: var(--accent-dim); + border: 1px solid var(--accent-border); + color: var(--accent); + font-size: 1.15rem; + margin-bottom: 1.1rem; +} + +.card-icon.is-info { + background: var(--info-dim); + border-color: var(--info-border); + color: var(--info); +} + +.card h3 { + margin-bottom: 0.55rem; + font-family: var(--font); + font-weight: 700; +} + +.card p { + font-size: 0.95rem; + color: var(--text-muted); +} + +/* ─── Code ─── */ + +code, kbd { + font-family: var(--mono); + font-size: 0.88em; +} + +:not(pre) > code { + background: var(--accent-dim); + color: var(--accent); + padding: 0.15em 0.45em; + border-radius: 6px; + border: 1px solid var(--accent-border); +} + +pre { + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.15rem 1.25rem; + overflow-x: clip; + overflow-y: hidden; + max-width: 100%; + width: 100%; + min-width: 0; + box-sizing: border-box; + font-family: var(--mono); + font-size: 0.8rem; + line-height: 1.7; + color: var(--text-secondary); + position: relative; + white-space: pre-wrap; + word-break: break-all; + overflow-wrap: anywhere; +} + +pre code { + background: none; + border: none; + padding: 0; + color: inherit; + font-size: inherit; + white-space: pre-wrap; + word-break: break-all; + overflow-wrap: anywhere; + display: block; + max-width: 100%; + min-width: 0; + overflow-x: clip; + box-sizing: border-box; +} + +.code-block { + position: relative; + margin: 1.25rem 0; + max-width: 100%; + min-width: 0; + overflow-x: clip; + overflow-y: visible; + width: 100%; + box-sizing: border-box; +} + +.code-block pre { + margin: 0; + padding-right: 4.5rem; + overflow-x: clip; + white-space: pre-wrap; + word-break: break-all; +} + +.code-block .copy-btn { + position: absolute; + top: 0.65rem; + right: 0.65rem; + z-index: 2; + font-size: 0.75rem; + padding: 0.35rem 0.65rem; + border-radius: 8px; + background: var(--bg-elevated); + border: 1px solid var(--border); + color: var(--text-muted); + cursor: pointer; + font-family: var(--font); + font-weight: 500; + transition: all 0.2s; +} + +.code-block .copy-btn:hover { + color: var(--accent); + border-color: var(--accent-border); +} + +.code-block .copy-btn.copied { + color: var(--success); + border-color: rgba(74, 222, 128, 0.35); +} + +/* ─── Tables ─── */ + +.table-wrap { + overflow-x: clip; + margin: 1.25rem 0; + border-radius: var(--radius); + border: 1px solid var(--border); + max-width: 100%; + width: 100%; + box-sizing: border-box; + min-width: 0; +} + +table { + width: 100%; + max-width: 100%; + border-collapse: collapse; + font-size: 0.92rem; + table-layout: fixed; +} + +th, td { + text-align: left; + padding: 0.85rem 1.1rem; + border-bottom: 1px solid var(--border); + word-break: break-word; + overflow-wrap: anywhere; + vertical-align: top; + min-width: 0; +} + +th code, +td code { + word-break: break-all; + overflow-wrap: anywhere; + white-space: normal; +} + +th { + background: var(--bg-tertiary); + color: var(--text-muted); + font-weight: 600; + font-size: 0.78rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +td { + color: var(--text-secondary); + background: color-mix(in srgb, var(--bg-secondary) 70%, transparent); +} + +tr:last-child td { + border-bottom: none; +} + +tr:hover td { + background: var(--accent-dim); +} + +/* ─── Badge / Chip ─── */ + +.badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 0.3rem 0.65rem; + border-radius: var(--radius-full); + background: var(--accent-dim); + color: var(--accent); + border: 1px solid var(--accent-border); +} + +.badge-muted { + background: rgba(255, 255, 255, 0.05); + color: var(--text-muted); + border-color: var(--border); +} + +.badge-info { + background: var(--info-dim); + color: var(--info); + border-color: var(--info-border); +} + +/* ─── Nav ─── */ + +.site-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + height: var(--nav-h); + display: flex; + align-items: center; + transition: background 0.3s, border-color 0.3s, backdrop-filter 0.3s; + border-bottom: 1px solid transparent; +} + +.site-nav.scrolled { + background: var(--bg-glass); + backdrop-filter: blur(16px) saturate(1.4); + -webkit-backdrop-filter: blur(16px) saturate(1.4); + border-bottom-color: var(--border); +} + +.nav-inner { + width: min(100% - 2rem, var(--max-wide)); + max-width: 100%; + margin-inline: auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + min-width: 0; + height: 100%; +} + +.nav-brand { + display: flex; + align-items: center; + gap: 0.6rem; + color: var(--text); + font-family: var(--font-display); + font-weight: 700; + font-size: 1.12rem; + letter-spacing: -0.02em; + flex-shrink: 0; + min-width: 0; +} + +.nav-brand:hover { + color: var(--text); +} + +.nav-brand-logo { + width: var(--brand-logo); + height: var(--brand-logo); + max-width: var(--brand-logo); + flex-shrink: 0; + display: block; + object-fit: contain; + border-radius: 9px; + transition: transform 0.25s var(--ease); +} + +.nav-brand:hover .nav-brand-logo { + transform: scale(1.06) rotate(-2deg); +} + +.nav-brand-text { + line-height: 1; + white-space: nowrap; +} + +.nav-brand-text .brand-accent { + color: var(--accent); +} + +.nav-links { + display: flex; + align-items: center; + gap: 0.2rem; + list-style: none; + min-width: 0; +} + +.nav-links a { + color: var(--text-muted); + font-size: 0.9rem; + font-weight: 500; + padding: 0.5rem 0.8rem; + border-radius: 10px; + transition: color 0.2s, background 0.2s; +} + +.nav-links a:hover, +.nav-links a.active { + color: var(--text); + background: var(--bg-hover); +} + +.nav-cta { + display: flex; + align-items: center; + gap: 0.6rem; + flex-shrink: 0; +} + +/* Theme switcher (dark ↔ light) */ +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + padding: 0; + border-radius: var(--radius-full); + border: 1px solid var(--border); + background: var(--bg-elevated); + color: var(--text-secondary); + cursor: pointer; + flex-shrink: 0; + transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s; +} + +.theme-toggle:hover { + color: var(--accent); + border-color: var(--accent-border); + background: var(--accent-dim); +} + +.theme-toggle:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.theme-toggle svg { + width: 1.05rem; + height: 1.05rem; + display: block; +} + +.theme-toggle .icon-sun { + display: block; +} +.theme-toggle .icon-moon { + display: none; +} +html[data-theme='light'] .theme-toggle .icon-sun { + display: none; +} +html[data-theme='light'] .theme-toggle .icon-moon { + display: block; +} + +.nav-toggle { + display: none; + width: 42px; + height: 42px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--bg-elevated); + color: var(--text); + cursor: pointer; + place-items: center; + font-size: 1.2rem; +} + +/* ─── Footer ─── */ + +.site-footer { + border-top: 1px solid var(--border); + background: var(--bg-secondary); + padding: 4rem 0 2rem; + margin-top: 2rem; +} + +.footer-grid { + display: grid; + grid-template-columns: 1.4fr repeat(3, 1fr); + gap: 2.5rem; + margin-bottom: 3rem; +} + +.footer-brand { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.footer-brand img { + width: 40px; + height: 40px; + border-radius: 10px; +} + +.footer-brand p { + font-size: 0.92rem; + color: var(--text-muted); + max-width: 28ch; +} + +.footer-col h4 { + font-family: var(--font); + font-size: 0.78rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); + margin-bottom: 1rem; +} + +.footer-col a { + display: block; + color: var(--text-muted); + font-size: 0.92rem; + padding: 0.3rem 0; +} + +.footer-col a:hover { + color: var(--accent); +} + +.footer-bottom { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding-top: 1.75rem; + border-top: 1px solid var(--border); + font-size: 0.85rem; + color: var(--text-faint); +} + +.footer-bottom a { + color: var(--text-muted); +} + +.footer-bottom a:hover { + color: var(--accent); +} + +/* ─── Ambient background ─── */ +/* Subtle radial teal/sky wash + a faint geometric (triangular) mesh. + No purple. Intensity kept low so content stays the focus. */ + +.bg-mesh { + position: fixed; + inset: 0; + z-index: -2; + pointer-events: none; + background: + radial-gradient(ellipse 70% 46% at 18% -8%, rgba(45, 212, 191, 0.13), transparent 55%), + radial-gradient(ellipse 46% 42% at 88% 6%, rgba(96, 165, 250, 0.08), transparent 52%), + radial-gradient(ellipse 50% 44% at 8% 66%, rgba(45, 212, 191, 0.05), transparent 55%), + var(--bg-primary); +} + +.bg-grid { + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + opacity: 0.5; + background-image: + linear-gradient(60deg, rgba(255, 255, 255, 0.028) 1px, transparent 1px), + linear-gradient(-60deg, rgba(255, 255, 255, 0.028) 1px, transparent 1px), + linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px); + background-size: 68px 118px, 68px 118px, 68px 118px; + mask-image: radial-gradient(ellipse 75% 65% at 50% 0%, black, transparent); + -webkit-mask-image: radial-gradient(ellipse 75% 65% at 50% 0%, black, transparent); +} + +/* ─── Utilities ─── */ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.flex { display: flex; } +.flex-wrap { flex-wrap: wrap; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-2 { gap: 0.5rem; } +.gap-3 { gap: 0.75rem; } +.gap-4 { gap: 1rem; } +.mt-2 { margin-top: 0.5rem; } +.mt-4 { margin-top: 1rem; } +.mt-6 { margin-top: 1.5rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-6 { margin-bottom: 1.5rem; } +.text-muted { color: var(--text-muted); } +.text-center { text-align: center; } + +/* ─── Grids ─── */ + +.grid { + display: grid; + gap: 1.25rem; +} + +.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } + +.grid > * { min-width: 0; } + +/* ─── CTA band (reusable across pages) ─── */ + +.cta-band { + padding: 5rem 0; +} + +.cta-card { + position: relative; + overflow: hidden; + border-radius: calc(var(--radius-lg) + 4px); + border: 1px solid var(--accent-border); + background: + radial-gradient(ellipse 80% 80% at 20% 0%, rgba(45, 212, 191, 0.16), transparent 55%), + radial-gradient(ellipse 60% 60% at 90% 100%, rgba(96, 165, 250, 0.1), transparent 50%), + linear-gradient(165deg, var(--bg-elevated), var(--bg-secondary)); + padding: 3.5rem 3rem; + text-align: center; + box-shadow: var(--shadow-glow); +} + +.cta-card h2 { + margin-bottom: 0.85rem; +} + +.cta-card .lead { + margin: 0 auto 2rem; + max-width: 42ch; +} + +.cta-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.85rem; +} + +/* ─── Reveal animation ─── */ + +.reveal { + opacity: 0; + transform: translateY(22px); + transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); +} + +.reveal.visible { + opacity: 1; + transform: none; +} + +/* ─── Responsive / mobile ─── */ + +.nav-backdrop, +.docs-sidebar-backdrop { + display: none; + position: fixed; + inset: 0; + z-index: 900; + background: rgba(0, 0, 0, 0.55); + -webkit-backdrop-filter: blur(2px); + backdrop-filter: blur(2px); + border: 0; + padding: 0; + cursor: pointer; +} + +.nav-backdrop.is-visible, +.docs-sidebar-backdrop.is-visible { + display: block; +} + +body.nav-open, +body.docs-sidebar-open { + overflow: hidden; + touch-action: none; +} + +@media (max-width: 900px) { + :root { + --nav-h: 60px; + --brand-logo: 30px; + } + + .nav-inner { + width: 100%; + max-width: 100%; + padding: 0.55rem 1rem; + padding-right: max(1rem, env(safe-area-inset-right)); + padding-left: max(1rem, env(safe-area-inset-left)); + flex-wrap: wrap; + row-gap: 0; + box-sizing: border-box; + } + + .site-nav { + height: auto; + min-height: var(--nav-h); + padding-top: env(safe-area-inset-top, 0); + } + + .nav-brand { + order: 1; + font-size: 1.05rem; + } + + .nav-toggle { + display: grid; + order: 2; + margin-left: auto; + min-width: 44px; + min-height: 44px; + touch-action: manipulation; + } + + .nav-links, + .nav-cta { + display: none; + } + + .site-nav.open { + background: var(--bg-secondary); + border-bottom-color: var(--border); + max-height: min(100dvh, 100vh); + overflow-y: auto; + overscroll-behavior: contain; + } + + .site-nav.open .nav-links { + display: flex; + flex-direction: column; + order: 3; + width: 100%; + flex-basis: 100%; + gap: 0.15rem; + padding: 0.75rem 0 0.35rem; + border-top: 1px solid var(--border); + margin-top: 0.5rem; + list-style: none; + } + + .site-nav.open .nav-links a { + display: flex; + align-items: center; + min-height: 48px; + padding: 0.75rem 0.85rem; + font-size: 1rem; + border-radius: 12px; + } + + .site-nav.open .nav-cta { + display: flex; + flex-direction: column; + order: 4; + width: 100%; + flex-basis: 100%; + gap: 0.65rem; + padding: 0.5rem 0 0.85rem; + } + + .site-nav.open .nav-cta .btn { + width: 100%; + max-width: 100%; + white-space: normal; + min-height: 48px; + justify-content: center; + } + + .footer-grid { + grid-template-columns: 1fr 1fr; + gap: 1.75rem 1.25rem; + } + + .section { + padding: 3.5rem 0; + } + + .card { + padding: 1.25rem; + } + + pre { + font-size: 0.76rem; + padding: 0.9rem 0.95rem; + } + + .code-block pre { + padding-right: 4rem; + } + + .btn { + min-height: 44px; + touch-action: manipulation; + } + + .btn-sm { + min-height: 40px; + } + + .table-wrap { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + margin-inline: 0; + } + + table { + min-width: 28rem; + table-layout: auto; + } + + th, td { + white-space: normal; + word-break: break-word; + } + + h1 { + font-size: clamp(1.9rem, 8vw, 2.6rem); + } + + h2 { + font-size: clamp(1.4rem, 5.5vw, 2rem); + } + + .lead { + font-size: 1.02rem; + } + + .grid-2, + .grid-3, + .grid-4 { + grid-template-columns: 1fr; + } +} + +@media (max-width: 560px) { + .footer-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .footer-col a { + padding: 0.45rem 0; + min-height: 40px; + display: flex; + align-items: center; + } + + .footer-bottom { + flex-direction: column; + align-items: flex-start; + gap: 0.65rem; + text-align: left; + } + + .container, + .container-wide { + width: min(100% - 1.25rem, var(--max)); + } + + .btn { + white-space: normal; + text-align: center; + } + + .btn-lg { + padding: 0.9rem 1.25rem; + width: 100%; + max-width: 100%; + } + + .cta-card { + padding: 1.75rem 1.15rem; + } + + .cta-actions { + flex-direction: column; + align-items: stretch; + } + + .cta-actions .btn { + width: 100%; + } + + .site-footer { + padding-bottom: max(2rem, env(safe-area-inset-bottom)); + } + + body { + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); + } +} + +@media (max-width: 380px) { + .nav-brand { + font-size: 1rem; + gap: 0.4rem; + } + + .container, + .container-wide { + width: min(100% - 1rem, var(--max)); + } +} + +/* ─── Hard ban: no horizontal scrollbars on code / install blocks ─── */ +pre, +pre code, +.code-block, +.install-card, +.table-wrap { + overflow-x: clip !important; + max-width: 100% !important; + min-width: 0 !important; +} + +pre, +pre code { + white-space: pre-wrap !important; + word-break: break-all !important; + overflow-wrap: anywhere !important; +} + diff --git a/src/styles/home.css b/src/styles/home.css new file mode 100644 index 0000000..f78915b --- /dev/null +++ b/src/styles/home.css @@ -0,0 +1,694 @@ +/* ═══════════════════════════════════════════════════════════ + BridgeSwarm · Home page + Full-bleed hero (brand first, no inset cards) · use-case rails · + feature rows · architecture strip · quickstart · CTA + ═══════════════════════════════════════════════════════════ */ + +/* ─── Hero (full-bleed, brand-first) ─── */ + +.hero { + position: relative; + min-height: 100vh; + display: flex; + align-items: center; + padding: calc(var(--nav-h) + 3rem) 0 4rem; + overflow: hidden; + isolation: isolate; +} + +.hero-canvas { + position: absolute; + inset: 0; + z-index: -1; + width: 100%; + height: 100%; + display: block; + opacity: 0.9; +} + +.hero-veil { + position: absolute; + inset: 0; + z-index: -1; + pointer-events: none; + background: + linear-gradient(180deg, transparent 0%, var(--bg-primary) 92%), + radial-gradient(ellipse 60% 55% at 50% 30%, rgba(45, 212, 191, 0.1), transparent 60%); +} + +.hero-inner { + width: 100%; + max-width: 100%; + text-align: center; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 0.55rem; + padding: 0.42rem 0.9rem 0.42rem 0.5rem; + background: rgba(45, 212, 191, 0.08); + border: 1px solid var(--accent-border); + border-radius: var(--radius-full); + font-family: var(--mono); + font-size: 0.78rem; + font-weight: 500; + letter-spacing: 0.02em; + color: var(--text-secondary); + margin-bottom: 2rem; +} + +.hero-badge .dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 12px var(--accent-glow); + animation: pulse-dot 2.2s ease infinite; +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.55; transform: scale(0.82); } +} + +.hero-brand-lockup { + display: flex; + align-items: center; + justify-content: center; + gap: 1.1rem; + margin-bottom: 1.5rem; +} + +.hero-brand-lockup img { + width: 64px; + height: 64px; + flex-shrink: 0; +} + +.hero h1 { + max-width: 20ch; + margin: 0 auto 1.5rem; +} + +.hero .lead { + max-width: 56ch; + margin: 0 auto 2.5rem; + font-size: clamp(1.05rem, 2vw, 1.3rem); +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.9rem; + margin-bottom: 3.5rem; +} + +.hero-stats { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 2.5rem; + padding-top: 2.25rem; + border-top: 1px solid var(--border); + max-width: 780px; + margin: 0 auto; +} + +.hero-stat { + display: flex; + flex-direction: column; + gap: 0.3rem; + min-width: 7rem; +} + +.hero-stat strong { + font-family: var(--font-display); + font-size: 1.65rem; + font-weight: 750; + letter-spacing: -0.02em; + background: linear-gradient(135deg, var(--text), var(--accent)); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.hero-stat span { + font-size: 0.8rem; + color: var(--text-faint); +} + +.hero-scroll-cue { + position: absolute; + left: 50%; + bottom: 1.75rem; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + color: var(--text-faint); + font-family: var(--mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + opacity: 0.75; +} + +.hero-scroll-cue .cue-line { + width: 1px; + height: 28px; + background: linear-gradient(180deg, var(--accent), transparent); + animation: cue-pulse 2.4s ease infinite; +} + +@keyframes cue-pulse { + 0%, 100% { opacity: 0.25; } + 50% { opacity: 1; } +} + +/* ─── Tech / protocol strip ─── */ + +.tech-strip { + padding: 2rem 0 3rem; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + background: var(--bg-tertiary); +} + +.tech-strip-inner { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 1rem 2.5rem; +} + +.tech-item { + display: flex; + align-items: center; + gap: 0.55rem; + color: var(--text-faint); + font-family: var(--mono); + font-size: 0.85rem; + font-weight: 500; + letter-spacing: -0.01em; +} + +.tech-item span { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + opacity: 0.55; +} + +/* ─── Use-case rail (horizontal scroll) ─── */ + +.usecase-rail-wrap { + position: relative; +} + +.usecase-rail { + display: flex; + gap: 1.1rem; + overflow-x: auto; + overflow-y: visible; + padding: 0.5rem 0.25rem 1.5rem; + margin: 0 -0.25rem; + scroll-snap-type: x proximity; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + scrollbar-color: rgba(45, 212, 191, 0.35) transparent; +} + +.usecase-rail::-webkit-scrollbar { + height: 6px; +} + +.usecase-rail::-webkit-scrollbar-thumb { + background: rgba(45, 212, 191, 0.3); + border-radius: 999px; +} + +.usecase-card { + scroll-snap-align: start; + flex: 0 0 auto; + width: min(320px, 82vw); + padding: 1.6rem 1.5rem; + border-radius: var(--radius-lg); + border: 1px solid var(--border); + background: linear-gradient(165deg, var(--bg-elevated), var(--bg-tertiary)); + transition: border-color 0.3s var(--ease), transform 0.3s var(--ease), box-shadow 0.3s var(--ease); +} + +.usecase-card:hover { + border-color: var(--accent-border); + transform: translateY(-4px); + box-shadow: var(--shadow-md); +} + +.usecase-card .usecase-icon { + width: 42px; + height: 42px; + border-radius: 12px; + display: grid; + place-items: center; + background: var(--accent-dim); + border: 1px solid var(--accent-border); + color: var(--accent); + font-size: 1.1rem; + margin-bottom: 1.1rem; +} + +.usecase-card:nth-child(3n+2) .usecase-icon { + background: var(--info-dim); + border-color: var(--info-border); + color: var(--info); +} + +.usecase-card h3 { + font-size: 1.05rem; + margin-bottom: 0.5rem; +} + +.usecase-card p { + font-size: 0.88rem; + color: var(--text-muted); + line-height: 1.6; +} + +.usecase-card a.usecase-link { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-top: 1rem; + font-size: 0.82rem; + font-weight: 600; + color: var(--accent); +} + +/* ─── Feature rows (alternating) ─── */ + +.feature-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3.5rem; + align-items: center; + margin-top: 4.5rem; +} + +.feature-row:first-of-type { + margin-top: 0; +} + +.feature-row.reverse { + direction: rtl; +} + +.feature-row.reverse > * { + direction: ltr; +} + +.feature-copy .section-label { + margin-bottom: 0.75rem; +} + +.feature-copy h2 { + margin-bottom: 1rem; +} + +.feature-copy .lead { + margin-bottom: 1.5rem; + max-width: none; +} + +.feature-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.feature-list li { + display: flex; + align-items: flex-start; + gap: 0.75rem; + color: var(--text-secondary); + font-size: 0.95rem; +} + +.feature-list li::before { + content: ''; + flex-shrink: 0; + width: 20px; + height: 20px; + margin-top: 2px; + border-radius: 50%; + background: var(--accent-dim); + border: 1px solid var(--accent-border); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%232dd4bf'%3E%3Cpath d='M6.5 11.2 3.3 8l1-1 2.2 2.2L11.7 4l1 1z'/%3E%3C/svg%3E"); + background-size: 12px; + background-repeat: no-repeat; + background-position: center; +} + +.feature-visual { + position: relative; + border-radius: var(--radius-lg); + border: 1px solid var(--border); + background: linear-gradient(165deg, var(--bg-elevated), var(--bg-tertiary)); + padding: 2rem; + min-height: 280px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.feature-visual::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(ellipse 70% 60% at 30% 0%, rgba(45, 212, 191, 0.1), transparent 60%); + pointer-events: none; +} + +.feature-visual pre { + width: 100%; + margin: 0; + position: relative; + z-index: 1; +} + +/* ─── Architecture strip ─── */ + +.arch-section { + background: + radial-gradient(ellipse 60% 50% at 50% 100%, rgba(45, 212, 191, 0.06), transparent), + var(--bg-secondary); + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} + +.arch-diagram { + margin-top: 2.5rem; + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); + gap: 1rem; + align-items: stretch; + max-width: 100%; + min-width: 0; +} + +.arch-diagram > * { + min-width: 0; +} + +.arch-node { + background: linear-gradient(165deg, var(--bg-elevated), var(--bg-tertiary)); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.4rem 1.1rem; + text-align: center; + position: relative; + overflow: hidden; +} + +.arch-node::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--accent), transparent); + opacity: 0.6; +} + +.arch-node h3 { + font-size: 0.98rem; + margin: 0.7rem 0 0.35rem; +} + +.arch-node p { + font-size: 0.8rem; + color: var(--text-muted); +} + +.arch-node .node-icon { + width: 46px; + height: 46px; + margin: 0 auto; + border-radius: 14px; + display: grid; + place-items: center; + background: var(--accent-dim); + border: 1px solid var(--accent-border); + color: var(--accent); + font-size: 1.25rem; +} + +.arch-node.is-info .node-icon { + background: var(--info-dim); + border-color: var(--info-border); + color: var(--info); +} + +.arch-arrow { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: var(--accent); + font-size: 0.66rem; + font-family: var(--mono); + font-weight: 600; + letter-spacing: 0.03em; + text-transform: uppercase; + gap: 0.3rem; + min-width: 40px; + flex-shrink: 0; +} + +.arch-arrow svg { + width: 32px; + opacity: 0.7; +} + +.arch-details { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.25rem; + margin-top: 2.25rem; +} + +.arch-detail { + padding: 1.4rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-elevated); + box-shadow: var(--shadow-sm); +} + +.arch-detail h4 { + color: var(--accent); + margin-bottom: 0.5rem; + font-size: 0.92rem; +} + +.arch-detail p { + font-size: 0.86rem; + color: var(--text-muted); +} + +/* ─── Quickstart install ─── */ + +.section#quickstart > .container { + width: min(100% - 2rem, var(--max, 1200px)); + max-width: 100%; +} + +.install-tabs { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.4rem; + margin: 0 auto 1.5rem; + padding: 0.3rem; + width: fit-content; + max-width: 100%; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 999px; +} + +.install-tab { + appearance: none; + border: 0; + background: transparent; + color: var(--text-muted); + font: inherit; + font-family: var(--mono); + font-size: 0.8rem; + font-weight: 600; + padding: 0.5rem 1rem; + border-radius: 999px; + cursor: pointer; + transition: background 0.2s, color 0.2s, box-shadow 0.2s; +} + +.install-tab:hover { + color: var(--text); + background: var(--bg-hover); +} + +.install-tab.is-active { + color: var(--accent-on); + background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); + box-shadow: 0 4px 16px var(--accent-glow); +} + +.install-card { + max-width: min(var(--install-max, 720px), 100%); + width: 100%; + margin: 0 auto; + box-sizing: border-box; + overflow-x: clip; + min-width: 0; +} + +.install-card .code-block, +.install-card pre { + max-width: 100%; + min-width: 0; + overflow-x: clip !important; + white-space: pre-wrap !important; + word-break: break-word; + overflow-wrap: anywhere; + font-size: clamp(0.78rem, 1.4vw, 0.88rem); +} + +.install-panel { + display: none; +} + +.install-panel.is-active { + display: block; + animation: install-panel-in 0.35s var(--ease); +} + +@keyframes install-panel-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: none; } +} + +.quickstart-steps { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1.1rem; + margin-top: 3rem; +} + +.quickstart-step { + padding: 1.4rem 1.3rem; + border-radius: var(--radius-lg); + border: 1px solid var(--border); + background: linear-gradient(165deg, var(--bg-elevated), var(--bg-tertiary)); +} + +.quickstart-step .step-num { + width: 34px; + height: 34px; + border-radius: 10px; + display: grid; + place-items: center; + font-family: var(--mono); + font-weight: 700; + font-size: 0.85rem; + margin-bottom: 0.85rem; + background: var(--accent-dim); + border: 1px solid var(--accent-border); + color: var(--accent); +} + +.quickstart-step h3 { + font-size: 1rem; + margin-bottom: 0.4rem; +} + +.quickstart-step p { + font-size: 0.86rem; + color: var(--text-muted); +} + +/* ─── Responsive ─── */ + +@media (max-width: 1000px) { + .feature-row, + .feature-row.reverse { + grid-template-columns: 1fr; + direction: ltr; + gap: 2rem; + } + + .arch-diagram { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .arch-arrow { + flex-direction: row; + transform: rotate(90deg); + min-height: 36px; + } + + .arch-details { + grid-template-columns: 1fr 1fr; + } + + .quickstart-steps { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .hero { + padding: calc(var(--nav-h) + 1.75rem) 0 3rem; + min-height: auto; + } + + .hero-brand-lockup img { + width: 48px; + height: 48px; + } + + .hero-stats { + gap: 1.5rem; + } + + .tech-strip-inner { + gap: 0.75rem 1.25rem; + justify-content: flex-start; + } + + .tech-item { + font-size: 0.78rem; + } + + .arch-details { + grid-template-columns: 1fr; + } + + .usecase-card { + width: min(280px, 78vw); + } +} + +@media (prefers-reduced-motion: reduce) { + .hero-badge .dot, + .hero-scroll-cue .cue-line { + animation: none !important; + } +} diff --git a/src/styles/learn.css b/src/styles/learn.css new file mode 100644 index 0000000..a8810e3 --- /dev/null +++ b/src/styles/learn.css @@ -0,0 +1,3 @@ +/* Learn long-form */ +.learn-hero { padding-bottom: 1.5rem; } +.learn-toc { margin: 1.25rem 0 2rem; } diff --git a/src/styles/legal.css b/src/styles/legal.css new file mode 100644 index 0000000..f567e42 --- /dev/null +++ b/src/styles/legal.css @@ -0,0 +1,62 @@ +/* ─── Legal area (extends docs layout) ─── */ + +.legal-kicker { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-family: var(--mono); + font-size: 0.7rem; + font-weight: 650; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent); + margin: 0 0 0.75rem; +} + +.legal-updated { + font-size: 0.88rem; + color: var(--text-faint); + margin: -0.5rem 0 1.5rem; +} + +.legal-main .docs-lead { + margin-bottom: 0.75rem; +} + +.legal-notice { + margin-top: 0.5rem; +} + +.legal-cards { + margin-top: 1.25rem; +} + +/* Entity table: first column as labels */ +.legal-main .table-wrap th[scope='row'] { + width: 38%; + text-transform: none; + letter-spacing: -0.01em; + font-size: 0.88rem; + font-weight: 600; + color: var(--text-muted); + background: var(--bg-tertiary); +} + +.legal-layout .docs-sidebar-brand-text span { + letter-spacing: 0.04em; +} + +/* Compact ordered legal sections */ +.legal-main h2 { + margin-top: 2.25rem; +} + +.legal-main ol > li { + margin-bottom: 0.5rem; +} + +/* Legal hub overview cards keep the same visual weight as docs cards + but read a touch more formal (square tag, no hover lift on tag). */ +.legal-main .docs-card .card-tag { + border-radius: 6px; +} diff --git a/src/styles/releases.css b/src/styles/releases.css new file mode 100644 index 0000000..289054a --- /dev/null +++ b/src/styles/releases.css @@ -0,0 +1,9 @@ +/* Releases page */ +.releases-list { display: flex; flex-direction: column; gap: 1rem; margin-top: 1.25rem; } +.releases-list .card { padding: 1.25rem 1.35rem; } +.releases-list .card h3 { font-family: var(--font-display); font-size: 1.15rem; margin: 0 0 0.35rem; } +.releases-list .card p { color: var(--text-muted); font-size: 0.9rem; margin: 0.35rem 0 0.85rem; } +.releases-meta { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem; } +.releases-assets { display: flex; flex-wrap: wrap; gap: 0.5rem; } +.releases-empty, .releases-error { padding: 2rem 1rem; text-align: center; color: var(--text-muted); } +.releases-loading { color: var(--text-muted); font-family: var(--mono); font-size: 0.85rem; } diff --git a/src/styles/theme-light.css b/src/styles/theme-light.css new file mode 100644 index 0000000..4306a96 --- /dev/null +++ b/src/styles/theme-light.css @@ -0,0 +1,394 @@ +/** + * BridgeSwarm website — light theme surface overrides. + * Applied when html[data-theme="light"]. Tokens live in global.css. + * Covers hard-coded dark gradients / colors that don't route through vars. + */ + +html[data-theme='light'] body { + background: + radial-gradient(1100px 560px at 12% -10%, rgba(15, 118, 110, 0.05), transparent 55%), + radial-gradient(900px 500px at 100% 0%, rgba(37, 99, 235, 0.035), transparent 50%), + var(--bg-primary); + color: var(--text); +} + +html[data-theme='light'] ::selection { + background: rgba(15, 118, 110, 0.16); + color: var(--text); +} + +/* ─── Ambient mesh ─── */ +html[data-theme='light'] .bg-mesh { + background: + radial-gradient(ellipse 70% 46% at 18% -8%, rgba(15, 118, 110, 0.08), transparent 55%), + radial-gradient(ellipse 46% 42% at 88% 6%, rgba(37, 99, 235, 0.05), transparent 52%), + radial-gradient(ellipse 50% 44% at 8% 66%, rgba(15, 118, 110, 0.04), transparent 55%), + var(--bg-primary); +} + +html[data-theme='light'] .bg-grid { + opacity: 0.35; + background-image: + linear-gradient(60deg, rgba(16, 16, 20, 0.035) 1px, transparent 1px), + linear-gradient(-60deg, rgba(16, 16, 20, 0.035) 1px, transparent 1px), + linear-gradient(rgba(16, 16, 20, 0.03) 1px, transparent 1px); +} + +/* ─── Nav ─── */ +html[data-theme='light'] .site-nav.scrolled { + background: #ffffff; + border-bottom-color: var(--border); + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: 0 1px 0 rgba(16, 16, 20, 0.04); +} + +html[data-theme='light'] .nav-links a:hover, +html[data-theme='light'] .nav-links a.active { + background: var(--bg-hover); + color: var(--text); +} + +html[data-theme='light'] .nav-toggle { + background: var(--bg-elevated); + border-color: var(--border); + color: var(--text); +} + +html[data-theme='light'] .site-nav.open, +html[data-theme='light'] body.nav-open .site-nav { + background: var(--bg-secondary); +} + +html[data-theme='light'] .nav-backdrop.is-visible, +html[data-theme='light'] .docs-sidebar-backdrop.is-visible { + background: rgba(16, 16, 20, 0.35); +} + +/* ─── Footer ─── */ +html[data-theme='light'] .site-footer { + background: var(--bg-secondary); + border-top-color: var(--border); +} + +html[data-theme='light'] .badge { + background: var(--accent-dim); + color: var(--accent); + border-color: var(--accent-border); +} + +html[data-theme='light'] .badge-info { + background: var(--info-dim); + color: var(--info); + border-color: var(--info-border); +} + +/* ─── Code / pre ─── */ +html[data-theme='light'] .code-block .copy-btn { + background: var(--bg-elevated); + border-color: var(--border); + color: var(--text-secondary); +} + +html[data-theme='light'] .code-block .copy-btn:hover { + color: var(--accent); + border-color: var(--accent-border); + background: var(--accent-dim); +} + +html[data-theme='light'] .code-block .copy-btn.copied { + color: var(--success); + border-color: rgba(21, 128, 61, 0.35); +} + +html[data-theme='light'] pre, +html[data-theme='light'] .code-block pre, +html[data-theme='light'] .docs-main pre, +html[data-theme='light'] .docs-main .code-block pre, +html[data-theme='light'] .feature-visual pre { + background: #f1f1f4 !important; + border-color: var(--border) !important; + color: #35353f !important; +} + +html[data-theme='light'] :not(pre) > code { + background: rgba(15, 118, 110, 0.08); + border-color: rgba(15, 118, 110, 0.2); + color: #0f766e; +} + +/* ─── Cards ─── */ +html[data-theme='light'] .card, +html[data-theme='light'] .docs-card, +html[data-theme='light'] .usecase-card, +html[data-theme='light'] .arch-node, +html[data-theme='light'] .arch-detail, +html[data-theme='light'] .feature-visual, +html[data-theme='light'] .quickstart-step, +html[data-theme='light'] .install-card { + background: #ffffff !important; + border-color: var(--border) !important; + color: var(--text); + box-shadow: var(--shadow-sm); +} + +html[data-theme='light'] .card:hover, +html[data-theme='light'] .docs-card:hover, +html[data-theme='light'] .usecase-card:hover { + box-shadow: var(--shadow-md); +} + +/* ─── Hero ─── */ +html[data-theme='light'] .hero-veil { + background: + linear-gradient(180deg, transparent 0%, var(--bg-primary) 92%), + radial-gradient(ellipse 60% 55% at 50% 30%, rgba(15, 118, 110, 0.07), transparent 60%); +} + +html[data-theme='light'] .hero-badge { + background: rgba(15, 118, 110, 0.06); + border-color: var(--accent-border); +} + +html[data-theme='light'] .hero-canvas { + opacity: 0.55; + mix-blend-mode: multiply; +} + +/* ─── Tech strip ─── */ +html[data-theme='light'] .tech-strip { + background: #eef0f3 !important; + border-color: var(--border) !important; +} + +html[data-theme='light'] .tech-item { + color: var(--text-muted) !important; +} + +html[data-theme='light'] .tech-item span { + background: var(--accent) !important; + opacity: 0.7; +} + +/* ─── Install tabs ─── */ +html[data-theme='light'] .install-tabs { + background: #eef0f3 !important; + border-color: var(--border) !important; +} + +html[data-theme='light'] .install-tab { + color: var(--text-muted) !important; +} + +html[data-theme='light'] .install-tab:hover { + color: var(--text) !important; + background: rgba(16, 16, 20, 0.05) !important; +} + +html[data-theme='light'] .install-tab.is-active { + background: var(--accent) !important; + color: #ffffff !important; + box-shadow: 0 4px 14px rgba(15, 118, 110, 0.22); +} + +/* ─── CTA band ─── */ +html[data-theme='light'] .cta-card { + background: + radial-gradient(ellipse 80% 80% at 20% 0%, rgba(15, 118, 110, 0.1), transparent 55%), + radial-gradient(ellipse 60% 60% at 90% 100%, rgba(37, 99, 235, 0.06), transparent 50%), + linear-gradient(165deg, #ffffff, #f4f5f7) !important; + border-color: var(--border) !important; + color: var(--text); +} + +/* ─── Docs layout ─── */ +html[data-theme='light'] .docs-sidebar { + background: linear-gradient(180deg, #ffffff 0%, #f7f8fa 100%) !important; + border-color: var(--border) !important; + box-shadow: 1px 0 0 var(--border); +} + +html[data-theme='light'] .docs-sidebar-brand, +html[data-theme='light'] .docs-sidebar-title { + color: var(--text-muted); +} + +html[data-theme='light'] .docs-sidebar-brand strong { + color: var(--text); +} + +html[data-theme='light'] .docs-nav a { + color: var(--text-secondary); +} + +html[data-theme='light'] .docs-nav a:hover, +html[data-theme='light'] .docs-nav a.active { + color: var(--text); + background: var(--accent-dim); +} + +html[data-theme='light'] .docs-nav a.active::before { + background: var(--accent); +} + +html[data-theme='light'] .docs-main { + color: var(--text-secondary); +} + +html[data-theme='light'] .docs-main h1, +html[data-theme='light'] .docs-main h2, +html[data-theme='light'] .docs-main h3, +html[data-theme='light'] .docs-main h4 { + color: var(--text); +} + +html[data-theme='light'] .docs-main a:not(.docs-card):not(.docs-pager a) { + color: #0f766e !important; + text-decoration-color: rgba(15, 118, 110, 0.35) !important; +} + +html[data-theme='light'] .docs-main a:not(.docs-card):not(.docs-pager a):hover { + color: #0d9488 !important; +} + +html[data-theme='light'] .docs-main :not(pre) > code { + background: rgba(15, 118, 110, 0.08) !important; + border-color: rgba(15, 118, 110, 0.18) !important; + color: #0f766e !important; +} + +html[data-theme='light'] .docs-breadcrumb { + color: var(--text-muted); +} + +html[data-theme='light'] .docs-breadcrumb a { + color: var(--accent); +} + +html[data-theme='light'] .docs-sidebar-toggle { + background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); + border-color: var(--accent-border); + color: #ffffff; + box-shadow: var(--shadow-md); +} + +/* Docs tables */ +html[data-theme='light'] .docs-main table, +html[data-theme='light'] .docs-main th, +html[data-theme='light'] .docs-main td { + border-color: var(--border); +} + +html[data-theme='light'] .docs-main th { + background: #f1f1f4; + color: var(--text); +} + +html[data-theme='light'] .docs-main tr:nth-child(even) td { + background: rgba(16, 16, 20, 0.02); +} + +html[data-theme='light'] .docs-main tr:hover td { + background: rgba(15, 118, 110, 0.05); +} + +html[data-theme='light'] .table-wrap, +html[data-theme='light'] table, +html[data-theme='light'] th, +html[data-theme='light'] td { + border-color: var(--border); +} + +html[data-theme='light'] th { + background: var(--bg-tertiary); + color: var(--text); +} + +/* Docs pager */ +html[data-theme='light'] .docs-pager a { + background: #ffffff !important; + border-color: var(--border) !important; + color: var(--text-muted) !important; + box-shadow: var(--shadow-sm); +} + +html[data-theme='light'] .docs-pager a strong { + color: var(--text) !important; +} + +html[data-theme='light'] .docs-pager a:hover { + border-color: rgba(15, 118, 110, 0.35) !important; + background: rgba(15, 118, 110, 0.06) !important; + color: var(--accent) !important; +} + +/* Callouts */ +html[data-theme='light'] .callout { + background: #ffffff !important; + border-color: var(--border) !important; + color: var(--text-secondary); +} + +/* TOC */ +html[data-theme='light'] .toc { + background: #ffffff !important; + border-color: var(--border) !important; +} + +html[data-theme='light'] .toc a:hover { + background: rgba(15, 118, 110, 0.06) !important; +} + +/* Mermaid */ +html[data-theme='light'] .mermaid-wrap { + background: #ffffff !important; + border-color: var(--border) !important; + box-shadow: var(--shadow-sm); +} + +html[data-theme='light'] .mermaid-wrap--error, +html[data-theme='light'] .mermaid-error-msg { + color: var(--danger); +} + +/* Gradient text stays vivid on light */ +html[data-theme='light'] .gradient-text { + background: linear-gradient(135deg, #0d9488 0%, #0f766e 45%, #2563eb 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +html[data-theme='light'] .hero-stat strong { + background: linear-gradient(135deg, var(--text), var(--accent)); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* Form controls if any */ +html[data-theme='light'] input, +html[data-theme='light'] select, +html[data-theme='light'] textarea { + background: var(--bg-elevated); + border-color: var(--border); + color: var(--text); +} + +html[data-theme='light'] hr { + border-color: var(--border); +} + +/* Mobile nav drawer */ +@media (max-width: 900px) { + html[data-theme='light'] .site-nav .nav-links { + background: var(--bg-secondary); + border-color: var(--border); + box-shadow: var(--shadow-lg); + } + + html[data-theme='light'] .site-nav .nav-cta { + background: var(--bg-secondary); + border-color: var(--border); + } +} diff --git a/src/styles/use-cases.css b/src/styles/use-cases.css new file mode 100644 index 0000000..95ed51a --- /dev/null +++ b/src/styles/use-cases.css @@ -0,0 +1,4 @@ +/* Use cases */ +.uc-block { padding: 2.5rem 0; border-top: 1px solid var(--border); } +.uc-block:first-of-type { border-top: 0; } +.uc-meta { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0 1rem; } diff --git a/use-cases/index.html b/use-cases/index.html new file mode 100644 index 0000000..ad78a36 --- /dev/null +++ b/use-cases/index.html @@ -0,0 +1,281 @@ + + + + + + + + +Use Cases — BridgeSwarm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +

What to build with BridgeSwarm

+

Every example below runs on the bundled demos server. Install BridgeSwarm, enable Examples server in Settings, and open http://127.0.0.1:4173/ in two tabs.

+
+
+ + + +
+ +
+
01
+
+

P2P chat rooms

+

A topic is a room. Everyone who joins the same topic string finds each other through Hyperswarm and exchanges messages over direct, Noise-encrypted connections — no chat server, no message broker, no accounts.

+

The chat example is a minimal single-file room: join a topic, broadcast text, render an inbox. chat-advanced builds rooms, presence, markdown rendering, emoji, and peer-to-peer file sharing on the same primitives.

+
const swarm = new BridgeSwarm({ appName: 'chat-demo' });
+await swarm.join('room:general');
+swarm.on('connection', (conn, peerInfo) => {
+  conn.on('data', (data) => render(peerInfo.publicKey, data));
+});
+function broadcast(text) {
+  for (const conn of swarm.connections()) conn.write(text);
+}
+ +
+
+ +
+
02
+
+

Collaborative apps

+

Anything that broadcasts small structured events to everyone in a session — cursors, strokes, edits — maps directly onto swarm connections. The whiteboard example is a shared canvas where every stroke is written to every connected peer as it happens.

+

For richer protocols than "write JSON, parse JSON," attach Protomux to the connection and define named message channels instead of hand-rolling a dispatch table.

+
swarm.on('connection', (conn) => {
+  conn.on('data', (buf) => applyRemoteStroke(JSON.parse(buf)));
+});
+canvas.addEventListener('pointermove', (e) => {
+  const stroke = strokeFrom(e);
+  for (const conn of swarm.connections()) conn.write(JSON.stringify(stroke));
+});
+ +
+
+ +
+
03
+
+

Data sync

+

When you don't want to hand-roll message protocols at all, let the host replicate structured storage directly. swarm.setAutoReplicate({ swarmId, enabled: true }) takes over every new connection on a swarm for Hypercore replication — those sockets stop forwarding raw data to the page and instead keep a Hypercore (or Hyperbee/Hyperdrive built on one) in sync across every peer automatically.

+

The data-demo example exercises Hyperbee, Hyperdrive, and Hyperdb directly through BridgeSwarm.request; sync-demo shows the auto-replicate path end to end.

+
await BridgeSwarm.request('beePut', { key: 'title', value: 'Q3 plan' });
+const doc = await BridgeSwarm.request('beeGet', { key: 'title' });
+
+await swarm.setAutoReplicate({ enabled: true }); // new connections auto-sync
+ +
+
+ +
+
04
+
+

Live media

+

The host's media capability pack wraps bare-ffmpeg for real encode work, including a live session mode: push captured frames in, get VP9/WebM segments out — to a Media Source Extensions player on the page, fanned out to swarm peers, or archived to disk, in any combination.

+
const live = BridgeSwarm.media.liveSession(videoEl);
+await live.start({ width: 640, height: 360, fps: 10, ingest: 'frames' });
+await live.pushFrame(canvas);
+await live.subscribe({ swarm: { connIds: [conn.connId] } }); // fan out to a peer
+await live.stop();
+

See live-encode for the host-encode → MSE pipeline, media-demo for batch probe/transform/transcode, and clip-studio for nearline recording workflows.

+ +
+
+ +
+
05
+
+

Self-hosted web apps

+

The bundled demos server is itself proof of a pattern worth building on: the Bare host can run a small bare-http1 static server on 127.0.0.1 and serve a full HTML/CSS/JS app with no cloud hosting step. Because the content script injects window.BridgeSwarm into pages served from real HTTP origins (not file://), an app served this way gets P2P networking for free — it's a normal page as far as the browser is concerned.

+

This is the same mechanism the extension itself uses: toggle Examples server in Settings and the host starts serving on 127.0.0.1:4173 with no separate deploy step.

+
// extension -> host, on settings change
+examplesServer.start({ host: '127.0.0.1', port: 4173 })
+// host responds
+{ running: true, url: 'http://127.0.0.1:4173/' }
+ +
+
+ +
+
06
+
+

Live topic domains

+

A DNS name is a static pointer maintained by infrastructure you rent. A Hyperswarm topic behaves more like an address you dial directly — whoever is currently announcing it is reachable, for as long as they keep at least one connection to the DHT. Nothing to register, nothing to renew, no hosting bill if nobody's listening.

+

You can lean on this directly: derive a topic from a room name, an invite code, or a content hash, and treat "join this topic" as the entire addressing scheme for a feature. The chat and whiteboard examples both do exactly this — the topic string is the room.

+
const topic = await sha256(`my-app:${roomSlug}`);
+await swarm.join(topic); // this room now "exists" for as long as someone's in it
+ +
+
+ +
+
07
+
+

Firewall-controlled rooms

+

Discovery is open by design — that's the point of a DHT — but connections don't have to be. swarm.setFirewall({ mode, keys }) switches a swarm between off, allowlist, and denylist modes keyed on peer public keys, and swarm.ban(publicKey) / peerInfo.ban() drops a specific peer outright.

+
await swarm.setFirewall({ mode: 'allowlist', keys: [trustedKeyHex] });
+
+swarm.on('connection', (conn, peerInfo) => {
+  if (isSpamming(peerInfo)) peerInfo.ban();
+});
+

The firewall-room example wires this up as a live allowlist/denylist/ban UI you can test against a second tab.

+ +
+
+ +
+
08
+
+

Developer tooling

+

Two building blocks for anyone shipping a protocol on top of BridgeSwarm rather than a demo app: Protomux for hand-designed message channels in the page, and HRPC for schema-defined, streaming RPC on the host.

+
// HRPC: unary + streaming from the page
+await BridgeSwarm.request('attachHrpc', { connId: conn.connId });
+const pong = await swarm.hrpcCall(conn.connId, 'ping', { value: 'hi' });
+await swarm.hrpcCall(conn.connId, 'fetchStream', { count: 3 }, {
+  onChunk: (c) => console.log(c),
+});
+

sdk-demo exercises swarm lifecycle and raw Protomux; hrpc-demo covers unary calls and both streaming directions end to end.

+ +
+
+ +
+ +
+
+
+

Pick one and go build it

+

Every demo above is source-available in the repo's examples/ directory as a starting point.

+ +
+
+
+ +
+ +
+ + + + diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..70b5146 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,76 @@ +import { defineConfig } from 'vite' +import { resolve } from 'path' + +export default defineConfig({ + root: '.', + publicDir: 'public', + build: { + outDir: 'dist', + emptyOutDir: true, + rollupOptions: { + input: { + // ── Marketing ── + main: resolve(__dirname, 'index.html'), + download: resolve(__dirname, 'download.html'), + releases: resolve(__dirname, 'releases.html'), + community: resolve(__dirname, 'community.html'), + learn: resolve(__dirname, 'learn/index.html'), + useCases: resolve(__dirname, 'use-cases/index.html'), + + // ── Docs ── + docs: resolve(__dirname, 'docs/index.html'), + quickstart: resolve(__dirname, 'docs/quickstart.html'), + userGuide: resolve(__dirname, 'docs/user-guide.html'), + developerGuide: resolve(__dirname, 'docs/developer-guide.html'), + architecture: resolve(__dirname, 'docs/architecture.html'), + api: resolve(__dirname, 'docs/api.html'), + data: resolve(__dirname, 'docs/data.html'), + protocols: resolve(__dirname, 'docs/protocols.html'), + capabilities: resolve(__dirname, 'docs/capabilities.html'), + examples: resolve(__dirname, 'docs/examples.html'), + security: resolve(__dirname, 'docs/security.html'), + faq: resolve(__dirname, 'docs/faq.html'), + release: resolve(__dirname, 'docs/release.html'), + + // ── Legal ── + legal: resolve(__dirname, 'legal/index.html'), + legalTerms: resolve(__dirname, 'legal/terms.html'), + legalEula: resolve(__dirname, 'legal/eula.html'), + legalPrivacy: resolve(__dirname, 'legal/privacy.html'), + legalLicense: resolve(__dirname, 'legal/license.html'), + legalAup: resolve(__dirname, 'legal/aup.html'), + legalCookies: resolve(__dirname, 'legal/cookies.html'), + legalCopyright: resolve(__dirname, 'legal/copyright.html'), + legalDisclaimer: resolve(__dirname, 'legal/disclaimer.html'), + legalSecurity: resolve(__dirname, 'legal/security.html'), + }, + }, + }, + server: { + port: 5174, + open: true, + proxy: { + // Mirror production /api/releases → Gitea (see server.mjs) + '/api/releases': { + target: 'https://git.ssh.surf', + changeOrigin: true, + rewrite: (p) => { + const base = '/api/v1/repos/snxraven/BridgeSwarm' + if (p === '/api/releases' || p.startsWith('/api/releases?')) { + return p.replace('/api/releases', `${base}/releases`) + } + if (p.startsWith('/api/releases/tags/')) { + return p.replace('/api/releases/tags/', `${base}/releases/tags/`) + } + if (p.startsWith('/api/releases/latest')) { + return `${base}/releases?limit=50` + } + return p.replace(/^\/api\/releases/, `${base}/releases`) + }, + }, + }, + }, + preview: { + port: 5174, + }, +})