diff --git a/docs/readthedocs/cad.md b/docs/readthedocs/cad.md index 88096a699..25d34ff72 100644 --- a/docs/readthedocs/cad.md +++ b/docs/readthedocs/cad.md @@ -1,6 +1,6 @@ # CAD Files ## [SAVIOUR 3D Printed Cases v1](https://grabcad.com/library/saviour-pi-5-cases-v1-0-0-2) -[![SAVIOUR 3D printed cases v1](images/saviour_cam_cad_sep_2025.jpg)](https://grabcad.com/library/saviour-pi-5-cases-v1-0-0-2) +[![SAVIOUR 3D printed cases v1](images/saviour_cam_cad_sep_2025.webp)](https://grabcad.com/library/saviour-pi-5-cases-v1-0-0-2) These were the earliest deployed versions of the SAVIOUR cases. They come in two halves and screw together using standoffs to get the spacing right from the Pi assembly. They have since been replaced with snap-fit cases that do not require additional standoffs, but they are still functional and very robust. \ No newline at end of file diff --git a/docs/readthedocs/faqs.md b/docs/readthedocs/faqs.md index 537c87638..c64bd8405 100644 --- a/docs/readthedocs/faqs.md +++ b/docs/readthedocs/faqs.md @@ -20,4 +20,19 @@ Many SAVIOUR users also capture electrophysiology data in their experiments. A T Primarily behavioural neuroscience labs, though the same approach suits any experiment needing several sensors recording in sync. ***Why was SAVIOUR made?***
-SAVIOUR was developed in response to many one-off data gathering rigs being created that did similar things in a messy, expensive, and hard to reproduce way. \ No newline at end of file +SAVIOUR was developed in response to many one-off data gathering rigs being created that did similar things in a messy, expensive, and hard to reproduce way. + +***How do I know if PTP sync is good enough to start recording?***
+Click "Check Ready" on the Recording page - it checks ptp4l and phc2sys offset on every module against a threshold (50 µs by default) and reports which modules aren't ready. The System page also shows live per-module PTP offset if you want to watch it settle after a reboot. + +***A camera was just rebooted - can I record straight away?***
+Give it 5–10 minutes first. phc2sys needs that long to converge its frequency estimate for that crystal; recording immediately after a reboot can leave a larger-than-usual (but still bounded) inter-camera phase offset. + +***What happens to a recording if the NAS/export share goes down?***
+Files are staged locally on the module and export is retried with backoff once the share comes back - recordings aren't lost, but they won't appear on the share until the export queue catches up. + +***Can I change which rig UI (basic / loom / apa / habitat / acoustic startle) a controller shows?***
+The frontend variant is selected by which App is imported in `src/controller/frontend/src/main.jsx`. Switching rigs means editing that import and rebuilding the frontend - it isn't a runtime setting yet. + +***Do all modules need to be the same type?***
+No - a system is any mix of camera, microphone, TTL, RFID and rig-specific module types (loom camera, APA camera/arduino) all reporting to one controller. Add or remove modules freely; the controller discovers them automatically over mDNS. \ No newline at end of file diff --git a/docs/readthedocs/getting_started.md b/docs/readthedocs/getting_started.md index 81f390cc9..e527748ff 100644 --- a/docs/readthedocs/getting_started.md +++ b/docs/readthedocs/getting_started.md @@ -1,3 +1,53 @@ # Getting Started -Coming soon! \ No newline at end of file +## Before you start + +This guide assumes: + +- You have the hardware together: a PoE+ switch, one controller, and one or more modules, all on the same network. See [Hardware](hardware.md) for what to buy and how to size your switch. +- Every device already has Raspberry Pi OS flashed to its SD card (or NVMe, for the controller) and boots normally - a plain [Raspberry Pi Imager](https://www.raspberrypi.com/software/) install, nothing SAVIOUR-specific needed at that stage. +- SAVIOUR itself is installed on each device. If it isn't yet, see below. + +### Installing SAVIOUR + +On a fresh Raspberry Pi OS install, run: + +``` +curl -fsSL https://raw.githubusercontent.com/Kind-Wyllie-lab/saviour/main/install.sh | bash +``` + +This clones the repo to `/usr/local/src/saviour` and runs `setup.sh`, which installs everything SAVIOUR needs (PTP, ffmpeg, Picamera2, Samba, etc.). It doesn't assign a role by itself - that's the next step, below. + +Doing this one device at a time works fine but is slow for a big rig - once you've got one device fully configured, it's usually faster to clone its SD card/NVMe image onto the rest instead (`scripts/multiclone.sh` in the repo). + +## Assigning a device role + +1. Boot the device on the PoE network (SAVIOUR should already be installed - see above). +2. Run `sudo saviour-config` on the device. +3. Choose Controller (one per system) or Module, then pick the module type (camera, microphone, TTL, RFID, ...). +4. The device reboots into its assigned role and appears automatically once discovered. + +## Connecting modules to the controller + +1. Power on the controller first - it acts as the PTP grandmaster and service discovery hub. +2. Power on modules; they register over mDNS and appear on the Dashboard within a few seconds. +3. Check the System page to confirm every module shows a recent heartbeat and a locked PTP offset before recording. + +## Running a recording session + +1. Configure each module on the Settings page (resolution, sample rate, etc.) before starting. +2. On the Recording page, click "Check Ready" to confirm PTP sync is within threshold on every module. +3. Start the session, and stop it (or let a scheduled window end it) once you're done. +4. Recordings export automatically to the controller's share once each module finishes. + +## Exporting and retrieving data + +1. Recordings land on the controller's Samba share, organised by session name and date. +2. Connect to the share from a lab workstation to copy files off, or point analysis tools at it directly. +3. Use `tools/analyse_framesync.py` and `tools/make_aligned_video.py` to check multi-camera timing and build aligned review videos. + +## Troubleshooting a module + +1. A module marked offline usually means its heartbeat timed out - check power, PoE link and network cabling first. +2. The System page shows per-module CPU, disk, temperature and PTP offset - a drifting PTP offset points to a clock sync problem, not a recording bug. +3. Reboot or shut down an individual module from its actions menu on the System page if it needs a clean restart. diff --git a/docs/readthedocs/how_it_works.md b/docs/readthedocs/how_it_works.md index 037c67e15..1da5230b3 100644 --- a/docs/readthedocs/how_it_works.md +++ b/docs/readthedocs/how_it_works.md @@ -1,3 +1,56 @@ # How SAVIOUR Works -Coming soon! \ No newline at end of file +## Overview + +A SAVIOUR system is one **controller** talking to any number of **modules** over a PoE LAN. Every device is a Raspberry Pi 5 running the same codebase, with `/etc/saviour/config` telling it which of the two roles to boot into. + +The controller is the brains of the operation: it's the PTP grandmaster (so every device's clock is disciplined to the same reference), it runs mDNS service discovery to find modules as they come online, and it hosts the web GUI you actually interact with (Flask + Socket.IO, port 5000 by default). Modules are dumber by design - each one just owns a single sensor or piece of equipment (camera, microphone, TTL I/O, etc.) and does what the controller tells it. + +Commands flow controller -> module over a ZeroMQ ROUTER/DEALER socket pair (port 5555): the controller binds a ROUTER, each module connects a DEALER using its own module ID as identity, and sends a "hello" frame to register. Status and heartbeats flow the other way on a separate PUB/SUB pair (port 5556) - modules publish, the controller subscribes to everything. This is plain ZeroMQ, not MQTT or anything with brokers or persistence; if a module isn't connected when a command goes out, it doesn't get it, which is why heartbeats and reconnect logic matter as much as they do. + +The recordings themselves follow a similar controller/module split. While recording, a module writes video/audio straight to its own local storage (its SD card, not the network) - the network is only carrying commands and status, never the recording itself, so a flaky wifi card or a busy switch can't drop frames. Once a segment (or the whole session) finishes, the module exports it across to the controller's NVMe drive over the Samba share described below. From there you can get at it two ways: download it straight from the GUI's Recording page (individual files, or the whole session as a zip), or connect to the controller's Samba share from your own PC and copy files off directly - the GUI download is disabled above a certain size and points you at the share instead, since a browser download isn't a sensible way to move tens of gigabytes. + +None of this needs the internet. The whole point of SAVIOUR is that it works on a closed lab LAN with no cloud dependency - the sections below cover how that's actually wired together. + +## Network + +Every device connects to the same PoE+ switch, controller included. The controller's `eth0` gets a static IP, `10.0.0.1/16` by default (chosen in `saviour-config`, written into `/etc/saviour/config`). Modules don't get static IPs - the controller runs `dnsmasq` as a small DHCP server for the network, handing out addresses from the top half of its own /16 range to anything that plugs in. + +The controller can also act as your gateway to the outside world if you want one (`GATEWAY_MODE=controller` in the config, NAT'd out through wifi or a second interface), or you can point it at an existing router, or run it fully offline. None of the recording/discovery/export machinery cares either way - it's LAN-only by design. + +## Avahi (mDNS hostname resolution) + +Avahi is the system mDNS daemon (the same one most Linux desktops use for network discovery), and on SAVIOUR its job is narrow: making `saviour.local` resolve to the controller's IP from any device on the same LAN, so you can type that into a browser instead of hunting for an IP address. `saviour-config` sets `host-name=saviour` in `/etc/avahi/avahi-daemon.conf` and restricts it to `eth0` only. + +That's genuinely all avahi does here - it is *not* how modules and the controller find each other. That's a separate mechanism, covered next, and it's worth keeping the two straight because they're easy to conflate. + +## Zeroconf (service discovery) + +Module <-> controller discovery is handled entirely by `python-zeroconf`, a Python library implementing mDNS/DNS-SD directly (it doesn't touch avahi at all, even though they're both speaking the same underlying protocol). Both sides register their own service type and browse for the other's: + +- The controller registers itself as `_controller._tcp.local.` and browses for `_module._tcp.local.`. +- Each module registers itself as `_module._tcp.local.` and browses for `_controller._tcp.local.`. + +The advertised service carries properties in its TXT record - a module's registration includes its `type`, `id`, `name`, `group` and current software `version`, so the controller can build a full picture of what just appeared on the network (IP and port come from the mDNS packet itself) without a separate handshake. This is also why a module rebooting with new firmware shows its updated version on the Dashboard automatically once it re-announces - no polling required. + +## Samba Shares + +Exporting recordings off a module and onto the controller is done over Samba (CIFS), mounted by the module at `/mnt/export`. The controller runs `smbd` serving a `controller_share`, with three separate accounts: `saviour_module` (a machine account every module authenticates as, password regenerated automatically), and `saviour_user`/`saviour_admin` for a researcher connecting from their own laptop to grab files. + +Getting a file onto the share safely, without ever losing a partially-copied recording, is the fiddly part. Each file is renamed locally to `PENDING_`, copied across to the share (also as `PENDING_`), `fsync`'d, then atomically renamed to its final name on both sides - only then is the local copy moved into an `exported/` folder. If anything fails partway through, the `PENDING_` rename is rolled back locally, so the file is always either safely on the controller or still sitting untouched on the module, never in a state where it looks exported but isn't. + +## systemd + +Everything runs under systemd. `saviour.service` is what actually launches the controller or module Python process - `saviour-config` writes its `WorkingDirectory`/`ExecStart` to point at the right entrypoint for whatever role and type you've configured, and it's set to restart automatically if the process dies. + +PTP sync (the thing that makes every camera's timestamps comparable to the millisecond) runs as its own pair of services, `ptp4l.service` and `phc2sys.service`, independent of `saviour.service` - `ptp4l` disciplines a hardware clock, `phc2sys` disciplines the system clock from it, and they keep running even if you restart the SAVIOUR process itself. + +One small logging detail worth knowing if you're ever reading logs directly rather than through the web UI: when running under systemd, log lines don't carry their own timestamp, because journald already timestamps every entry it receives (`journalctl` shows both). Run the same code manually outside systemd and you'll see timestamps reappear in the log lines themselves - it's not a bug, it's just avoiding printing the same information twice. + +## Picamera2 + +Every camera-type module (`camera`, `apa_camera`, `loom_camera`) is built on Picamera2, the current Raspberry Pi camera stack, wrapped in a shared `CameraBase` class that owns the whole lifecycle: opening the sensor, configuring resolution/framerate/sensor mode, the MJPEG live-preview stream you see on the Dashboard, and segmented recording to disk. + +Recording isn't one long file - it's split into segments (so a multi-hour session isn't one enormous, hard-to-recover video), and every segment gets its own timestamp CSV sidecar recording the real capture time of each frame. That per-frame timestamp is what makes multi-camera alignment possible later - frame *index* alone isn't reliable enough once you're comparing cameras that may drift by a frame or two over a long session. + +`CameraBase` itself is relatively recent - the three camera module types each used to be their own ~1200-line, independently-maintained implementation of the same Picamera2 plumbing, which meant a bug fixed in one didn't get fixed in the other two. They're now thin subclasses (as little as 47 lines) of one shared base, which is also why cross-cutting camera features - FrameSync, rotation, sensor-mode switching - only need to be written once. diff --git a/docs/readthedocs/images/acquisition-board.webp b/docs/readthedocs/images/acquisition-board.webp new file mode 100644 index 000000000..3d686d453 Binary files /dev/null and b/docs/readthedocs/images/acquisition-board.webp differ diff --git a/docs/readthedocs/images/complete_system.webp b/docs/readthedocs/images/complete_system.webp new file mode 100644 index 000000000..fb7c72e59 Binary files /dev/null and b/docs/readthedocs/images/complete_system.webp differ diff --git a/docs/readthedocs/images/ephys-gui-ttls.webp b/docs/readthedocs/images/ephys-gui-ttls.webp new file mode 100644 index 000000000..0a5e4303e Binary files /dev/null and b/docs/readthedocs/images/ephys-gui-ttls.webp differ diff --git a/docs/readthedocs/images/in_out_label.webp b/docs/readthedocs/images/in_out_label.webp new file mode 100644 index 000000000..c9b538d3d Binary files /dev/null and b/docs/readthedocs/images/in_out_label.webp differ diff --git a/docs/readthedocs/images/io-board.png-2.webp b/docs/readthedocs/images/io-board.png-2.webp new file mode 100644 index 000000000..7821c3289 Binary files /dev/null and b/docs/readthedocs/images/io-board.png-2.webp differ diff --git a/docs/readthedocs/images/saviour_cam_cad_sep_2025.jpg b/docs/readthedocs/images/saviour_cam_cad_sep_2025.jpg deleted file mode 100644 index 2be3d7fe5..000000000 Binary files a/docs/readthedocs/images/saviour_cam_cad_sep_2025.jpg and /dev/null differ diff --git a/docs/readthedocs/images/saviour_cam_cad_sep_2025.webp b/docs/readthedocs/images/saviour_cam_cad_sep_2025.webp new file mode 100644 index 000000000..3bf0137e3 Binary files /dev/null and b/docs/readthedocs/images/saviour_cam_cad_sep_2025.webp differ diff --git a/docs/readthedocs/index.md b/docs/readthedocs/index.md index 1e209128e..c1291832c 100644 --- a/docs/readthedocs/index.md +++ b/docs/readthedocs/index.md @@ -6,6 +6,20 @@ A SAVIOUR system consists of a "controller" device talking to one or more "modul SAVIOUR was developed to meet the needs of behavioural neuroscience researchers (at the Simon's Initative for the Developing Brain, University of Edinburgh) who wanted an affordable, high throughput means of running their experiments with low barriers to entry. -To learn more about making recordings with SAVIOUR, visit [Getting Started](getting_started.md) and [FAQs](faqs.md). +To learn more about making recordings with SAVIOUR, visit [Getting Started](getting_started.md) and [FAQs](faqs.md). -To learn about developing modules, GUIs, and the process of contributing to SAVIOUR, start with [How it Works](how_it_works.md) and then visit [Contributing to SAVIOUR](about/contributing.md). +To learn about developing modules, GUIs, and the process of contributing to SAVIOUR, start with [How it Works](how_it_works.md) and then visit [Contributing to SAVIOUR](about/contributing.md). + +## All pages + +- [Getting Started](getting_started.md) - assigning device roles, connecting modules, running and exporting a recording session. +- [FAQs](faqs.md) +- [Using SAVIOUR with Ephys](open_ephys.md) - syncing and aligning SAVIOUR recordings with an Open Ephys rig. +- [How it Works](how_it_works.md) - architecture and internals, for contributors. +- [Hardware](hardware.md) - the parts list for a controller, and for each module type. +- [CAD/3D Prints](cad.md) +- [Contributing to SAVIOUR](about/contributing.md) +- [License](about/license.md) +- [Acknowledgements](about/acknowledgements.md) + +These pages are also published in full at [saviour.readthedocs.io](https://saviour.readthedocs.io) - the same content shown here in the SAVIOUR GUI is the single source for both. diff --git a/docs/readthedocs/open_ephys.md b/docs/readthedocs/open_ephys.md index 8cbfa45e6..aa42b46ee 100644 --- a/docs/readthedocs/open_ephys.md +++ b/docs/readthedocs/open_ephys.md @@ -1,3 +1,39 @@ # Using SAVIOUR with Open Ephys -Coming soon! \ No newline at end of file +**IMPORTANT**: read this guide, and double check everything is working correctly before hitting record! Researchers have lost hours of data because a cable was plugged into the wrong port of their HDMI board. + +![A complete SAVIOUR and Open Ephys rig: acquisition laptop, acquisition board, and the animal in the recording chamber](images/complete_system.webp) + +The acquisition board itself, up close: + +![The Open Ephys acquisition board](images/acquisition-board.webp) + +On the back of it are four HDMI ports for digital I/O - SAVIOUR only ever uses **Digital In**. + +![The four HDMI digital I/O ports on the acquisition board, labelled - Digital In is the one SAVIOUR uses](images/in_out_label.webp) + +Each HDMI port carries 8 TTL channels, so you'll need an IO board to break the HDMI connector out into individual BNC connectors before you can wire a single channel to a TTL module. SAVIOUR only needs one of those 8 channels on Digital In - a pseudorandom pulse train for alignment, per the steps below. The other 7 channels on that same port are free for anything else you want logged alongside the ephys data - an experiment-start signal, a stimulus trigger, a lever press, whatever your experiment needs. + +## Syncing SAVIOUR to an ephys acquisition system + +1. Wire a shared TTL line between the ephys acquisition system and a TTL module input pin so both systems see the same sync pulses. +2. Recording an input pin logs each pulse edge with a PTP-disciplined timestamp, in the same clock domain as every camera/microphone/TTL module on the network. +3. On the ephys side, log the same pulses against its own acquisition clock - the shared pulses are your alignment reference between the two clocks. + +Once wired up correctly, each channel's pulses show up directly in the Open Ephys GUI as coloured columns overlaid on the recording - one colour per input channel, so you can see exactly which channel fired, and when, right alongside the neural data it's aligning. + +![Open Ephys GUI showing inbound TTL pulses overlaid on the recording as coloured columns, one colour per input channel](images/ephys-gui-ttls.webp) + +## Sending sync pulses out to the ephys rig + +1. Configure a TTL module output pin (fixed-interval or pseudorandom pulse train) and wire it into a spare digital/sync input on the acquisition system. +2. Start the pulse train before recording begins so there are reference edges throughout the whole session, not just at the start. +3. A pseudorandom (non-periodic) pulse train is easier to align unambiguously than a fixed-rate one if a few pulses are missed on either side. + +## Aligning ephys data post-hoc + +1. Export the TTL module's per-pulse timestamp CSV alongside the ephys recording. +2. Match pulse edges between the two logs to fit a clock offset (and drift, if the ephys clock isn't disciplined) between ephys time and SAVIOUR/PTP time. +3. Apply that mapping to bring spike times, video frames and any other module's timestamps into one common timeline. + +For more information and tooling, see the dedicated [saviour-ephys-analysis](https://github.com/Kind-Wyllie-lab/saviour-ephys-analysis) repository. diff --git a/src/controller/frontend/package-lock.json b/src/controller/frontend/package-lock.json index 64ad7e3e8..b4922e5d7 100644 --- a/src/controller/frontend/package-lock.json +++ b/src/controller/frontend/package-lock.json @@ -8,9 +8,14 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "github-slugger": "^2.0.0", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-markdown": "^10.1.0", "react-router": "^8.3.0", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", "socket.io-client": "^4.8.1" }, "devDependencies": { @@ -1430,13 +1435,39 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1444,11 +1475,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1464,6 +1509,18 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -1548,6 +1605,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1644,6 +1711,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1661,6 +1738,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1681,6 +1798,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1720,7 +1847,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -1740,6 +1866,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1747,6 +1886,28 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.375", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", @@ -1776,6 +1937,18 @@ "node": ">=10.0.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -2008,6 +2181,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2018,6 +2201,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2133,6 +2322,12 @@ "node": ">=6.9.0" } }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2169,6 +2364,186 @@ "node": ">=8" } }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2206,6 +2581,46 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2229,6 +2644,28 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2360,6 +2797,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2370,29 +2817,884 @@ "yallist": "^3.0.2" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", "engines": { - "node": "*" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2488,6 +3790,43 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2567,6 +3906,16 @@ "node": ">= 0.8.0" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2598,6 +3947,33 @@ "react": "^19.2.8" } }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -2629,6 +4005,104 @@ } } }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2761,6 +4235,30 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2774,6 +4272,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2804,6 +4320,26 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2817,6 +4353,93 @@ "node": ">= 0.8.0" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2858,6 +4481,48 @@ "punycode": "^2.1.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -2933,6 +4598,16 @@ } } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3007,6 +4682,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/src/controller/frontend/package.json b/src/controller/frontend/package.json index a57a944a7..4144b8935 100644 --- a/src/controller/frontend/package.json +++ b/src/controller/frontend/package.json @@ -10,9 +10,14 @@ "preview": "vite preview" }, "dependencies": { + "github-slugger": "^2.0.0", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-markdown": "^10.1.0", "react-router": "^8.3.0", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", "socket.io-client": "^4.8.1" }, "devDependencies": { diff --git a/src/controller/frontend/src/acoustic_startle/pages/AcousticStartleDashboard/AcousticStartleDashboard.jsx b/src/controller/frontend/src/acoustic_startle/pages/AcousticStartleDashboard/AcousticStartleDashboard.jsx index ff87b599f..332a7c950 100644 --- a/src/controller/frontend/src/acoustic_startle/pages/AcousticStartleDashboard/AcousticStartleDashboard.jsx +++ b/src/controller/frontend/src/acoustic_startle/pages/AcousticStartleDashboard/AcousticStartleDashboard.jsx @@ -41,7 +41,7 @@ function Dashboard() { key={m.id} ip={m.ip} port={STREAM_PORTS.ttl} - label={`${m.name} — TTL`} + label={`${m.name} - TTL`} isRecording={m.status === "RECORDING"} /> ))} diff --git a/src/controller/frontend/src/basic/App.css b/src/controller/frontend/src/basic/App.css index 08b68aa9c..b61a30194 100644 --- a/src/controller/frontend/src/basic/App.css +++ b/src/controller/frontend/src/basic/App.css @@ -16,6 +16,10 @@ flex-direction: column; } + .content > .recording-status-widget { + flex-shrink: 0; + } + .nas-warning-banner { background: color-mix(in srgb, var(--error-color, #e74c3c) 15%, var(--card-bg-color)); color: var(--error-color, #e74c3c); diff --git a/src/controller/frontend/src/basic/App.jsx b/src/controller/frontend/src/basic/App.jsx index 4dd342bf7..565281c89 100644 --- a/src/controller/frontend/src/basic/App.jsx +++ b/src/controller/frontend/src/basic/App.jsx @@ -14,6 +14,7 @@ import Guide from "./pages/Guide/Guide"; import ClockModal from "./components/ClockModal/ClockModal"; import FaultAlertModal from "./components/FaultAlertModal/FaultAlertModal"; import ConnectionOverlay from "./components/ConnectionOverlay/ConnectionOverlay"; +import RecordingStatusWidget from "./components/RecordingStatusWidget/RecordingStatusWidget"; import useClockOnce from "/src/hooks/useClockOnce"; import useSessions from "/src/hooks/useSessions"; import socket from "/src/socket"; @@ -90,9 +91,10 @@ function App() {
+ {nasHealth?.status === "error" && (
- NAS unreachable — exports will queue locally until the share is restored. + NAS unreachable - exports will queue locally until the share is restored. {nasHealth.error && ({nasHealth.error})}
)} diff --git a/src/controller/frontend/src/basic/components/ConfigCard/APACameraConfigCard/APACameraConfigCard.jsx b/src/controller/frontend/src/basic/components/ConfigCard/APACameraConfigCard/APACameraConfigCard.jsx index 74fb24253..161576ae6 100644 --- a/src/controller/frontend/src/basic/components/ConfigCard/APACameraConfigCard/APACameraConfigCard.jsx +++ b/src/controller/frontend/src/basic/components/ConfigCard/APACameraConfigCard/APACameraConfigCard.jsx @@ -275,7 +275,7 @@ function APACameraConfigCard({ id, module, clipboard, onCopy }) { }}> {presets.map(p => ( ))} @@ -312,12 +312,12 @@ function APACameraConfigCard({ id, module, clipboard, onCopy }) {
Sensor mode: {selectedMode.label}
) : (
- Sensor modes not yet loaded — click Refresh + Sensor modes not yet loaded - click Refresh
)} {fpsOverMax && (
- {cam.fps} fps exceeds mode max ({maxFps} fps) — will be clamped on apply + {cam.fps} fps exceeds mode max ({maxFps} fps) - will be clamped on apply
)} diff --git a/src/controller/frontend/src/basic/components/ConfigCard/CameraConfigCard/CameraConfigCard.jsx b/src/controller/frontend/src/basic/components/ConfigCard/CameraConfigCard/CameraConfigCard.jsx index 9ed71782b..dda88e2fd 100644 --- a/src/controller/frontend/src/basic/components/ConfigCard/CameraConfigCard/CameraConfigCard.jsx +++ b/src/controller/frontend/src/basic/components/ConfigCard/CameraConfigCard/CameraConfigCard.jsx @@ -261,7 +261,7 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) { }}> {presets.map(p => ( ))} @@ -298,7 +298,7 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) { )} {currentSyncMode === "client" && serverFps != null && (
- FPS locked to {serverFps} fps via frame sync — resolution can be changed freely. + FPS locked to {serverFps} fps via frame sync - resolution can be changed freely.
)} {selectedMode && currentSyncMode !== "client" && ( @@ -311,7 +311,7 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) { )} {fpsOverMax && (
- {currentFps} fps exceeds mode max ({maxFps} fps) — will be clamped on apply + {currentFps} fps exceeds mode max ({maxFps} fps) - will be clamped on apply
)} @@ -475,7 +475,7 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) { )} {framesyncEnabled && currentSyncMode === "server" && (
- This camera is the FrameSync transmitter — it broadcasts timing to every + This camera is the FrameSync transmitter - it broadcasts timing to every other FrameSync-enabled camera. The controller assigns this automatically; start client cameras first, then this one.
@@ -488,18 +488,18 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) { )} {framesyncEnabled && currentSyncMode === "none" && (
- Waiting for the controller to assign a FrameSync role — this camera + Waiting for the controller to assign a FrameSync role - this camera may be offline, or has just been enabled and hasn't been reconciled yet.
)} {fpsMismatch && (
- FPS mismatch: this camera is {cam.fps} fps but server {syncServerModule.name} is {serverFps} fps — frame sync will not be 1:1. + FPS mismatch: this camera is {cam.fps} fps but server {syncServerModule.name} is {serverFps} fps - frame sync will not be 1:1.
)} {exposureMismatch && !fpsMismatch && (
- Exposure mismatch: {clientExposureUs}µs here vs {serverExposureUs}µs on server — brightness will differ. + Exposure mismatch: {clientExposureUs}µs here vs {serverExposureUs}µs on server - brightness will differ.
)}
@@ -509,7 +509,7 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) { onChange={e => handleChange(["camera", "sync_lock_exposure"], e)} />
- While frame-synced: fixes exposure/gain so brightness matches across cameras — + While frame-synced: fixes exposure/gain so brightness matches across cameras - overrides Image tab's "Auto gain/exposure".
@@ -551,7 +551,7 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) { onChange={e => handleChange(["camera", "overlay_framerate_on_preview"], e)} />
- Live monitoring stream only — never on saved videos. + Live monitoring stream only - never on saved videos.
@@ -626,9 +626,9 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) {
Opens a window on each detected physical monitor, picked by GLFW's raw detection order. There's no way to know in advance which is - Monitor 0 vs 1 — use the "Near screen" test flash button below to + Monitor 0 vs 1 - use the "Test screens" flash button below to check, and switch this if it's backwards. If a second monitor is - detected it automatically gets the keepalive/near-test display. + detected it automatically gets the near/keepalive-only display.
@@ -756,7 +756,7 @@ function CameraConfigCard({ id, module, clipboard, onCopy, syncServerModule }) { onChange={e => handleChange(["loom_stimulus", "photodiode_y_ndc"], e)} />
-
Near screen
+
Keep-alive (both screens)
handleChange(["audiomoth", "gain"], e)}> {GAIN_LABELS.map((label, i) => ( - + ))}
@@ -553,7 +553,7 @@ function MicrophoneConfigCard({ id, module, clipboard, onCopy }) { <> {allSerials.length === 0 ? (
- No AudioMoths discovered — connect devices and refresh + No AudioMoths discovered - connect devices and refresh
) : allSerials.map(serial => (
diff --git a/src/controller/frontend/src/basic/components/ConfigCard/TTLConfigCard/TTLConfigCard.jsx b/src/controller/frontend/src/basic/components/ConfigCard/TTLConfigCard/TTLConfigCard.jsx index e5baf1b36..a300f5ef8 100644 --- a/src/controller/frontend/src/basic/components/ConfigCard/TTLConfigCard/TTLConfigCard.jsx +++ b/src/controller/frontend/src/basic/components/ConfigCard/TTLConfigCard/TTLConfigCard.jsx @@ -405,7 +405,7 @@ function TTLConfigCard({ id, module, clipboard, onCopy }) {

This will overwrite the {applyAllConfirm.label.toLowerCase()} config on every{" "} - {applyAllConfirm.moduleType ?? "module"} and save immediately — unsaved changes on other modules will be lost. + {applyAllConfirm.moduleType ?? "module"} and save immediately - unsaved changes on other modules will be lost.

diff --git a/src/controller/frontend/src/basic/components/FaultAlertModal/FaultAlertModal.jsx b/src/controller/frontend/src/basic/components/FaultAlertModal/FaultAlertModal.jsx index 1e58723f8..45b78c15d 100644 --- a/src/controller/frontend/src/basic/components/FaultAlertModal/FaultAlertModal.jsx +++ b/src/controller/frontend/src/basic/components/FaultAlertModal/FaultAlertModal.jsx @@ -10,7 +10,7 @@ function parseTimestamp(str) { } function formatDateTime(date) { - if (!date) return "—"; + if (!date) return "-"; return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } @@ -40,7 +40,7 @@ export default function FaultAlertModal({ faultedSessions, onAcknowledge }) { const title = faultedSessions.length === 1 ? (faultedSessions[0].state === "error" ? "Recording Fault Detected" - : "Recording Fault — Session Recovered") + : "Recording Fault - Session Recovered") : "Recording Faults Detected"; return ( diff --git a/src/controller/frontend/src/basic/components/HealthSummaryWidget/HealthSummaryWidget.jsx b/src/controller/frontend/src/basic/components/HealthSummaryWidget/HealthSummaryWidget.jsx index 69300b10f..b0d1d50eb 100644 --- a/src/controller/frontend/src/basic/components/HealthSummaryWidget/HealthSummaryWidget.jsx +++ b/src/controller/frontend/src/basic/components/HealthSummaryWidget/HealthSummaryWidget.jsx @@ -197,7 +197,7 @@ export default function HealthSummaryWidget() {
Hottest device - {maxTemp}°C — {maxTempId} + {maxTemp}°C - {maxTempId}
)} @@ -206,7 +206,7 @@ export default function HealthSummaryWidget() {
Fullest disk - {maxDisk}% — {maxDiskId} + {maxDisk}% - {maxDiskId}
)} @@ -215,7 +215,7 @@ export default function HealthSummaryWidget() {
PTP sync (worst) - {ptpDisplay(worstPtp)} — {worstPtpId} + {ptpDisplay(worstPtp)} - {worstPtpId}
)} diff --git a/src/controller/frontend/src/basic/components/ModuleActionsMenu/ModuleActionsMenu.jsx b/src/controller/frontend/src/basic/components/ModuleActionsMenu/ModuleActionsMenu.jsx index af1dc36fd..ba3274178 100644 --- a/src/controller/frontend/src/basic/components/ModuleActionsMenu/ModuleActionsMenu.jsx +++ b/src/controller/frontend/src/basic/components/ModuleActionsMenu/ModuleActionsMenu.jsx @@ -118,18 +118,18 @@ function ModuleActionsMenu({ id, name, isOnline }) {
) : (
- -
+ {/* Admin/Guest badge — lives here (below the title) rather than in + .footer below; class names keep the "footer-" prefix since the + styling itself is unchanged, only its position moved. */}
+ + +
+ +

© SIDB 2026

- - GitHub Page - +
{showUpdateModal && ( @@ -408,7 +424,7 @@ function Sidebar({ navItems }) {

Power action for all modules and controller

Reboot will restart all devices and reconnect automatically.
- Shutdown will power off all devices — manual restart required. + Shutdown will power off all devices - manual restart required.

diff --git a/src/controller/frontend/src/basic/pages/Dashboard/Dashboard.css b/src/controller/frontend/src/basic/pages/Dashboard/Dashboard.css index 11f95a639..f81a32e0d 100644 --- a/src/controller/frontend/src/basic/pages/Dashboard/Dashboard.css +++ b/src/controller/frontend/src/basic/pages/Dashboard/Dashboard.css @@ -1,7 +1,8 @@ .dashboard { display: flex; flex-direction: column; - height: 100vh; + flex: 1; + min-height: 0; overflow: hidden; } diff --git a/src/controller/frontend/src/basic/pages/Dashboard/Dashboard.jsx b/src/controller/frontend/src/basic/pages/Dashboard/Dashboard.jsx index 5cf7a6a80..76ad06f32 100644 --- a/src/controller/frontend/src/basic/pages/Dashboard/Dashboard.jsx +++ b/src/controller/frontend/src/basic/pages/Dashboard/Dashboard.jsx @@ -5,7 +5,6 @@ import useModules from "/src/hooks/useModules"; import MJPEGStreamCard from "/src/basic/components/MJPEGStreamCard/MJPEGStreamCard"; import HealthSummaryWidget from "/src/basic/components/HealthSummaryWidget/HealthSummaryWidget"; import ModuleList from "/src/basic/components/ModuleList/ModuleList"; -import RecordingStatusWidget from "/src/basic/components/RecordingStatusWidget/RecordingStatusWidget"; // MJPEG stream port by module type const STREAM_PORTS = { @@ -142,12 +141,12 @@ function Dashboard() { })), ...micModules.map(m => ({ id: m.id, ip: m.ip, port: STREAM_PORTS.microphone, - label: `${m.name} — Audio`, isRecording: m.status === "RECORDING", + label: `${m.name} - Audio`, isRecording: m.status === "RECORDING", syncStatus: m.config_sync_status, })), ...ttlModules.map(m => ({ id: m.id, ip: m.ip, port: STREAM_PORTS.ttl, - label: `${m.name} — TTL`, isRecording: m.status === "RECORDING", + label: `${m.name} - TTL`, isRecording: m.status === "RECORDING", syncStatus: m.config_sync_status, })), ], [cameraModules, micModules, ttlModules]); @@ -156,8 +155,6 @@ function Dashboard() { return (
- - {groups.length > 0 && (
@@ -238,7 +235,7 @@ function Dashboard() { key={m.id} ip={m.ip} port={STREAM_PORTS.microphone} - label={`${m.name} — Audio`} + label={`${m.name} - Audio`} isRecording={m.status === "RECORDING"} syncStatus={m.config_sync_status} /> @@ -248,7 +245,7 @@ function Dashboard() { key={m.id} ip={m.ip} port={STREAM_PORTS.ttl} - label={`${m.name} — TTL`} + label={`${m.name} - TTL`} isRecording={m.status === "RECORDING"} syncStatus={m.config_sync_status} /> diff --git a/src/controller/frontend/src/basic/pages/Guide/Guide.css b/src/controller/frontend/src/basic/pages/Guide/Guide.css index fedced03d..871384ba0 100644 --- a/src/controller/frontend/src/basic/pages/Guide/Guide.css +++ b/src/controller/frontend/src/basic/pages/Guide/Guide.css @@ -1,140 +1,259 @@ .guide-page { padding: 16px 24px; + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 28px; + /* Contained scroll region (matches Settings/Recording) rather than + letting a long doc grow the outer app shell — also what makes the + contents panel's own `sticky` below actually stick against something, + instead of scrolling away with the rest of the page. */ + flex: 1; + min-height: 0; + overflow-y: auto; } -.guide-header { - margin-bottom: 16px; -} +/* ── Contents panel — left-aligned, like readthedocs' own sidebar nav or a + Word/Google Docs outline pane, not a button bar. ─────────────────────── */ -.guide-header h2 { - margin: 0 0 4px; +.guide-contents { + flex-shrink: 0; + width: 200px; + position: sticky; + top: 16px; + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 14px 10px; + background: var(--card-bg-color); } -.guide-intro { - color: var(--secondary-text-color); - margin: 0; +.guide-contents-title { + font-size: 0.72em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + opacity: 0.6; + padding: 0 8px; + margin-bottom: 8px; } -.guide-tabs { - position: sticky; - top: 0; - z-index: 1; +.guide-contents-list { + list-style: none; + margin: 0; + padding: 0; display: flex; - gap: 4px; - margin-bottom: 16px; - padding: 8px 0; - background: var(--bg-color); - border-bottom: 1px solid var(--border-color); + flex-direction: column; + gap: 1px; } -.guide-tab { - padding: 6px 14px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--card-bg-color); +.guide-contents-link { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + border-left: 2px solid transparent; + padding: 6px 8px; + border-radius: 0 6px 6px 0; color: var(--text-color); - cursor: pointer; font-size: 0.9em; + cursor: pointer; +} + +.guide-contents-link:hover { + background: color-mix(in srgb, var(--text-color) 8%, transparent); } -.guide-tab:hover { - border-color: var(--accent-color); +.guide-contents-link--active { + border-left-color: var(--accent-color); + background: color-mix(in srgb, var(--accent-color) 12%, transparent); color: var(--accent-color); + font-weight: 600; } -.guide-tab--active { - background: var(--accent-color); - border-color: var(--accent-color); - color: #fff; +/* ── Content column ──────────────────────────────────────────────────────── */ + +.guide-content { + flex: 1; + min-width: 0; } .guide-topics { display: flex; flex-direction: column; gap: 16px; + max-width: 900px; } .guide-topic { min-width: 0; - max-width: 720px; } -.guide-topic h3 { - margin-top: 0; +/* ── On-this-page outline — right rail, mirrors .guide-contents but lighter + since it's secondary/in-page navigation, not primary site navigation. ─── */ + +.guide-outline { + flex-shrink: 0; + width: 180px; + position: sticky; + top: 16px; } -.guide-video-wrap { - position: relative; - width: 100%; - padding-top: 56.25%; /* 16:9 */ - margin-bottom: 12px; - border-radius: 6px; - overflow: hidden; - background: var(--bg-color); +.guide-outline-title { + font-size: 0.72em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + opacity: 0.6; + margin-bottom: 8px; } -.guide-video { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - border: 0; +.guide-outline-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 2px; + border-left: 1px solid var(--border-color); +} + +.guide-outline-item a { + display: block; + padding: 3px 0 3px 12px; + margin-left: -1px; + border-left: 1px solid transparent; + color: var(--secondary-text-color); + font-size: 0.82em; + line-height: 1.3; +} + +.guide-outline-item a:hover { + color: var(--accent-color); + border-left-color: var(--accent-color); } -.guide-video-placeholder { +.guide-outline-item--level3 a { + padding-left: 22px; + font-size: 0.78em; +} + +/* Outline is a "nice to have" on wide screens; the primary contents panel + stays visible all the way down to the mobile stacked layout below. */ +@media (max-width: 1000px) { + .guide-outline { + display: none; + } +} + +@media (max-width: 720px) { + .guide-page { + flex-direction: column; + } + + .guide-contents { + width: 100%; + position: static; + } +} + +.guide-missing { color: var(--secondary-text-color); font-style: italic; - margin: 0 0 12px; } -.guide-steps { - margin: 0; - padding-left: 20px; +/* ── Rendered markdown body ──────────────────────────────────────────────── */ +/* .card h2 (index.css) centers card-title-style headings; markdown body + subsection headings read better left-aligned. */ + +.guide-topic h1 { + margin-top: 0; + font-size: 1.5em; } -.guide-steps li { - margin-bottom: 6px; +.guide-topic h2 { + text-align: left; + font-size: 1.2em; + margin: 1.2em 0 0.5em; } -.guide-steps li:last-child { - margin-bottom: 0; +.guide-topic h2:first-child { + margin-top: 0; } -.guide-faq { - max-width: 720px; - padding: 4px 12px; +.guide-topic h3 { + font-size: 1.05em; + margin: 1em 0 0.4em; } -.guide-faq-item { - border-bottom: 1px solid var(--border-color); +.guide-topic p { + line-height: 1.5; + margin: 0 0 0.8em; } -.guide-faq-item:last-child { - border-bottom: none; +.guide-topic ul, +.guide-topic ol { + margin: 0 0 0.8em; + padding-left: 22px; + line-height: 1.5; } -.guide-faq-question { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - width: 100%; - padding: 12px 4px; - border: none; +.guide-topic li { + margin-bottom: 4px; +} + +.guide-topic a { + color: var(--accent-color); +} + +.guide-topic code { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.9em; + background: color-mix(in srgb, var(--text-color) 8%, transparent); + padding: 0.1em 0.35em; + border-radius: 4px; +} + +.guide-topic pre { + background: color-mix(in srgb, var(--text-color) 8%, transparent); + padding: 10px 12px; + border-radius: 6px; + overflow-x: auto; +} + +.guide-topic pre code { background: none; - color: var(--text-color); + padding: 0; +} + +.guide-topic blockquote { + margin: 0 0 0.8em; + padding: 4px 12px; + border-left: 3px solid var(--border-color); + color: var(--secondary-text-color); +} + +.guide-topic table { + border-collapse: collapse; + width: 100%; + margin: 0 0 0.8em; + font-size: 0.92em; +} + +.guide-topic th, +.guide-topic td { + border: 1px solid var(--border-color); + padding: 6px 10px; text-align: left; - font-size: 0.95em; - font-weight: 600; - cursor: pointer; } -.guide-faq-caret { - flex-shrink: 0; - opacity: 0.6; +.guide-topic img { + max-width: 100%; + border-radius: 6px; } -.guide-faq-answer { - margin: 0 4px 12px; - color: var(--secondary-text-color); +.guide-topic hr { + border: none; + border-top: 1px solid var(--border-color); + margin: 1em 0; } diff --git a/src/controller/frontend/src/basic/pages/Guide/Guide.jsx b/src/controller/frontend/src/basic/pages/Guide/Guide.jsx index fab64478d..78a5e438f 100644 --- a/src/controller/frontend/src/basic/pages/Guide/Guide.jsx +++ b/src/controller/frontend/src/basic/pages/Guide/Guide.jsx @@ -1,207 +1,158 @@ -import React, { useState } from "react"; +import React, { useState, useMemo, useCallback, useRef } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import rehypeRaw from "rehype-raw"; +import rehypeSlug from "rehype-slug"; import "./Guide.css"; +import { docs, resolveDocLink, resolveDocImage, extractOutline } from "/src/docs/loadDocs.js"; -// Each topic gets a short written walkthrough plus an optional tutorial -// video. Fill in `youtubeId` (the id from a youtube.com/watch?v= URL, -// or a full youtube.com/embed/ share link's id) to embed a video — -// topics with no id yet just show the written steps. -const BASICS_TOPICS = [ - { - title: "Assigning a device role", - youtubeId: null, - steps: [ - "Flash the SD card and boot the Raspberry Pi 5 on the PoE network.", - "Run `sudo saviour-config` on the device.", - "Choose Controller (one per system) or Module, then pick the module type (camera, microphone, TTL, RFID, ...).", - "The device reboots into its assigned role and appears automatically once discovered.", - ], - }, - { - title: "Connecting modules to the controller", - youtubeId: null, - steps: [ - "Power on the controller first — it acts as the PTP grandmaster and service discovery hub.", - "Power on modules; they register over mDNS and appear on the Dashboard within a few seconds.", - "Check the System page to confirm every module shows a recent heartbeat and a locked PTP offset before recording.", - ], - }, - { - title: "Running a recording session", - youtubeId: null, - steps: [ - "Configure each module on the Settings page (resolution, sample rate, etc.) before starting.", - "On the Recording page, click \"Check Ready\" to confirm PTP sync is within threshold on every module.", - "Start the session, and stop it (or let a scheduled window end it) once you're done.", - "Recordings export automatically to the controller's share once each module finishes.", - ], - }, - { - title: "Exporting and retrieving data", - youtubeId: null, - steps: [ - "Recordings land on the controller's Samba share, organised by session name and date.", - "Connect to the share from a lab workstation to copy files off, or point analysis tools at it directly.", - "Use tools/analyse_framesync.py and tools/make_aligned_video.py to check multi-camera timing and build aligned review videos.", - ], - }, - { - title: "Troubleshooting a module", - youtubeId: null, - steps: [ - "A module marked offline usually means its heartbeat timed out — check power, PoE link and network cabling first.", - "The System page shows per-module CPU, disk, temperature and PTP offset — a drifting PTP offset points to a clock sync problem, not a recording bug.", - "Reboot or shut down an individual module from its actions menu on the System page if it needs a clean restart.", - ], - }, +// Mirrors mkdocs.yml's nav — the RTD site's structure is this page's +// structure too, so there's one source of truth for both. +const TABS = [ + { id: "index", label: "Home" }, + { id: "getting_started", label: "Getting Started" }, + { id: "faqs", label: "FAQs" }, + { id: "open_ephys", label: "Using with Ephys" }, + { id: "how_it_works", label: "How it Works" }, + { id: "hardware", label: "Hardware" }, + { id: "cad", label: "CAD/3D Prints" }, + { id: "about", label: "About" }, ]; -const EPHYS_TOPICS = [ - { - title: "Syncing SAVIOUR to an ephys acquisition system", - youtubeId: null, - steps: [ - "Wire a shared TTL line between the ephys acquisition system and a TTL module input pin so both systems see the same sync pulses.", - "Recording an input pin logs each pulse edge with a PTP-disciplined timestamp, in the same clock domain as every camera/microphone/TTL module on the network.", - "On the ephys side, log the same pulses against its own acquisition clock — the shared pulses are your alignment reference between the two clocks.", - ], - }, - { - title: "Sending sync pulses out to the ephys rig", - youtubeId: null, - steps: [ - "Configure a TTL module output pin (fixed-interval or pseudorandom pulse train) and wire it into a spare digital/sync input on the acquisition system.", - "Start the pulse train before recording begins so there are reference edges throughout the whole session, not just at the start.", - "A pseudorandom (non-periodic) pulse train is easier to align unambiguously than a fixed-rate one if a few pulses are missed on either side.", - ], - }, - { - title: "Aligning ephys data post-hoc", - youtubeId: null, - steps: [ - "Export the TTL module's per-pulse timestamp CSV alongside the ephys recording.", - "Match pulse edges between the two logs to fit a clock offset (and drift, if the ephys clock isn't disciplined) between ephys time and SAVIOUR/PTP time.", - "Apply that mapping to bring spike times, video frames and any other module's timestamps into one common timeline.", - ], - }, -]; - -const FAQS = [ - { - question: "How do I know if PTP sync is good enough to start recording?", - answer: "Click \"Check Ready\" on the Recording page — it checks ptp4l and phc2sys offset on every module against a threshold (50 µs by default) and reports which modules aren't ready. The System page also shows live per-module PTP offset if you want to watch it settle after a reboot.", - }, - { - question: "A camera was just rebooted — can I record straight away?", - answer: "Give it 5–10 minutes first. phc2sys needs that long to converge its frequency estimate for that crystal; recording immediately after a reboot can leave a larger-than-usual (but still bounded) inter-camera phase offset.", - }, - { - question: "What happens to a recording if the NAS/export share goes down?", - answer: "Files are staged locally on the module and export is retried with backoff once the share comes back — recordings aren't lost, but they won't appear on the share until the export queue catches up.", - }, - { - question: "Can I change which rig UI (basic / loom / apa / habitat / acoustic startle) a controller shows?", - answer: "The frontend variant is selected by which App is imported in src/controller/frontend/src/main.jsx. Switching rigs means editing that import and rebuilding the frontend — it isn't a runtime setting yet.", - }, - { - question: "Do all modules need to be the same type?", - answer: "No — a system is any mix of camera, microphone, TTL, RFID and rig-specific module types (loom camera, APA camera/arduino) all reporting to one controller. Add or remove modules freely; the controller discovers them automatically over mDNS.", - }, -]; +// mkdocs nests these three under "About" in its nav; combined onto one tab +// here rather than a two-level tab widget, since each file is short. +const ABOUT_DOC_IDS = ["about/license", "about/contributing", "about/acknowledgements"]; -const SECTIONS = [ - { id: "basics", label: "Basics" }, - { id: "ephys", label: "Using with Ephys" }, - { id: "faq", label: "FAQ" }, -]; - -function TopicCard({ topic }) { - return ( -
-

{topic.title}

+function makeMarkdownComponents(docId, onNavigate) { + return { + a({ href, children, ...props }) { + const resolved = resolveDocLink(href, docId); + if (resolved.type === "external") { + return ( + + {children} + + ); + } + if (resolved.type === "anchor") { + return {children}; + } + return ( + { + e.preventDefault(); + onNavigate(resolved.tabId); + }} + {...props} + > + {children} + + ); + }, + img({ src, alt, ...props }) { + return {alt}; + }, + }; +} - {topic.youtubeId ? ( -
-