Compare commits

..

3 Commits

Author SHA1 Message Date
yannickschuchmann
a7b81d5791 Add README documentation and refactor test suite
Replace minimal README with full project docs (setup, usage, architecture).
Remove trivial tests and add meaningful edge case coverage.
2026-03-08 20:39:15 +00:00
yannickschuchmann
b936095286 Add implementation 2026-03-08 18:01:28 +00:00
yannickschuchmann
cc5394c3db Add CLAUDE.md, plan and tasklist 2026-03-08 13:49:28 +00:00
66 changed files with 68475 additions and 2551 deletions

4
.gitignore vendored
View File

@@ -1,3 +1 @@
node_modules node_modules
dist
.DS_Store

55
CLAUDE.md Normal file
View File

@@ -0,0 +1,55 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project
P2P Poll App — a peer-to-peer polling application for the Evocracy democratic coding research experiment (https://demcode.evocracy.org/).
### Goal
Build a simple P2P polling app where users can add poll options and vote. Data exchange works peer-to-peer between clients (no central server for data), though a signaling/sync server for connection establishment is acceptable.
### Research Context
- **Phase 1 (2 weeks):** Submit individual code proposal
- **Phase 2:** Groups of 3 merge solutions into a working prototype
- **Phase 3:** Representatives iteratively merge until one final solution remains
- Code must be clean, explainable, and easy to merge with others' work
- Final code published open-source under MIT license
- Language: JavaScript/TypeScript
## Tech Stack
- **Runtime:** Bun
- **Framework:** Waku (minimal React framework with RSC support)
- **Styling:** Tailwind CSS v4
- **P2P/Data sync:** Automerge with automerge-repo (CRDT-based)
- **Networking:** `automerge-repo-network-websocket` (WebSocketClientAdapter) + `automerge-repo-network-broadcastchannel` (cross-tab sync)
- **Storage:** `automerge-repo-storage-indexeddb` (client-side persistence)
**Waku documentation:** https://waku.gg/llms.txt
## Commands
```bash
bun install # Install dependencies
bun run dev # Start dev server
bun run build # Production build
bun test # Run tests (Bun's built-in test runner)
```
## Architecture
- Waku pages router: pages live in `src/pages/`, layouts in `_layout.tsx`
- Client components use `'use client'` directive
- Automerge Repo is initialized in a client-side provider component wrapping the app
- The shared CRDT document holds the poll state (title, options, votes)
- Peers sync via a lightweight WebSocket sync server (can use `automerge-repo` sync server or the public `wss://sync.automerge.org`)
- `BroadcastChannelNetworkAdapter` enables cross-tab sync
- `useDocument` hook from `@automerge/automerge-repo-react-hooks` for reactive document access
- Every Waku page/layout must export `getConfig` specifying render mode (`'static'` or `'dynamic'`)
## Key Design Principles
- Keep it simple and merge-friendly for Phase 2 group work
- Minimal file count, clear separation of concerns
- No over-engineering — this is a research experiment, not production software

View File

@@ -1,310 +0,0 @@
# Minimal MVP Plan: P2P Poll App
## Recommendation
Build the MVP with:
- Vanilla JS or TypeScript
- Vite for local dev/build
- Yjs for shared state
- `y-webrtc` for browser-to-browser sync
- `y-indexeddb` for local persistence/offline recovery
This is the best fit for a small poll app because the data model is tiny, concurrent edits are possible, and Yjs already solves merge/conflict handling while `y-webrtc` gives a direct P2P transport with only signaling infrastructure.
## Why This Stack
### Recommended: Yjs + y-webrtc
- Best balance of simplicity and correctness
- Clients connect directly over WebRTC
- No custom conflict-resolution logic needed
- Easy to add offline persistence
- Good fit for a single shared document/room
### Not recommended for the first MVP
#### PeerJS
- Very simple transport layer
- But you must design and maintain your own replication and merge rules
- Fine for demos with a host-authoritative peer, weaker for true collaborative editing
#### GUN
- Fast to prototype realtime shared state
- But official tutorials commonly use a GUN peer/relay server for shared data
- For this app, its data model is less explicit than a CRDT and gives less control over vote semantics
#### Automerge
- Very capable CRDT and local-first model
- But heavier than needed for a single small poll
- Better choice if the project is expected to evolve into a richer collaborative app
## MVP Scope
Ship exactly one shared poll per room.
Included:
- Join a poll by opening a shared URL
- Add a new option
- Vote for one option
- Change your vote
- Realtime updates across peers
- Local persistence in the browser
- Basic connection status UI
Excluded:
- User accounts
- Multiple polls per room
- Option deletion/editing
- Authentication/authorization
- Rich presence
- Advanced discovery
- Production-grade TURN/signaling deployment
## MVP UX
Keep the interface intentionally small:
- Poll title at the top
- Option list with vote counts
- One button per option to cast vote
- Small form to add an option
- Status line showing `connecting`, `connected`, or `offline`
- Small label showing which option you voted for
Join model:
- Room ID in the URL, e.g. `/?room=poll-demo`
- Users share the URL manually
Identity model:
- Generate a local `userId` once and store it in `localStorage`
- Optional local display name, also stored locally
## Shared Data Model
Use a single Yjs document per room.
Suggested structure:
- `poll` as a `Y.Map`
- `poll.title` as a string
- `poll.options` as a `Y.Map<optionId, Y.Map>`
- `poll.votes` as a `Y.Map<userId, optionId>`
Each option record:
- `id`
- `label`
- `createdBy`
- `createdAt`
Important design choice:
Do not store vote counters as mutable shared numbers for the MVP.
Instead, derive counts from `votes`.
Why:
- A user changing vote becomes a single write: `votes[userId] = optionId`
- No double-counting logic
- Concurrent writes are easier to reason about
- Rendering counts from `votes` is trivial at this scale
## Sync Model
### Networking
- Use `WebrtcProvider(roomId, ydoc)`
- Start with the default public signaling servers for local development
- If needed, swap to a self-hosted signaling server later without changing the app model
### Persistence
- Use `IndexeddbPersistence(roomId, ydoc)`
- This preserves state across reloads and helps when reconnecting after temporary disconnects
### Conflict behavior
- Concurrent option additions merge naturally
- Concurrent vote changes resolve at the per-user key level
- Tally is recalculated from the merged vote map
## Suggested Project Structure
```text
src/
main.ts
app.ts
state.ts
sync.ts
render.ts
identity.ts
styles.css
index.html
```
Responsibilities:
- `sync.ts`: create Yjs doc, WebRTC provider, IndexedDB provider
- `state.ts`: shared structures, add option, cast vote, selectors
- `identity.ts`: local `userId` and optional name
- `render.ts`: DOM updates
- `app.ts`: wire events to state and rendering
## Implementation Plan
### Phase 1: App shell
- Create Vite app with vanilla TS or JS
- Add a minimal single-page UI
- Parse `room` from the URL
- Generate and persist `userId`
Success check:
- App loads
- Room ID appears in UI
- User can type in the form
### Phase 2: Shared state
- Add Yjs document
- Create root shared maps
- Seed default poll title if missing
- Observe document updates and re-render UI
Success check:
- State exists in one browser tab
- Refresh keeps local state when IndexedDB is enabled
### Phase 3: P2P sync
- Add `y-webrtc`
- Join room based on URL room ID
- Show connection status from provider events
Success check:
- Two browsers on different tabs/devices see the same options
- New options appear in near real time
### Phase 4: Voting logic
- Implement `castVote(optionId)` as `votes.set(userId, optionId)`
- Derive tallies from `votes`
- Highlight the local users current vote
Success check:
- Votes update live across peers
- Changing vote updates counts correctly
### Phase 5: Basic hardening
- Trim/validate option labels
- Prevent empty options
- Ignore duplicate labels case-insensitively for MVP
- Show simple offline/connecting text
Success check:
- Basic misuse does not break the UI
- Reconnect restores updates
## Minimal API Surface
These functions are enough for the first build:
- `getRoomId(): string`
- `getUserId(): string`
- `initSync(roomId): AppSync`
- `ensurePollInitialized()`
- `addOption(label: string)`
- `castVote(optionId: string)`
- `getViewModel(): PollViewModel`
- `render(viewModel)`
## Risks And MVP Mitigations
### NAT / WebRTC connectivity
Risk:
- Some peer pairs may fail to connect in restrictive networks
Mitigation:
- Accept this for MVP
- Keep signaling configurable
- Add TURN only if testing shows frequent failures
### Small public signaling dependency
Risk:
- Public signaling is fine for demos, not ideal for production
Mitigation:
- Treat it as replaceable infrastructure
- Self-host later if the prototype is kept
### Duplicate or messy options
Risk:
- Users may add near-duplicate entries
Mitigation:
- Normalize labels
- Prevent exact duplicates for MVP
### No trust/auth model
Risk:
- Any participant in the room can add options and vote
Mitigation:
- Accept for MVP
- Frame rooms as small trusted groups
## Estimated MVP Size
For one developer:
- Initial prototype: 0.5 to 1 day
- Polished MVP with basic resilience: 1 to 2 days
Rough code size:
- 250 to 500 lines plus styles
## Nice Next Steps After MVP
- Copy-link button
- Participant list / presence
- Vote limit modes: single-choice or multi-choice
- Option editing/deletion
- QR code for room join
- Self-hosted signaling server
- PWA packaging for better offline behavior
## Sources
- PeerJS getting started: https://peerjs.com/client/getting-started
- Yjs collaborative editor guide: https://docs.yjs.dev/getting-started/a-collaborative-editor
- Yjs offline support: https://docs.yjs.dev/getting-started/allowing-offline-editing
- y-webrtc README: https://github.com/yjs/y-webrtc
- GUN tutorial showing shared data via a GUN peer: https://gun.eco/converse
- Automerge network sync: https://automerge.org/docs/tutorial/network-sync/

View File

@@ -1,39 +1,91 @@
# P2P Poll App # P2P Poll App
Small peer-to-peer polling app built with `Vite`, `TypeScript`, `Yjs`, `y-webrtc`, and `y-indexeddb`. A peer-to-peer polling application where users can create polls, add options, and vote — all without a central server. Built for the [Evocracy democratic coding research experiment](https://demcode.evocracy.org/).
## Features ## How It Works
- single shared poll per room Data is synchronized directly between peers using [Automerge](https://automerge.org/) CRDTs (Conflict-free Replicated Data Types). There is no backend database — every client holds a full copy of the poll document and changes merge automatically, even when made offline or concurrently.
- add options collaboratively
- one vote per user, changeable at any time
- peer-to-peer sync over WebRTC
- local browser persistence for refresh/reconnect recovery
- shareable room URL
## Run locally **Sync layers:**
- **WebSocket** (`wss://sync.automerge.org`) — cross-device sync via a public relay
- **BroadcastChannel** — instant cross-tab sync within the same browser
- **IndexedDB** — local persistence across page reloads and offline use
## Requirements
- [Bun](https://bun.sh/) (standalone runtime, no Node.js needed)
## Getting Started
```bash ```bash
npm install bun install # Install dependencies
npm run dev bun run dev # Start dev server (http://localhost:3000)
``` ```
Open the local URL in two tabs or browsers and use the same `?room=` query string to join the same poll. ## Usage
Example: 1. **Create a poll** — Enter a title on the home page and click "Create"
2. **Share it** — Copy the shareable link and send it to others
3. **Vote** — Click an option to vote; click again to unvote
4. **Add options** — Anyone with the link can add new poll options
```text Each browser gets a stable peer ID (stored in localStorage) so votes are tracked per-device and double-voting is prevented.
http://localhost:5173/?room=poll-demo
## Tech Stack
| Layer | Technology |
|---|---|
| Runtime | [Bun](https://bun.sh/) |
| Framework | [Waku](https://waku.gg/) (React Server Components) |
| Styling | Tailwind CSS v4 |
| P2P sync | Automerge + automerge-repo |
| Storage | IndexedDB (client-side) |
## Project Structure
```
src/
├── pages/ # Waku page router
│ ├── _root.tsx # HTML document shell
│ ├── _layout.tsx # Root layout + Providers
│ ├── index.tsx # Home page (create/join polls)
│ └── poll/[id].tsx # Poll view page
├── components/
│ ├── Providers.tsx # Automerge Repo initialization
│ ├── HomeClient.tsx # Create/join poll UI
│ ├── PollPageClient.tsx # Poll ID validation
│ ├── PollView.tsx # Poll display, voting, options
│ └── ConnectionStatus.tsx # P2P connection indicator
├── lib/
│ ├── types.ts # Poll & PollOption interfaces
│ ├── repo.ts # Automerge Repo singleton
│ ├── poll.ts # Pure poll mutation functions
│ ├── peer.ts # Peer ID management
│ └── __tests__/ # Unit tests
└── styles/
└── global.css # Tailwind CSS
``` ```
## Build ## Testing
```bash ```bash
npm run build bun test
``` ```
## Notes Covers poll creation, voting/unvoting, double-vote prevention, option management, and peer ID persistence.
- The app uses public signaling through `y-webrtc` for MVP simplicity. ## Architecture Notes
- Poll state is not stored on an application server.
- WebRTC connectivity can still depend on the network environment of the participating peers. - **Pure business logic** — Poll mutations in `src/lib/poll.ts` are pure functions, used inside Automerge's `changeDoc()` for CRDT-safe updates
- **No server state** — The WebSocket relay only forwards sync messages; it never stores or processes poll data
- **Offline-first** — The app works fully offline; changes sync when connectivity resumes
- **Conflict-free** — Concurrent edits (e.g., two users voting at the same time) merge automatically without conflicts
## Built With
This project was built in collaboration with [Claude Code](https://claude.ai/code), Anthropic's agentic coding tool.
## License
MIT

588
bun.lock Normal file
View File

@@ -0,0 +1,588 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "p2p-poll-app",
"dependencies": {
"@automerge/automerge": "^3.2.4",
"@automerge/automerge-repo": "^2.5.3",
"@automerge/automerge-repo-network-broadcastchannel": "^2.5.3",
"@automerge/automerge-repo-network-websocket": "^2.5.3",
"@automerge/automerge-repo-react-hooks": "^2.5.3",
"@automerge/automerge-repo-storage-indexeddb": "^2.5.3",
"@tailwindcss/vite": "^4.2.1",
"react": "~19.2.4",
"react-dom": "~19.2.4",
"react-server-dom-webpack": "~19.2.4",
"tailwindcss": "^4.2.1",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"waku": "1.0.0-alpha.5",
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "^5.9.3",
},
},
},
"packages": {
"@automerge/automerge": ["@automerge/automerge@3.2.4", "", {}, "sha512-/IAShHSxme5d4ZK0Vs4A0P+tGaR/bSz6KtIJSCIPfwilCxsIqfRHAoNjmsXip6TGouadmbuw3WfLop6cal8pPQ=="],
"@automerge/automerge-repo": ["@automerge/automerge-repo@2.5.3", "", { "dependencies": { "@automerge/automerge": "2.2.8 - 3", "bs58check": "^3.0.1", "cbor-x": "^1.3.0", "debug": "^4.3.4", "eventemitter3": "^5.0.1", "fast-sha256": "^1.3.0", "uuid": "^9.0.0", "xstate": "^5.9.1" } }, "sha512-5ppzsUlzCs6PY+GeFlquaYLnVnYPz/hWzedep37zjb15tqZju1ukPPkBzT7KGEhEAnA99l4vfhfqSVC+1GktJg=="],
"@automerge/automerge-repo-network-broadcastchannel": ["@automerge/automerge-repo-network-broadcastchannel@2.5.3", "", { "dependencies": { "@automerge/automerge-repo": "2.5.3" } }, "sha512-V/JpOsOkPPqe+hJs7Zs8oSKO3dp2/4Qd6D+x3D1rda7OhyOmlJ1HnMLghycnhHDQG8lCP9ex3AH6MYe1ZqgCKw=="],
"@automerge/automerge-repo-network-websocket": ["@automerge/automerge-repo-network-websocket@2.5.3", "", { "dependencies": { "@automerge/automerge-repo": "2.5.3", "cbor-x": "^1.3.0", "debug": "^4.3.4", "eventemitter3": "^5.0.1", "isomorphic-ws": "^5.0.0", "ws": "^8.7.0" } }, "sha512-p6K1YLo34cyGxJ6oMWqeBbWX9rGvEPGyPdX2eq8S/iWNDjYf8lbjKSOhOD8DX87uAMZN/y1XOY/RckwS8lPJbQ=="],
"@automerge/automerge-repo-react-hooks": ["@automerge/automerge-repo-react-hooks@2.5.3", "", { "dependencies": { "@automerge/automerge": "2.2.8 - 3", "@automerge/automerge-repo": "2.5.3", "eventemitter3": "^5.0.1", "react-usestateref": "^1.0.8" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-573jhVX1LMiBf8zV0NaYT2PgsEuqHNz0RnKSzIp49+g+swgb+KtRhpdiF38tbQoDnkFsKpYGR6P+qoAVW8veaw=="],
"@automerge/automerge-repo-storage-indexeddb": ["@automerge/automerge-repo-storage-indexeddb@2.5.3", "", { "dependencies": { "@automerge/automerge-repo": "2.5.3" } }, "sha512-i4tSzY7sC8ZqurHm4Sd9H1lFL2Don9v9yaSoq0FXi4T+4Wg8eus/mtMlUCmS5XwYQL8GX+epbplrlBaBe+fYiw=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w=="],
"@cbor-extract/cbor-extract-darwin-x64": ["@cbor-extract/cbor-extract-darwin-x64@2.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w=="],
"@cbor-extract/cbor-extract-linux-arm": ["@cbor-extract/cbor-extract-linux-arm@2.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q=="],
"@cbor-extract/cbor-extract-linux-arm64": ["@cbor-extract/cbor-extract-linux-arm64@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ=="],
"@cbor-extract/cbor-extract-linux-x64": ["@cbor-extract/cbor-extract-linux-x64@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw=="],
"@cbor-extract/cbor-extract-win32-x64": ["@cbor-extract/cbor-extract-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
"@rollup/plugin-virtual": ["@rollup/plugin-virtual@3.0.2", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@swc/core": ["@swc/core@1.15.18", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.18", "@swc/core-darwin-x64": "1.15.18", "@swc/core-linux-arm-gnueabihf": "1.15.18", "@swc/core-linux-arm64-gnu": "1.15.18", "@swc/core-linux-arm64-musl": "1.15.18", "@swc/core-linux-x64-gnu": "1.15.18", "@swc/core-linux-x64-musl": "1.15.18", "@swc/core-win32-arm64-msvc": "1.15.18", "@swc/core-win32-ia32-msvc": "1.15.18", "@swc/core-win32-x64-msvc": "1.15.18" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.18", "", { "os": "linux", "cpu": "arm" }, "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.18", "", { "os": "linux", "cpu": "x64" }, "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.18", "", { "os": "linux", "cpu": "x64" }, "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.18", "", { "os": "win32", "cpu": "ia32" }, "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.18", "", { "os": "win32", "cpu": "x64" }, "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
"@swc/wasm": ["@swc/wasm@1.15.18", "", {}, "sha512-zeSORFArxqUwfVMTRHu8AN9k9LlfSn0CKDSzLhJDITpgLoS0xpnocxsgMjQjUcVYDgO47r9zLP49HEjH/iGsFg=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
"@vitejs/plugin-rsc": ["@vitejs/plugin-rsc@0.5.21", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.5", "es-module-lexer": "^2.0.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21", "periscopic": "^4.0.2", "srvx": "^0.11.7", "strip-literal": "^3.1.0", "turbo-stream": "^3.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*", "react-server-dom-webpack": "*", "vite": "*" }, "optionalPeers": ["react-server-dom-webpack"] }, "sha512-uNayLT8IKvWoznvQyfwKuGiEFV28o7lxUDnw/Av36VCuGpDFZnMmvVCwR37gTvnSmnpul9V0tdJqY3tBKEaDqw=="],
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
"acorn-loose": ["acorn-loose@8.5.2", "", { "dependencies": { "acorn": "^8.15.0" } }, "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"base-x": ["base-x@4.0.1", "", {}, "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"bs58": ["bs58@5.0.0", "", { "dependencies": { "base-x": "^4.0.0" } }, "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ=="],
"bs58check": ["bs58check@3.0.1", "", { "dependencies": { "@noble/hashes": "^1.2.0", "bs58": "^5.0.0" } }, "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
"cbor-extract": ["cbor-extract@2.2.0", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", "@cbor-extract/cbor-extract-linux-arm": "2.2.0", "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", "@cbor-extract/cbor-extract-linux-x64": "2.2.0", "@cbor-extract/cbor-extract-win32-x64": "2.2.0" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA=="],
"cbor-x": ["cbor-x@1.6.0", "", { "optionalDependencies": { "cbor-extract": "^2.2.0" } }, "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg=="],
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"periscopic": ["periscopic@4.0.2", "", { "dependencies": { "@types/estree": "*", "is-reference": "^3.0.2", "zimmerframe": "^1.0.0" } }, "sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"react-server-dom-webpack": ["react-server-dom-webpack@19.2.4", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "webpack-sources": "^3.2.0" }, "peerDependencies": { "react": "^19.2.4", "react-dom": "^19.2.4", "webpack": "^5.59.0" } }, "sha512-zEhkWv6RhXDctC2N7yEUHg3751nvFg81ydHj8LTTZuukF/IF1gcOKqqAL6Ds+kS5HtDVACYPik0IvzkgYXPhlQ=="],
"react-usestateref": ["react-usestateref@1.0.9", "", { "peerDependencies": { "react": ">16.0.0" } }, "sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"rsc-html-stream": ["rsc-html-stream@0.0.7", "", {}, "sha512-v9+fuY7usTgvXdNl8JmfXCvSsQbq2YMd60kOeeMIqCJFZ69fViuIxztHei7v5mlMMa2h3SqS+v44Gu9i9xANZA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"srvx": ["srvx@0.11.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-2n9t0YnAXPJjinytvxccNgs7rOA5gmE7Wowt/8Dy2dx2fDC6sBhfBpbrCvjYKALlVukPS/Uq3QwkolKNa7P/2Q=="],
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
"terser-webpack-plugin": ["terser-webpack-plugin@5.3.17", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"turbo-stream": ["turbo-stream@3.2.0", "", {}, "sha512-EK+bZ9UVrVh7JLslVFOV0GEMsociOqVOvEMTAd4ixMyffN5YNIEdLZWXUx5PJqDbTxSIBWw04HS9gCY4frYQDQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vite-plugin-top-level-await": ["vite-plugin-top-level-await@1.6.0", "", { "dependencies": { "@rollup/plugin-virtual": "^3.0.2", "@swc/core": "^1.12.14", "@swc/wasm": "^1.12.14", "uuid": "10.0.0" }, "peerDependencies": { "vite": ">=2.8" } }, "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww=="],
"vite-plugin-wasm": ["vite-plugin-wasm@3.5.0", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" } }, "sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ=="],
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
"waku": ["waku@1.0.0-alpha.5", "", { "dependencies": { "@hono/node-server": "^1.19.0", "@vitejs/plugin-react": "^5.1.0", "@vitejs/plugin-rsc": "^0.5.19", "dotenv": "^17.3.1", "hono": "^4.10.0", "magic-string": "^0.30.21", "picocolors": "^1.1.1", "rsc-html-stream": "^0.0.7", "vite": "^7.2.0" }, "peerDependencies": { "react": "~19.2.4", "react-dom": "~19.2.4", "react-server-dom-webpack": "~19.2.4" }, "bin": { "waku": "cli.js" } }, "sha512-DCKZD3grtGvuQdAQyIfqz9PkZoppto7Ysjf5yubD0oXcSQFJSlzLTz7APGuLiGf0NH67PBw1DFGxnV3Tf3J3BA=="],
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
"webpack": ["webpack@5.105.4", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.17", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw=="],
"webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"xstate": ["xstate@5.28.0", "", {}, "sha512-Iaqq6ZrUzqeUtA3hC5LQKZfR8ZLzEFTImMHJM3jWEdVvXWdKvvVLXZEiNQWm3SCA9ZbEou/n5rcsna1wb9t28A=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@vitejs/plugin-rsc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"vite-plugin-top-level-await/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
}
}

210
dist/public/assets/_id_-cxsEKuSj.js vendored Normal file
View File

@@ -0,0 +1,210 @@
import { r as a, j as e, __tla as __tla_0 } from "./index-I8cR0Dsm.js";
import { i as j, __tla as __tla_1 } from "./fullfat_bundler-C8o4MXnP.js";
import { u as v, a as N } from "./index-ChbWTil4.js";
import { h as p, a as w, u as C, v as k } from "./poll-BI_0HvZY.js";
let $;
let __tla = Promise.all([
(() => {
try {
return __tla_0;
} catch {
}
})(),
(() => {
try {
return __tla_1;
} catch {
}
})()
]).then(async () => {
const h = "p2p-poll-peer-id";
function g() {
return crypto.randomUUID();
}
function S() {
if (typeof globalThis.localStorage > "u") return g();
let s = localStorage.getItem(h);
return s || (s = g(), localStorage.setItem(h, s)), s;
}
function D() {
const s = v(), [r, d] = a.useState(false), l = a.useCallback(() => {
d(s.peers.length > 0);
}, [
s
]);
return a.useEffect(() => {
l();
const o = () => l();
return s.networkSubsystem.on("peer", o), s.networkSubsystem.on("peer-disconnected", o), () => {
s.networkSubsystem.off("peer", o), s.networkSubsystem.off("peer-disconnected", o);
};
}, [
s,
l
]), e.jsxs("div", {
className: "flex items-center gap-2 text-xs text-gray-500",
children: [
e.jsx("span", {
className: `inline-block h-2 w-2 rounded-full ${r ? "bg-green-500" : "bg-yellow-500"}`
}),
r ? "Connected" : "Connecting..."
]
});
}
function I({ docUrl: s }) {
const [r, d] = N(s), [l, o] = a.useState(""), [b, u] = a.useState(false), i = a.useMemo(() => S(), []);
if (!r) return e.jsx("div", {
className: "flex items-center justify-center py-12",
children: e.jsx("div", {
className: "text-gray-500",
children: "Loading poll..."
})
});
const c = r.options.reduce((t, n) => t + n.votes.length, 0), x = () => {
const t = l.trim();
t && (d((n) => w(n, t)), o(""));
}, f = (t) => {
p(r, t, i) ? d((n) => C(n, t, i)) : d((n) => k(n, t, i));
}, y = () => {
const t = window.location.href;
navigator.clipboard.writeText(t).then(() => {
u(true), setTimeout(() => u(false), 2e3);
});
};
return e.jsxs("div", {
className: "space-y-6",
children: [
e.jsxs("div", {
className: "flex items-start justify-between",
children: [
e.jsxs("div", {
children: [
e.jsx("h2", {
className: "text-2xl font-bold",
children: r.title
}),
e.jsxs("p", {
className: "mt-1 text-sm text-gray-500",
children: [
c,
" vote",
c !== 1 ? "s" : "",
" total"
]
})
]
}),
e.jsx(D, {})
]
}),
e.jsxs("div", {
className: "space-y-3",
children: [
r.options.map((t) => {
const n = p(r, t.id, i), m = c > 0 ? t.votes.length / c * 100 : 0;
return e.jsxs("div", {
className: "relative overflow-hidden rounded-lg border border-gray-200 bg-white",
children: [
e.jsx("div", {
className: "absolute inset-y-0 left-0 bg-blue-50 transition-all duration-300",
style: {
width: `${m}%`
}
}),
e.jsxs("div", {
className: "relative flex items-center justify-between px-4 py-3",
children: [
e.jsxs("button", {
onClick: () => f(t.id),
className: `flex items-center gap-2 text-left text-sm font-medium ${n ? "text-blue-600" : "text-gray-700 hover:text-blue-600"}`,
children: [
e.jsx("span", {
className: `flex h-5 w-5 items-center justify-center rounded border text-xs ${n ? "border-blue-600 bg-blue-600 text-white" : "border-gray-300"}`,
children: n ? "\u2713" : ""
}),
t.text
]
}),
e.jsxs("span", {
className: "text-sm text-gray-500",
children: [
t.votes.length,
" (",
m.toFixed(0),
"%)"
]
})
]
})
]
}, t.id);
}),
r.options.length === 0 && e.jsx("p", {
className: "py-4 text-center text-sm text-gray-400",
children: "No options yet. Add one below!"
})
]
}),
e.jsxs("div", {
className: "flex gap-2",
children: [
e.jsx("input", {
type: "text",
value: l,
onChange: (t) => o(t.target.value),
onKeyDown: (t) => t.key === "Enter" && x(),
placeholder: "Add an option...",
className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
}),
e.jsx("button", {
onClick: x,
className: "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
children: "Add"
})
]
}),
e.jsx("div", {
className: "border-t border-gray-200 pt-4",
children: e.jsx("button", {
onClick: y,
className: "rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50",
children: b ? "Copied!" : "Copy shareable link"
})
})
]
});
}
function U({ id: s }) {
const r = `automerge:${s}`;
return j(r) ? e.jsx("div", {
className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm",
children: e.jsx(I, {
docUrl: r
})
}) : e.jsxs("div", {
className: "rounded-lg border border-red-200 bg-red-50 p-6 text-center",
children: [
e.jsx("h2", {
className: "text-lg font-semibold text-red-800",
children: "Invalid Poll ID"
}),
e.jsx("p", {
className: "mt-2 text-sm text-red-600",
children: "The poll ID in the URL is not valid."
}),
e.jsx("a", {
href: "/",
className: "mt-4 inline-block text-sm text-blue-600 hover:underline",
children: "Go back home"
})
]
});
}
$ = {
default: U
};
});
export {
__tla,
$ as export_4af94835fa0f
};

49
dist/public/assets/_layout-C9jEQBWP.js vendored Normal file
View File

@@ -0,0 +1,49 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/repo-CjvObF6Z.js","assets/fullfat_bundler-C8o4MXnP.js"])))=>i.map(i=>d[i]);
import { r as a, _ as l, j as i, __tla as __tla_0 } from "./index-I8cR0Dsm.js";
import { R as p } from "./index-ChbWTil4.js";
let v;
let __tla = Promise.all([
(() => {
try {
return __tla_0;
} catch {
}
})()
]).then(async () => {
function f({ children: c }) {
const [o, d] = a.useState(null);
return a.useEffect(() => {
let n, e;
return l(async () => {
const { getRepo: t, cleanupRepo: r } = await import("./repo-CjvObF6Z.js").then(async (m) => {
await m.__tla;
return m;
});
return {
getRepo: t,
cleanupRepo: r
};
}, __vite__mapDeps([0,1])).then(({ getRepo: t, cleanupRepo: r }) => {
const s = t();
d(s), n = r, e = () => {
s.networkSubsystem.adapters.forEach((u) => u.disconnect());
}, window.addEventListener("beforeunload", e);
}), () => {
e && window.removeEventListener("beforeunload", e), n == null ? void 0 : n();
};
}, []), o ? i.jsx(p.Provider, {
value: o,
children: c
}) : i.jsx("div", {
className: "flex min-h-screen items-center justify-center text-gray-400",
children: "Loading..."
});
}
v = {
default: f
};
});
export {
__tla,
v as export_125820ecd802
};

File diff suppressed because one or more lines are too long

Binary file not shown.

6
dist/public/assets/client-CsOmnPdF.js vendored Normal file
View File

@@ -0,0 +1,6 @@
import { I as r, E as o, S as e, C as t } from "./index-I8cR0Dsm.js";
const n = { Children: t, Slot: e }, s = { ErrorBoundary: o, INTERNAL_ServerRouter: r };
export {
s as export_6d786e16fc6b,
n as export_847a2b1045ef
};

File diff suppressed because it is too large Load Diff

21
dist/public/assets/index-BQZM9uZj.js vendored Normal file
View File

@@ -0,0 +1,21 @@
import { r as i, j as t } from "./index-I8cR0Dsm.js";
import { u as m } from "./index-ChbWTil4.js";
import { c as x } from "./poll-BI_0HvZY.js";
function p() {
const [o, d] = i.useState(""), [r, c] = i.useState(""), u = m(), s = () => {
if (!o.trim()) return;
const e = u.create();
e.change((l) => {
const a = x(o.trim());
l.title = a.title, l.options = a.options;
}), window.location.href = `/poll/${e.documentId}`;
}, n = () => {
const e = r.trim();
e && (window.location.href = `/poll/${e}`);
};
return t.jsxs("div", { className: "space-y-8", children: [t.jsx("title", { children: "P2P Poll" }), t.jsxs("section", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: [t.jsx("h2", { className: "mb-4 text-lg font-semibold", children: "Create a New Poll" }), t.jsxs("div", { className: "flex gap-2", children: [t.jsx("input", { type: "text", value: o, onChange: (e) => d(e.target.value), onKeyDown: (e) => e.key === "Enter" && s(), placeholder: "Enter poll title...", className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" }), t.jsx("button", { onClick: s, className: "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700", children: "Create" })] })] }), t.jsxs("section", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: [t.jsx("h2", { className: "mb-4 text-lg font-semibold", children: "Join an Existing Poll" }), t.jsxs("div", { className: "flex gap-2", children: [t.jsx("input", { type: "text", value: r, onChange: (e) => c(e.target.value), onKeyDown: (e) => e.key === "Enter" && n(), placeholder: "Paste poll ID or link...", className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" }), t.jsx("button", { onClick: n, className: "rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700", children: "Join" })] })] })] });
}
const f = { default: p };
export {
f as export_5ce7e027532e
};

2543
dist/public/assets/index-ChbWTil4.js vendored Normal file

File diff suppressed because it is too large Load Diff

10668
dist/public/assets/index-I8cR0Dsm.js vendored Normal file

File diff suppressed because it is too large Load Diff

27
dist/public/assets/poll-BI_0HvZY.js vendored Normal file
View File

@@ -0,0 +1,27 @@
function u(n) {
return { title: n, options: [] };
}
function c(n, t) {
n.options.push({ id: crypto.randomUUID(), text: t, votes: [] });
}
function f(n, t, i) {
const o = n.options.find((s) => s.id === t);
return o ? o.votes.includes(i) : false;
}
function r(n, t, i) {
const o = n.options.find((s) => s.id === t);
o && (o.votes.includes(i) || o.votes.push(i));
}
function d(n, t, i) {
const o = n.options.find((e) => e.id === t);
if (!o) return;
const s = o.votes.indexOf(i);
s !== -1 && o.votes.splice(s, 1);
}
export {
c as a,
u as c,
f as h,
d as u,
r as v
};

1254
dist/public/assets/repo-CjvObF6Z.js vendored Normal file

File diff suppressed because it is too large Load Diff

13
dist/serve-node.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { INTERNAL_runFetch, unstable_serverEntry } from './server/index.js';
const { serve } = unstable_serverEntry;
const host = process.env.HOST;
const port = process.env.PORT;
serve({
fetch: (req, ...args) => INTERNAL_runFetch(process.env, req, ...args),
...(host ? { hostname: host } : {}),
...(port ? { port: parseInt(port, 10) } : {}),
});

View File

@@ -0,0 +1,54 @@
export default {
"bootstrapScriptContent": "import(\"/assets/index-I8cR0Dsm.js\")",
"clientReferenceDeps": {
"847a2b1045ef": {
"js": [
"/assets/client-CsOmnPdF.js",
"/assets/index-I8cR0Dsm.js"
],
"css": []
},
"6d786e16fc6b": {
"js": [
"/assets/client-CsOmnPdF.js",
"/assets/index-I8cR0Dsm.js"
],
"css": []
},
"5ce7e027532e": {
"js": [
"/assets/index-BQZM9uZj.js",
"/assets/index-I8cR0Dsm.js",
"/assets/index-ChbWTil4.js",
"/assets/poll-BI_0HvZY.js"
],
"css": []
},
"4af94835fa0f": {
"js": [
"/assets/_id_-cxsEKuSj.js",
"/assets/index-I8cR0Dsm.js",
"/assets/fullfat_bundler-C8o4MXnP.js",
"/assets/index-ChbWTil4.js",
"/assets/poll-BI_0HvZY.js"
],
"css": []
},
"125820ecd802": {
"js": [
"/assets/_layout-C9jEQBWP.js",
"/assets/index-I8cR0Dsm.js",
"/assets/index-ChbWTil4.js"
],
"css": []
}
},
"serverResources": {
"src/pages/_layout.tsx": {
"js": [],
"css": [
"/assets/_layout-ChjUcnq2.css"
]
}
}
}

1
dist/server/__waku_build_metadata.js vendored Normal file
View File

@@ -0,0 +1 @@
export const buildMetadata = new Map([["defineRouter:cachedElements","{}"],["defineRouter:path2moduleIds","{}"]]);

25
dist/server/assets/_id_-D3sxE6t8.js vendored Normal file
View File

@@ -0,0 +1,25 @@
import { r as registerClientReference, a as jsxRuntime_reactServerExports } from "./server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "../__vite_rsc_assets_manifest.js";
import "../__waku_build_metadata.js";
const PollPageClient = /* @__PURE__ */ registerClientReference(() => {
throw new Error("Unexpectedly client reference export 'default' is called on server");
}, "4af94835fa0f", "default");
async function PollPage({ id }) {
return /* @__PURE__ */ jsxRuntime_reactServerExports.jsx(PollPageClient, { id });
}
const getConfig = async () => {
return { render: "dynamic" };
};
export {
PollPage as default,
getConfig
};

File diff suppressed because one or more lines are too long

58
dist/server/assets/_layout-Cnq791cx.js vendored Normal file
View File

@@ -0,0 +1,58 @@
import assetsManifest from "../__vite_rsc_assets_manifest.js";
import { _ as __vite_rsc_react__, r as registerClientReference, a as jsxRuntime_reactServerExports } from "./server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "../__waku_build_metadata.js";
const RemoveDuplicateServerCss = void 0;
const Resources = /* @__PURE__ */ ((React, deps, RemoveDuplicateServerCss2, precedence) => {
return function Resources2() {
return React.createElement(React.Fragment, null, [...deps.css.map((href) => React.createElement("link", {
key: "css:" + href,
rel: "stylesheet",
...{ precedence },
href,
"data-rsc-css-href": href
})), RemoveDuplicateServerCss2]);
};
})(
__vite_rsc_react__,
assetsManifest.serverResources["src/pages/_layout.tsx"],
RemoveDuplicateServerCss,
"vite-rsc/importer-resources"
);
const Providers = /* @__PURE__ */ registerClientReference(() => {
throw new Error("Unexpectedly client reference export 'default' is called on server");
}, "125820ecd802", "default");
const Layout = ({ children }) => /* @__PURE__ */ jsxRuntime_reactServerExports.jsx(Providers, { children: /* @__PURE__ */ jsxRuntime_reactServerExports.jsxs("div", { className: "min-h-screen bg-gray-50 text-gray-900", children: [
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("header", { className: "border-b border-gray-200 bg-white px-4 py-3", children: /* @__PURE__ */ jsxRuntime_reactServerExports.jsx("h1", { className: "text-xl font-bold", children: /* @__PURE__ */ jsxRuntime_reactServerExports.jsx("a", { href: "/", className: "hover:text-blue-600", children: "P2P Poll" }) }) }),
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("main", { className: "mx-auto max-w-xl px-4 py-8", children })
] }) });
const $$default = Layout;
let getConfig = async () => {
return { render: "static" };
};
const $$wrap_$$default = /* @__PURE__ */ __vite_rsc_wrap_css__($$default, "Layout");
function __vite_rsc_wrap_css__(value, name) {
if (typeof value !== "function") return value;
function __wrapper(props) {
return __vite_rsc_react__.createElement(
__vite_rsc_react__.Fragment,
null,
__vite_rsc_react__.createElement(Resources),
__vite_rsc_react__.createElement(value, props)
);
}
Object.defineProperty(__wrapper, "name", { value: name });
return __wrapper;
}
export {
$$wrap_$$default as default,
getConfig
};

28
dist/server/assets/_root-_BZAZd6T.js vendored Normal file
View File

@@ -0,0 +1,28 @@
import { a as jsxRuntime_reactServerExports } from "./server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "../__vite_rsc_assets_manifest.js";
import "../__waku_build_metadata.js";
function Root({ children }) {
return /* @__PURE__ */ jsxRuntime_reactServerExports.jsxs("html", { lang: "en", children: [
/* @__PURE__ */ jsxRuntime_reactServerExports.jsxs("head", { children: [
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("meta", { charSet: "utf-8" }),
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" })
] }),
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("body", { children })
] });
}
const getConfig = () => {
return { render: "dynamic" };
};
export {
Root as default,
getConfig
};

25
dist/server/assets/index-BU_VRdfq.js vendored Normal file
View File

@@ -0,0 +1,25 @@
import { r as registerClientReference, a as jsxRuntime_reactServerExports } from "./server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "../__vite_rsc_assets_manifest.js";
import "../__waku_build_metadata.js";
const HomeClient = /* @__PURE__ */ registerClientReference(() => {
throw new Error("Unexpectedly client reference export 'default' is called on server");
}, "5ce7e027532e", "default");
async function HomePage() {
return /* @__PURE__ */ jsxRuntime_reactServerExports.jsx(HomeClient, {});
}
const getConfig = async () => {
return { render: "dynamic" };
};
export {
HomePage as default,
getConfig
};

File diff suppressed because it is too large Load Diff

54
dist/server/build.js vendored Normal file
View File

@@ -0,0 +1,54 @@
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import { I as INTERNAL_setAllEnv, s as serverEntry, j as joinPath, __tla as __tla_0 } from "./assets/server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "./__vite_rsc_assets_manifest.js";
import "./__waku_build_metadata.js";
let INTERNAL_runBuild;
let __tla = Promise.all([
(() => {
try {
return __tla_0;
} catch {
}
})()
]).then(async () => {
function resolveModuleId(moduleId, rootDir) {
if (moduleId.startsWith("file://")) {
return moduleId;
}
if (moduleId.startsWith("/")) {
return pathToFileURL(joinPath(rootDir, moduleId.slice(1))).href;
}
const require2 = createRequire(joinPath(rootDir, "DUMMY.js"));
const resolved = require2.resolve(moduleId);
return pathToFileURL(resolved).href;
}
INTERNAL_runBuild = async function({ rootDir, emitFile }) {
INTERNAL_setAllEnv(process.env);
let build = serverEntry.build;
for (const enhancer of serverEntry.buildEnhancers || []) {
const moduleId = resolveModuleId(enhancer, rootDir);
const mod = await import(moduleId).then(async (m) => {
await m.__tla;
return m;
});
build = await mod.default(build);
}
await build({
emitFile
}, serverEntry.buildOptions || {});
};
});
export {
INTERNAL_runBuild,
__tla
};

22
dist/server/index.js vendored Normal file
View File

@@ -0,0 +1,22 @@
import { s as serverEntry, I as INTERNAL_setAllEnv } from "./assets/server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "./__vite_rsc_assets_manifest.js";
import "./__waku_build_metadata.js";
async function INTERNAL_runFetch(env, req, ...args) {
INTERNAL_setAllEnv(env);
return serverEntry.fetch(req, ...args);
}
const entry_server = serverEntry.defaultExport;
export {
INTERNAL_runFetch,
entry_server as default,
serverEntry as unstable_serverEntry
};

View File

@@ -0,0 +1,54 @@
export default {
"bootstrapScriptContent": "import(\"/assets/index-I8cR0Dsm.js\")",
"clientReferenceDeps": {
"847a2b1045ef": {
"js": [
"/assets/client-CsOmnPdF.js",
"/assets/index-I8cR0Dsm.js"
],
"css": []
},
"6d786e16fc6b": {
"js": [
"/assets/client-CsOmnPdF.js",
"/assets/index-I8cR0Dsm.js"
],
"css": []
},
"5ce7e027532e": {
"js": [
"/assets/index-BQZM9uZj.js",
"/assets/index-I8cR0Dsm.js",
"/assets/index-ChbWTil4.js",
"/assets/poll-BI_0HvZY.js"
],
"css": []
},
"4af94835fa0f": {
"js": [
"/assets/_id_-cxsEKuSj.js",
"/assets/index-I8cR0Dsm.js",
"/assets/fullfat_bundler-C8o4MXnP.js",
"/assets/index-ChbWTil4.js",
"/assets/poll-BI_0HvZY.js"
],
"css": []
},
"125820ecd802": {
"js": [
"/assets/_layout-C9jEQBWP.js",
"/assets/index-I8cR0Dsm.js",
"/assets/index-ChbWTil4.js"
],
"css": []
}
},
"serverResources": {
"src/pages/_layout.tsx": {
"js": [],
"css": [
"/assets/_layout-ChjUcnq2.css"
]
}
}
}

192
dist/server/ssr/assets/_id_-BAEItp57.js vendored Normal file
View File

@@ -0,0 +1,192 @@
import { r as reactExports, j as jsxRuntimeExports } from "../index.js";
import { i as isValidAutomergeUrl } from "./fullfat_node-75TjwUrn.js";
import { u as useRepo, a as useDocument } from "./index-BSpyO9eA.js";
import { h as hasVoted, a as addOption, u as unvote, v as vote } from "./poll-R5-eIJ_b.js";
import "../__vite_rsc_assets_manifest.js";
import "node:async_hooks";
import "tty";
import "util";
import "os";
import "node:crypto";
import "crypto";
import "module";
import "fs";
const PEER_ID_KEY = "p2p-poll-peer-id";
function generateUUID() {
return crypto.randomUUID();
}
function getPeerId() {
if (typeof globalThis.localStorage === "undefined") {
return generateUUID();
}
let id = localStorage.getItem(PEER_ID_KEY);
if (!id) {
id = generateUUID();
localStorage.setItem(PEER_ID_KEY, id);
}
return id;
}
function ConnectionStatus() {
const repo = useRepo();
const [connected, setConnected] = reactExports.useState(false);
const updateStatus = reactExports.useCallback(() => {
setConnected(repo.peers.length > 0);
}, [repo]);
reactExports.useEffect(() => {
updateStatus();
const onChange = () => updateStatus();
repo.networkSubsystem.on("peer", onChange);
repo.networkSubsystem.on("peer-disconnected", onChange);
return () => {
repo.networkSubsystem.off("peer", onChange);
repo.networkSubsystem.off("peer-disconnected", onChange);
};
}, [repo, updateStatus]);
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-xs text-gray-500", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"span",
{
className: `inline-block h-2 w-2 rounded-full ${connected ? "bg-green-500" : "bg-yellow-500"}`
}
),
connected ? "Connected" : "Connecting..."
] });
}
function PollView({ docUrl }) {
const [doc, changeDoc] = useDocument(docUrl);
const [newOption, setNewOption] = reactExports.useState("");
const [copied, setCopied] = reactExports.useState(false);
const peerId = reactExports.useMemo(() => getPeerId(), []);
if (!doc) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-500", children: "Loading poll..." }) });
}
const totalVotes = doc.options.reduce((sum, o) => sum + o.votes.length, 0);
const handleAddOption = () => {
const text = newOption.trim();
if (!text) return;
changeDoc((d) => addOption(d, text));
setNewOption("");
};
const handleVote = (optionId) => {
if (hasVoted(doc, optionId, peerId)) {
changeDoc((d) => unvote(d, optionId, peerId));
} else {
changeDoc((d) => vote(d, optionId, peerId));
}
};
const handleCopy = () => {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2e3);
});
};
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-2xl font-bold", children: doc.title }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "mt-1 text-sm text-gray-500", children: [
totalVotes,
" vote",
totalVotes !== 1 ? "s" : "",
" total"
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(ConnectionStatus, {})
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
doc.options.map((option) => {
const voted = hasVoted(doc, option.id, peerId);
const pct = totalVotes > 0 ? option.votes.length / totalVotes * 100 : 0;
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
"div",
{
className: "relative overflow-hidden rounded-lg border border-gray-200 bg-white",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
className: "absolute inset-y-0 left-0 bg-blue-50 transition-all duration-300",
style: { width: `${pct}%` }
}
),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center justify-between px-4 py-3", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(
"button",
{
onClick: () => handleVote(option.id),
className: `flex items-center gap-2 text-left text-sm font-medium ${voted ? "text-blue-600" : "text-gray-700 hover:text-blue-600"}`,
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"span",
{
className: `flex h-5 w-5 items-center justify-center rounded border text-xs ${voted ? "border-blue-600 bg-blue-600 text-white" : "border-gray-300"}`,
children: voted ? "\u2713" : ""
}
),
option.text
]
}
),
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-sm text-gray-500", children: [
option.votes.length,
" (",
pct.toFixed(0),
"%)"
] })
] })
]
},
option.id
);
}),
doc.options.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "py-4 text-center text-sm text-gray-400", children: "No options yet. Add one below!" })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"input",
{
type: "text",
value: newOption,
onChange: (e) => setNewOption(e.target.value),
onKeyDown: (e) => e.key === "Enter" && handleAddOption(),
placeholder: "Add an option...",
className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
onClick: handleAddOption,
className: "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
children: "Add"
}
)
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-gray-200 pt-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
onClick: handleCopy,
className: "rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50",
children: copied ? "Copied!" : "Copy shareable link"
}
) })
] });
}
function PollPageClient({ id }) {
const automergeUrl = `automerge:${id}`;
if (!isValidAutomergeUrl(automergeUrl)) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-lg border border-red-200 bg-red-50 p-6 text-center", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg font-semibold text-red-800", children: "Invalid Poll ID" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-2 text-sm text-red-600", children: "The poll ID in the URL is not valid." }),
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/", className: "mt-4 inline-block text-sm text-blue-600 hover:underline", children: "Go back home" })
] });
}
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx(PollView, { docUrl: automergeUrl }) });
}
const export_4af94835fa0f = {
default: PollPageClient
};
export {
export_4af94835fa0f
};

View File

@@ -0,0 +1,53 @@
import { r as reactExports, j as jsxRuntimeExports, __tla as __tla_0 } from "../index.js";
import { R as RepoContext } from "./index-BSpyO9eA.js";
import "../__vite_rsc_assets_manifest.js";
import "node:async_hooks";
let export_125820ecd802;
let __tla = Promise.all([
(() => {
try {
return __tla_0;
} catch {
}
})()
]).then(async () => {
function Providers({ children }) {
const [repo, setRepo] = reactExports.useState(null);
reactExports.useEffect(() => {
let cleanup;
let handleBeforeUnload;
import("./repo-zy9lifAg.js").then(({ getRepo, cleanupRepo }) => {
const r = getRepo();
setRepo(r);
cleanup = cleanupRepo;
handleBeforeUnload = () => {
r.networkSubsystem.adapters.forEach((adapter) => adapter.disconnect());
};
window.addEventListener("beforeunload", handleBeforeUnload);
});
return () => {
if (handleBeforeUnload) {
window.removeEventListener("beforeunload", handleBeforeUnload);
}
cleanup == null ? void 0 : cleanup();
};
}, []);
if (!repo) {
return jsxRuntimeExports.jsx("div", {
className: "flex min-h-screen items-center justify-center text-gray-400",
children: "Loading..."
});
}
return jsxRuntimeExports.jsx(RepoContext.Provider, {
value: repo,
children
});
}
export_125820ecd802 = {
default: Providers
};
});
export {
__tla,
export_125820ecd802
};

View File

@@ -0,0 +1,104 @@
import { j as jsxRuntimeExports, S as Slot, r as reactExports, C as Children } from "../index.js";
import "../__vite_rsc_assets_manifest.js";
import "node:async_hooks";
const RouterContext = /* @__PURE__ */ reactExports.createContext(null);
const notAvailableInServer = (name) => () => {
throw new Error(`${name} is not in the server`);
};
function renderError(message) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("html", {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("head", {
children: /* @__PURE__ */ jsxRuntimeExports.jsx("title", {
children: "Unhandled Error"
})
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("body", {
style: {
height: "100vh",
display: "flex",
flexDirection: "column",
placeContent: "center",
placeItems: "center",
fontSize: "16px",
margin: 0
},
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h1", {
children: "Caught an unexpected error"
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", {
children: [
"Error: ",
message
]
})
]
})
]
});
}
class ErrorBoundary extends reactExports.Component {
constructor(props) {
super(props);
this.state = {};
}
static getDerivedStateFromError(error) {
return {
error
};
}
render() {
if ("error" in this.state) {
if (this.state.error instanceof Error) {
return renderError(this.state.error.message);
}
return renderError(String(this.state.error));
}
return this.props.children;
}
}
const getRouteSlotId = (path) => "route:" + path;
const MOCK_ROUTE_CHANGE_LISTENER = {
on: () => notAvailableInServer("routeChange:on"),
off: () => notAvailableInServer("routeChange:off")
};
function INTERNAL_ServerRouter({ route, httpstatus }) {
const routeElement = /* @__PURE__ */ jsxRuntimeExports.jsx(Slot, {
id: getRouteSlotId(route.path)
});
const rootElement = /* @__PURE__ */ jsxRuntimeExports.jsxs(Slot, {
id: "root",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("meta", {
name: "httpstatus",
content: `${httpstatus}`
}),
routeElement
]
});
return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, {
children: /* @__PURE__ */ jsxRuntimeExports.jsx(RouterContext, {
value: {
route,
changeRoute: notAvailableInServer("changeRoute"),
prefetchRoute: notAvailableInServer("prefetchRoute"),
routeChangeEvents: MOCK_ROUTE_CHANGE_LISTENER,
fetchingSlices: /* @__PURE__ */ new Set()
},
children: rootElement
})
});
}
const export_847a2b1045ef = {
Children,
Slot
};
const export_6d786e16fc6b = {
ErrorBoundary,
INTERNAL_ServerRouter
};
export {
export_6d786e16fc6b,
export_847a2b1045ef
};

File diff suppressed because it is too large Load Diff

5592
dist/server/ssr/assets/index-BSpyO9eA.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,82 @@
import { r as reactExports, j as jsxRuntimeExports } from "../index.js";
import { u as useRepo } from "./index-BSpyO9eA.js";
import { c as createPoll } from "./poll-R5-eIJ_b.js";
import "../__vite_rsc_assets_manifest.js";
import "node:async_hooks";
function HomeClient() {
const [title, setTitle] = reactExports.useState("");
const [joinId, setJoinId] = reactExports.useState("");
const repo = useRepo();
const handleCreate = () => {
if (!title.trim()) return;
const handle = repo.create();
handle.change((doc) => {
const poll = createPoll(title.trim());
doc.title = poll.title;
doc.options = poll.options;
});
window.location.href = `/poll/${handle.documentId}`;
};
const handleJoin = () => {
const id = joinId.trim();
if (!id) return;
window.location.href = `/poll/${id}`;
};
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-8", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: "P2P Poll" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "mb-4 text-lg font-semibold", children: "Create a New Poll" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"input",
{
type: "text",
value: title,
onChange: (e) => setTitle(e.target.value),
onKeyDown: (e) => e.key === "Enter" && handleCreate(),
placeholder: "Enter poll title...",
className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
onClick: handleCreate,
className: "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
children: "Create"
}
)
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "mb-4 text-lg font-semibold", children: "Join an Existing Poll" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"input",
{
type: "text",
value: joinId,
onChange: (e) => setJoinId(e.target.value),
onKeyDown: (e) => e.key === "Enter" && handleJoin(),
placeholder: "Paste poll ID or link...",
className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
onClick: handleJoin,
className: "rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700",
children: "Join"
}
)
] })
] })
] });
}
const export_5ce7e027532e = {
default: HomeClient
};
export {
export_5ce7e027532e
};

39
dist/server/ssr/assets/poll-R5-eIJ_b.js vendored Normal file
View File

@@ -0,0 +1,39 @@
function createPoll(title) {
return {
title,
options: []
};
}
function addOption(poll, text) {
poll.options.push({
id: crypto.randomUUID(),
text,
votes: []
});
}
function hasVoted(poll, optionId, peerId) {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return false;
return option.votes.includes(peerId);
}
function vote(poll, optionId, peerId) {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return;
if (option.votes.includes(peerId)) return;
option.votes.push(peerId);
}
function unvote(poll, optionId, peerId) {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return;
const idx = option.votes.indexOf(peerId);
if (idx !== -1) {
option.votes.splice(idx, 1);
}
}
export {
addOption as a,
createPoll as c,
hasVoted as h,
unvote as u,
vote as v
};

5494
dist/server/ssr/assets/repo-zy9lifAg.js vendored Normal file

File diff suppressed because it is too large Load Diff

11296
dist/server/ssr/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>P2P Poll</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1473
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,32 @@
{ {
"name": "p2p-poll-app", "name": "p2p-poll-app",
"version": "0.1.0", "version": "0.0.0",
"private": true,
"type": "module", "type": "module",
"private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "waku dev",
"build": "tsc --noEmit && vite build", "build": "waku build",
"preview": "vite preview" "start": "waku start"
}, },
"dependencies": { "dependencies": {
"y-indexeddb": "^9.0.12", "@automerge/automerge": "^3.2.4",
"y-webrtc": "^10.3.0", "@automerge/automerge-repo": "^2.5.3",
"yjs": "^13.6.27" "@automerge/automerge-repo-network-broadcastchannel": "^2.5.3",
"@automerge/automerge-repo-network-websocket": "^2.5.3",
"@automerge/automerge-repo-react-hooks": "^2.5.3",
"@automerge/automerge-repo-storage-indexeddb": "^2.5.3",
"@tailwindcss/vite": "^4.2.1",
"react": "~19.2.4",
"react-dom": "~19.2.4",
"react-server-dom-webpack": "~19.2.4",
"tailwindcss": "^4.2.1",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"waku": "1.0.0-alpha.5"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2", "@types/react": "^19.2.14",
"vite": "^7.1.5" "@types/react-dom": "^19.2.3",
"typescript": "^5.9.3"
} }
} }

View File

@@ -1,107 +0,0 @@
import { getUserId } from "./identity";
import { renderApp } from "./render";
import { addOption, castVote, createViewModel } from "./state";
import { initSync } from "./sync";
const ROOM_PARAM = "room";
function createRoomId() {
const seed = typeof crypto.randomUUID === "function"
? crypto.randomUUID().slice(0, 8)
: Math.random().toString(36).slice(2, 10);
return `poll-${seed}`;
}
function ensureRoomId() {
const url = new URL(window.location.href);
let roomId = url.searchParams.get(ROOM_PARAM)?.trim();
if (!roomId) {
roomId = createRoomId();
url.searchParams.set(ROOM_PARAM, roomId);
window.history.replaceState({}, "", url);
}
return roomId;
}
export function initApp(container: HTMLElement) {
const roomId = ensureRoomId();
const userId = getUserId();
const sync = initSync(roomId);
let feedbackMessage = "Ready to collaborate.";
let feedbackTimer: number | undefined;
const setFeedbackMessage = (message: string) => {
feedbackMessage = message;
render();
if (feedbackTimer) {
window.clearTimeout(feedbackTimer);
}
feedbackTimer = window.setTimeout(() => {
feedbackMessage = "Ready to collaborate.";
render();
}, 2400);
};
const render = () => {
const viewModel = createViewModel({
meta: sync.meta,
options: sync.options,
votes: sync.votes,
roomId,
shareUrl: window.location.href,
connectionStatus: sync.getConnectionStatus(),
userId,
});
renderApp(container, viewModel, feedbackMessage, {
onSubmitOption: (label) => {
const result = addOption(sync.options, label, userId);
if (!result.ok) {
setFeedbackMessage(result.error);
return;
}
setFeedbackMessage("Option added and syncing.");
},
onVote: (optionId) => {
castVote(sync.votes, userId, optionId);
setFeedbackMessage("Vote updated.");
},
onCopyLink: async () => {
try {
await navigator.clipboard.writeText(window.location.href);
setFeedbackMessage("Room link copied.");
} catch {
setFeedbackMessage("Could not copy the link in this browser.");
}
},
});
};
const rerender = () => render();
const observe = () => rerender();
sync.doc.on("update", observe);
sync.persistence.once("synced", rerender);
sync.provider.on("status", rerender);
window.addEventListener("online", rerender);
window.addEventListener("offline", rerender);
render();
return () => {
if (feedbackTimer) {
window.clearTimeout(feedbackTimer);
}
sync.doc.off("update", observe);
sync.provider.off("status", rerender);
window.removeEventListener("online", rerender);
window.removeEventListener("offline", rerender);
sync.destroy();
};
}

View File

@@ -0,0 +1,37 @@
"use client";
import { useRepo } from "@automerge/automerge-repo-react-hooks";
import { useEffect, useState, useCallback } from "react";
export default function ConnectionStatus() {
const repo = useRepo();
const [connected, setConnected] = useState(false);
const updateStatus = useCallback(() => {
setConnected(repo ? repo.peers.length > 0 : false);
}, [repo]);
useEffect(() => {
updateStatus();
const onChange = () => updateStatus();
repo.networkSubsystem.on("peer", onChange);
repo.networkSubsystem.on("peer-disconnected", onChange);
return () => {
repo.networkSubsystem.off("peer", onChange);
repo.networkSubsystem.off("peer-disconnected", onChange);
};
}, [repo, updateStatus]);
return (
<div className="flex items-center gap-2 text-xs text-gray-500">
<span
className={`inline-block h-2 w-2 rounded-full ${
connected ? "bg-green-500" : "bg-yellow-500"
}`}
/>
{connected ? "Connected" : "Connecting..."}
</div>
);
}

View File

@@ -0,0 +1,79 @@
"use client";
import { useState } from "react";
import { useRouter } from "waku";
import { useRepo } from "@automerge/automerge-repo-react-hooks";
import { createPoll } from "../lib/poll.js";
import type { Poll } from "../lib/types.js";
export default function HomeClient() {
const [title, setTitle] = useState("");
const [joinId, setJoinId] = useState("");
const repo = useRepo();
const router = useRouter();
const handleCreate = () => {
if (!title.trim()) return;
const handle = repo.create<Poll>();
handle.change((doc) => {
const poll = createPoll(title.trim());
doc.title = poll.title;
doc.options = poll.options;
});
router.push(`/poll/${handle.documentId}`);
};
const handleJoin = () => {
const id = joinId.trim();
if (!id) return;
router.push(`/poll/${id}`);
};
return (
<div className="space-y-8">
<title>P2P Poll</title>
{/* Create Poll */}
<section className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="mb-4 text-lg font-semibold">Create a New Poll</h2>
<div className="flex gap-2">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
placeholder="Enter poll title..."
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
onClick={handleCreate}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Create
</button>
</div>
</section>
{/* Join Poll */}
<section className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="mb-4 text-lg font-semibold">Join an Existing Poll</h2>
<div className="flex gap-2">
<input
type="text"
value={joinId}
onChange={(e) => setJoinId(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleJoin()}
placeholder="Paste poll ID or link..."
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
onClick={handleJoin}
className="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700"
>
Join
</button>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,28 @@
"use client";
import { isValidAutomergeUrl } from "@automerge/automerge-repo";
import PollView from "./PollView.js";
export default function PollPageClient({ id }: { id: string }) {
const automergeUrl = `automerge:${id}` as const;
if (!isValidAutomergeUrl(automergeUrl)) {
return (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-center">
<h2 className="text-lg font-semibold text-red-800">Invalid Poll ID</h2>
<p className="mt-2 text-sm text-red-600">
The poll ID in the URL is not valid.
</p>
<a href="/" className="mt-4 inline-block text-sm text-blue-600 hover:underline">
Go back home
</a>
</div>
);
}
return (
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<PollView docUrl={automergeUrl} />
</div>
);
}

140
src/components/PollView.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client";
import { useState, useMemo } from "react";
import type { AutomergeUrl } from "@automerge/automerge-repo";
import { useDocument } from "@automerge/automerge-repo-react-hooks";
import { addOption, vote, unvote, hasVoted } from "../lib/poll.js";
import { getPeerId } from "../lib/peer.js";
import type { Poll } from "../lib/types.js";
import ConnectionStatus from "./ConnectionStatus.js";
export default function PollView({ docUrl }: { docUrl: AutomergeUrl }) {
const [doc, changeDoc] = useDocument<Poll>(docUrl);
const [newOption, setNewOption] = useState("");
const [copied, setCopied] = useState(false);
const peerId = useMemo(() => getPeerId(), []);
if (!doc) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-gray-500">Loading poll...</div>
</div>
);
}
const totalVotes = doc.options.reduce((sum, o) => sum + o.votes.length, 0);
const handleAddOption = () => {
const text = newOption.trim();
if (!text) return;
changeDoc((d) => addOption(d, text));
setNewOption("");
};
const handleVote = (optionId: string) => {
if (hasVoted(doc, optionId, peerId)) {
changeDoc((d) => unvote(d, optionId, peerId));
} else {
changeDoc((d) => vote(d, optionId, peerId));
}
};
const handleCopy = () => {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(() => {});
};
return (
<div className="space-y-6">
<div className="flex items-start justify-between">
<div>
<h2 className="text-2xl font-bold">{doc.title}</h2>
<p className="mt-1 text-sm text-gray-500">
{totalVotes} vote{totalVotes !== 1 ? "s" : ""} total
</p>
</div>
<ConnectionStatus />
</div>
{/* Options list */}
<div className="space-y-3">
{doc.options.map((option) => {
const voted = hasVoted(doc, option.id, peerId);
const pct = totalVotes > 0 ? (option.votes.length / totalVotes) * 100 : 0;
return (
<div
key={option.id}
className="relative overflow-hidden rounded-lg border border-gray-200 bg-white"
>
{/* Vote bar */}
<div
className="absolute inset-y-0 left-0 bg-blue-50 transition-all duration-300"
style={{ width: `${pct}%` }}
/>
<div className="relative flex items-center justify-between px-4 py-3">
<button
onClick={() => handleVote(option.id)}
className={`flex items-center gap-2 text-left text-sm font-medium ${
voted ? "text-blue-600" : "text-gray-700 hover:text-blue-600"
}`}
>
<span
className={`flex h-5 w-5 items-center justify-center rounded border text-xs ${
voted
? "border-blue-600 bg-blue-600 text-white"
: "border-gray-300"
}`}
>
{voted ? "\u2713" : ""}
</span>
{option.text}
</button>
<span className="text-sm text-gray-500">
{option.votes.length} ({pct.toFixed(0)}%)
</span>
</div>
</div>
);
})}
{doc.options.length === 0 && (
<p className="py-4 text-center text-sm text-gray-400">
No options yet. Add one below!
</p>
)}
</div>
{/* Add option form */}
<div className="flex gap-2">
<input
type="text"
value={newOption}
onChange={(e) => setNewOption(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAddOption()}
placeholder="Add an option..."
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
onClick={handleAddOption}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Add
</button>
</div>
{/* Share */}
<div className="border-t border-gray-200 pt-4">
<button
onClick={handleCopy}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50"
>
{copied ? "Copied!" : "Copy shareable link"}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,45 @@
"use client";
import { type ReactNode, useState, useEffect } from "react";
import { RepoContext } from "@automerge/automerge-repo-react-hooks";
import type { Repo } from "@automerge/automerge-repo";
export default function Providers({ children }: { children: ReactNode }) {
const [repo, setRepo] = useState<Repo | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cleanup: (() => void) | undefined;
let handleBeforeUnload: (() => void) | undefined;
import("../lib/repo.js").then(({ getRepo, cleanupRepo }) => {
const r = getRepo();
setRepo(r);
cleanup = cleanupRepo;
handleBeforeUnload = () => {
r.networkSubsystem.adapters.forEach((adapter) => adapter.disconnect());
};
window.addEventListener("beforeunload", handleBeforeUnload);
}).catch(() => {
setError("Failed to initialize. Please refresh the page.");
});
return () => {
if (handleBeforeUnload) {
window.removeEventListener("beforeunload", handleBeforeUnload);
}
cleanup?.();
};
}, []);
if (error) {
return <div className="flex min-h-screen items-center justify-center text-red-500">{error}</div>;
}
if (!repo) {
return <div className="flex min-h-screen items-center justify-center text-gray-400">Loading...</div>;
}
return <RepoContext.Provider value={repo}>{children}</RepoContext.Provider>;
}

View File

@@ -1,20 +0,0 @@
const USER_ID_KEY = "p2p-poll:user-id";
function createUserId() {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `user-${Math.random().toString(36).slice(2, 10)}`;
}
export function getUserId() {
const existing = localStorage.getItem(USER_ID_KEY);
if (existing) {
return existing;
}
const next = createUserId();
localStorage.setItem(USER_ID_KEY, next);
return next;
}

View File

@@ -0,0 +1,56 @@
import { describe, test, expect, beforeEach } from "bun:test";
// Mock localStorage
const storage = new Map<string, string>();
const localStorageMock = {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key),
clear: () => storage.clear(),
get length() {
return storage.size;
},
key: (_index: number) => null,
};
Object.defineProperty(globalThis, "localStorage", {
value: localStorageMock,
writable: true,
});
// Import after mock is set up
const { getPeerId } = await import("../peer.js");
describe("getPeerId", () => {
beforeEach(() => {
storage.clear();
});
test("persists and returns the same ID on subsequent calls", () => {
const id1 = getPeerId();
const id2 = getPeerId();
expect(id1).toBe(id2);
});
test("stores the ID in localStorage", () => {
const id = getPeerId();
expect(storage.get("p2p-poll-peer-id")).toBe(id);
});
test("returns a new UUID each time when localStorage is unavailable", () => {
const saved = globalThis.localStorage;
// @ts-expect-error — deliberately removing localStorage to test fallback
globalThis.localStorage = undefined;
const id1 = getPeerId();
const id2 = getPeerId();
expect(id1).not.toBe(id2);
expect(id1).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
);
// Restore
globalThis.localStorage = saved;
});
});

View File

@@ -0,0 +1,94 @@
import { describe, test, expect } from "bun:test";
import { createPoll, addOption, vote, unvote, hasVoted } from "../poll.js";
describe("vote", () => {
test("prevents double-vote (no duplicate entries)", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
vote(poll, optionId, "peer-1");
vote(poll, optionId, "peer-1");
expect(poll.options[0]!.votes).toEqual(["peer-1"]);
expect(poll.options[0]!.votes).toHaveLength(1);
});
test("allows different peers to vote", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
vote(poll, optionId, "peer-1");
vote(poll, optionId, "peer-2");
expect(poll.options[0]!.votes).toEqual(["peer-1", "peer-2"]);
});
test("is a no-op for non-existent option ID", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
vote(poll, "non-existent-id", "peer-1");
expect(poll.options[0]!.votes).toEqual([]);
});
test("voting on one option does not affect another option", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
addOption(poll, "Blue");
const redId = poll.options[0]!.id;
const blueId = poll.options[1]!.id;
vote(poll, redId, "peer-1");
expect(poll.options[0]!.votes).toEqual(["peer-1"]);
expect(poll.options[1]!.votes).toEqual([]);
vote(poll, blueId, "peer-2");
expect(poll.options[0]!.votes).toEqual(["peer-1"]);
expect(poll.options[1]!.votes).toEqual(["peer-2"]);
});
});
describe("unvote", () => {
test("removes peer ID from votes array", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
vote(poll, optionId, "peer-1");
unvote(poll, optionId, "peer-1");
expect(poll.options[0]!.votes).toEqual([]);
});
test("does nothing if peer has not voted", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
unvote(poll, optionId, "peer-1");
expect(poll.options[0]!.votes).toEqual([]);
});
});
describe("hasVoted", () => {
test("returns true when peer has voted", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
vote(poll, optionId, "peer-1");
expect(hasVoted(poll, optionId, "peer-1")).toBe(true);
});
test("returns false for non-existent option", () => {
const poll = createPoll("Test");
expect(hasVoted(poll, "non-existent", "peer-1")).toBe(false);
});
});

22
src/lib/peer.ts Normal file
View File

@@ -0,0 +1,22 @@
const PEER_ID_KEY = "p2p-poll-peer-id";
/** Generate a UUID v4 */
function generateUUID(): string {
return crypto.randomUUID();
}
/**
* Get or create a persistent peer ID.
* Stored in localStorage so each browser tab/device gets a stable identity.
*/
export function getPeerId(): string {
if (typeof globalThis.localStorage === "undefined") {
return generateUUID();
}
let id = localStorage.getItem(PEER_ID_KEY);
if (!id) {
id = generateUUID();
localStorage.setItem(PEER_ID_KEY, id);
}
return id;
}

43
src/lib/poll.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { Poll } from "./types.js";
/** Create a new poll document with the given title and no options */
export function createPoll(title: string): Poll {
return {
title,
options: [],
};
}
/** Add a new option to the poll */
export function addOption(poll: Poll, text: string): void {
poll.options.push({
id: crypto.randomUUID(),
text,
votes: [],
});
}
/** Check if a peer has already voted on an option */
export function hasVoted(poll: Poll, optionId: string, peerId: string): boolean {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return false;
return option.votes.includes(peerId);
}
/** Add a vote for the given option. Prevents double-voting. */
export function vote(poll: Poll, optionId: string, peerId: string): void {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return;
if (option.votes.includes(peerId)) return;
option.votes.push(peerId);
}
/** Remove a vote from the given option */
export function unvote(poll: Poll, optionId: string, peerId: string): void {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return;
const idx = option.votes.indexOf(peerId);
if (idx !== -1) {
option.votes.splice(idx, 1);
}
}

29
src/lib/repo.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Repo } from "@automerge/automerge-repo";
import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel";
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
let repo: Repo | null = null;
/** Get or create the singleton Automerge Repo instance (browser-only) */
export function getRepo(): Repo {
if (repo) return repo;
repo = new Repo({
network: [
new BrowserWebSocketClientAdapter("wss://sync.automerge.org"),
new BroadcastChannelNetworkAdapter(),
],
storage: new IndexedDBStorageAdapter("p2p-poll-app"),
});
return repo;
}
/** Shut down the repo and reset the singleton */
export function cleanupRepo(): void {
if (repo) {
repo.shutdown();
repo = null;
}
}

12
src/lib/types.ts Normal file
View File

@@ -0,0 +1,12 @@
/** A single poll option that users can vote on */
export interface PollOption {
id: string;
text: string;
votes: string[]; // Array of peer IDs
}
/** The shared poll document stored in Automerge */
export interface Poll {
title: string;
options: PollOption[];
}

View File

@@ -1,11 +0,0 @@
import "./styles.css";
import { initApp } from "./app";
const container = document.querySelector<HTMLElement>("#app");
if (!container) {
throw new Error("App container not found.");
}
initApp(container);

27
src/pages.gen.ts Normal file
View File

@@ -0,0 +1,27 @@
// deno-fmt-ignore-file
// biome-ignore format: generated types do not need formatting
// prettier-ignore
import type { PathsForPages, GetConfigResponse } from 'waku/router';
// prettier-ignore
import type { getConfig as File_Root_getConfig } from './pages/_root';
// prettier-ignore
import type { getConfig as File_Index_getConfig } from './pages/index';
// prettier-ignore
import type { getConfig as File_PollId_getConfig } from './pages/poll/[id]';
// prettier-ignore
type Page =
| ({ path: '/_root' } & GetConfigResponse<typeof File_Root_getConfig>)
| ({ path: '/' } & GetConfigResponse<typeof File_Index_getConfig>)
| ({ path: '/poll/[id]' } & GetConfigResponse<typeof File_PollId_getConfig>);
// prettier-ignore
declare module 'waku/router' {
interface RouteConfig {
paths: PathsForPages<Page>;
}
interface CreatePagesConfig {
pages: Page;
}
}

24
src/pages/_layout.tsx Normal file
View File

@@ -0,0 +1,24 @@
import type { ReactNode } from "react";
import "../styles/global.css";
import Providers from "../components/Providers.js";
const Layout = ({ children }: { children: ReactNode }) => (
<Providers>
<div className="min-h-screen bg-gray-50 text-gray-900">
<header className="border-b border-gray-200 bg-white px-4 py-3">
<h1 className="text-xl font-bold">
<a href="/" className="hover:text-blue-600">
P2P Poll
</a>
</h1>
</header>
<main className="mx-auto max-w-xl px-4 py-8">{children}</main>
</div>
</Providers>
);
export default Layout;
export const getConfig = async () => {
return { render: "static" } as const;
};

17
src/pages/_root.tsx Normal file
View File

@@ -0,0 +1,17 @@
import type { ReactNode } from "react";
export default function Root({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>{children}</body>
</html>
);
}
export const getConfig = () => {
return { render: "dynamic" } as const;
};

9
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,9 @@
import HomeClient from "../components/HomeClient.js";
export default async function HomePage() {
return <HomeClient />;
}
export const getConfig = async () => {
return { render: "dynamic" } as const;
};

10
src/pages/poll/[id].tsx Normal file
View File

@@ -0,0 +1,10 @@
import type { PageProps } from "waku/router";
import PollPageClient from "../../components/PollPageClient.js";
export default async function PollPage({ id }: PageProps<"/poll/[id]">) {
return <PollPageClient id={id} />;
}
export const getConfig = async () => {
return { render: "dynamic" } as const;
};

View File

@@ -1,131 +0,0 @@
import type { PollViewModel } from "./state";
interface RenderActions {
onSubmitOption: (label: string) => void;
onVote: (optionId: string) => void;
onCopyLink: () => void;
}
function escapeHtml(value: string) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
export function renderApp(
container: HTMLElement,
viewModel: PollViewModel,
feedbackMessage: string,
actions: RenderActions,
) {
const optionMarkup = viewModel.options.length
? viewModel.options
.map((option) => {
const buttonLabel = option.isSelectedByMe ? "Selected" : "Vote";
const selectedClass = option.isSelectedByMe ? "option-card selected" : "option-card";
return `
<li class="${selectedClass}">
<div class="option-main">
<div>
<p class="option-label">${escapeHtml(option.label)}</p>
<p class="option-meta">${option.voteCount} vote${option.voteCount === 1 ? "" : "s"}</p>
</div>
<button class="vote-button" data-option-id="${option.id}">
${buttonLabel}
</button>
</div>
</li>
`;
})
.join("")
: `
<li class="empty-state">
No options yet. Add the first one and it will sync to everyone in this room.
</li>
`;
const statusClass = `status-pill ${viewModel.connectionStatus}`;
const myVoteLabel = viewModel.myVoteOptionId
? "Your vote is currently synced."
: "You have not voted yet.";
container.innerHTML = `
<main class="shell">
<section class="hero">
<div class="hero-copy">
<p class="eyebrow">Peer-to-peer room</p>
<h1>${escapeHtml(viewModel.title)}</h1>
<p class="hero-text">
Share the room link, add options, and vote live without a central app server storing poll state.
</p>
</div>
<div class="hero-meta">
<span class="${statusClass}">${viewModel.connectionStatus}</span>
<p class="room-label">Room: <code>${escapeHtml(viewModel.roomId)}</code></p>
</div>
</section>
<section class="panel share-panel">
<label class="field-label" for="share-url">Share this room</label>
<div class="share-row">
<input id="share-url" class="share-input" type="text" readonly value="${escapeHtml(viewModel.shareUrl)}" />
<button id="copy-link-button" class="secondary-button" type="button">Copy link</button>
</div>
</section>
<section class="panel">
<div class="section-header">
<div>
<h2>Options</h2>
<p>${myVoteLabel}</p>
</div>
</div>
<ul class="options-list">${optionMarkup}</ul>
</section>
<section class="panel">
<h2>Add an option</h2>
<form id="add-option-form" class="option-form">
<input
id="option-input"
name="option"
type="text"
maxlength="80"
placeholder="Add an option"
autocomplete="off"
required
/>
<button class="primary-button" type="submit">Add option</button>
</form>
<p class="feedback" aria-live="polite">${escapeHtml(feedbackMessage)}</p>
</section>
</main>
`;
const addOptionForm = container.querySelector<HTMLFormElement>("#add-option-form");
const optionInput = container.querySelector<HTMLInputElement>("#option-input");
const copyLinkButton = container.querySelector<HTMLButtonElement>("#copy-link-button");
addOptionForm?.addEventListener("submit", (event) => {
event.preventDefault();
const label = optionInput?.value ?? "";
actions.onSubmitOption(label);
});
copyLinkButton?.addEventListener("click", () => {
actions.onCopyLink();
});
container.querySelectorAll<HTMLButtonElement>("[data-option-id]").forEach((button) => {
button.addEventListener("click", () => {
const optionId = button.dataset.optionId;
if (optionId) {
actions.onVote(optionId);
}
});
});
}

View File

@@ -1,124 +0,0 @@
import * as Y from "yjs";
export const DEFAULT_POLL_TITLE = "Shared Poll";
export type ConnectionStatus = "connecting" | "connected" | "offline";
export interface OptionRecord {
id: string;
label: string;
createdAt: number;
createdBy: string;
}
export interface PollOptionViewModel extends OptionRecord {
voteCount: number;
isSelectedByMe: boolean;
}
export interface PollViewModel {
title: string;
roomId: string;
shareUrl: string;
connectionStatus: ConnectionStatus;
options: PollOptionViewModel[];
myVoteOptionId: string | null;
}
const META_KEY_TITLE = "title";
function createOptionId() {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `option-${Math.random().toString(36).slice(2, 10)}`;
}
function normalizeLabel(label: string) {
return label.trim().replace(/\s+/g, " ");
}
function getPollTitle(meta: Y.Map<string>) {
return meta.get(META_KEY_TITLE) ?? DEFAULT_POLL_TITLE;
}
export function ensurePollInitialized(meta: Y.Map<string>) {
if (!meta.has(META_KEY_TITLE)) {
meta.set(META_KEY_TITLE, DEFAULT_POLL_TITLE);
}
}
export function addOption(
options: Y.Map<OptionRecord>,
rawLabel: string,
userId: string,
) {
const label = normalizeLabel(rawLabel);
if (!label) {
return { ok: false as const, error: "Option label cannot be empty." };
}
const normalizedTarget = label.toLocaleLowerCase();
const duplicate = Array.from(options.values()).some(
(option) => option.label.trim().toLocaleLowerCase() === normalizedTarget,
);
if (duplicate) {
return { ok: false as const, error: "That option already exists." };
}
const option: OptionRecord = {
id: createOptionId(),
label,
createdAt: Date.now(),
createdBy: userId,
};
options.set(option.id, option);
return { ok: true as const, optionId: option.id };
}
export function castVote(votes: Y.Map<string>, userId: string, optionId: string) {
votes.set(userId, optionId);
}
export function createViewModel(params: {
meta: Y.Map<string>;
options: Y.Map<OptionRecord>;
votes: Y.Map<string>;
roomId: string;
shareUrl: string;
connectionStatus: ConnectionStatus;
userId: string;
}): PollViewModel {
const { meta, options, votes, roomId, shareUrl, connectionStatus, userId } =
params;
const tally = new Map<string, number>();
for (const optionId of votes.values()) {
tally.set(optionId, (tally.get(optionId) ?? 0) + 1);
}
const myVoteOptionId = votes.get(userId) ?? null;
const sortedOptions = Array.from(options.values()).sort((left, right) => {
if (left.createdAt !== right.createdAt) {
return left.createdAt - right.createdAt;
}
return left.label.localeCompare(right.label);
});
return {
title: getPollTitle(meta),
roomId,
shareUrl,
connectionStatus,
myVoteOptionId,
options: sortedOptions.map((option) => ({
...option,
voteCount: tally.get(option.id) ?? 0,
isSelectedByMe: myVoteOptionId === option.id,
})),
};
}

View File

@@ -1,256 +0,0 @@
:root {
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
color: #132238;
background:
radial-gradient(circle at top left, rgba(255, 190, 120, 0.55), transparent 28%),
radial-gradient(circle at top right, rgba(86, 201, 166, 0.3), transparent 24%),
linear-gradient(180deg, #f9f4e8 0%, #f4efe6 48%, #ece5d8 100%);
line-height: 1.5;
font-weight: 400;
color-scheme: light;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
}
button,
input {
font: inherit;
}
button {
cursor: pointer;
}
#app {
min-height: 100vh;
}
.shell {
width: min(920px, calc(100% - 2rem));
margin: 0 auto;
padding: 2rem 0 3rem;
}
.hero {
display: flex;
justify-content: space-between;
gap: 1.5rem;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.hero-copy h1 {
margin: 0.15rem 0 0.5rem;
font-size: clamp(2.4rem, 6vw, 4rem);
line-height: 0.95;
letter-spacing: -0.05em;
}
.hero-text,
.section-header p,
.feedback,
.option-meta,
.room-label {
color: #4a5a6a;
}
.eyebrow {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.8rem;
color: #915f00;
}
.hero-meta {
min-width: 15rem;
padding: 1rem 1.1rem;
border-radius: 1.2rem;
background: rgba(255, 255, 255, 0.68);
border: 1px solid rgba(19, 34, 56, 0.08);
backdrop-filter: blur(10px);
}
.status-pill {
display: inline-flex;
align-items: center;
padding: 0.35rem 0.75rem;
border-radius: 999px;
font-weight: 700;
text-transform: capitalize;
font-size: 0.9rem;
}
.status-pill.connected {
background: rgba(86, 201, 166, 0.18);
color: #135d43;
}
.status-pill.connecting {
background: rgba(255, 190, 120, 0.22);
color: #8c5300;
}
.status-pill.offline {
background: rgba(219, 82, 82, 0.16);
color: #8f1f1f;
}
.panel {
margin-bottom: 1.25rem;
padding: 1.25rem;
border-radius: 1.3rem;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(19, 34, 56, 0.08);
box-shadow: 0 14px 40px rgba(19, 34, 56, 0.08);
backdrop-filter: blur(10px);
}
.panel h2 {
margin: 0 0 0.45rem;
font-size: 1.1rem;
}
.share-row,
.option-form,
.option-main {
display: flex;
gap: 0.75rem;
}
.field-label {
display: block;
margin-bottom: 0.6rem;
font-weight: 700;
}
.share-input,
.option-form input {
width: 100%;
min-width: 0;
border-radius: 0.95rem;
border: 1px solid rgba(19, 34, 56, 0.14);
background: rgba(255, 255, 255, 0.94);
padding: 0.85rem 1rem;
color: #132238;
}
.share-input:focus,
.option-form input:focus {
outline: 2px solid rgba(255, 190, 120, 0.65);
outline-offset: 1px;
}
.primary-button,
.secondary-button,
.vote-button {
border: none;
border-radius: 0.95rem;
padding: 0.85rem 1rem;
font-weight: 700;
transition: transform 150ms ease, box-shadow 150ms ease, background-color 150ms ease;
}
.primary-button,
.vote-button {
background: #132238;
color: #f9f4e8;
box-shadow: 0 10px 20px rgba(19, 34, 56, 0.14);
}
.secondary-button {
background: rgba(19, 34, 56, 0.08);
color: #132238;
}
.primary-button:hover,
.secondary-button:hover,
.vote-button:hover {
transform: translateY(-1px);
}
.options-list {
list-style: none;
margin: 1rem 0 0;
padding: 0;
display: grid;
gap: 0.75rem;
}
.option-card,
.empty-state {
border-radius: 1.1rem;
padding: 1rem;
background: rgba(247, 244, 236, 0.95);
border: 1px solid rgba(19, 34, 56, 0.08);
}
.option-card.selected {
border-color: rgba(86, 201, 166, 0.85);
background: rgba(232, 249, 242, 0.95);
}
.option-main {
align-items: center;
justify-content: space-between;
}
.option-label {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
}
.option-meta,
.feedback,
.room-label {
margin: 0.35rem 0 0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
}
.feedback {
min-height: 1.5rem;
}
code {
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
}
@media (max-width: 720px) {
.shell {
width: min(100% - 1rem, 920px);
padding-top: 1rem;
}
.hero,
.share-row,
.option-form,
.option-main,
.section-header {
flex-direction: column;
}
.hero-meta {
width: 100%;
min-width: 0;
}
.primary-button,
.secondary-button,
.vote-button {
width: 100%;
}
}

1
src/styles/global.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -1,64 +0,0 @@
import { IndexeddbPersistence } from "y-indexeddb";
import { WebrtcProvider } from "y-webrtc";
import * as Y from "yjs";
import { type ConnectionStatus, ensurePollInitialized, type OptionRecord } from "./state";
export interface AppSync {
doc: Y.Doc;
meta: Y.Map<string>;
options: Y.Map<OptionRecord>;
votes: Y.Map<string>;
provider: WebrtcProvider;
persistence: IndexeddbPersistence;
getConnectionStatus: () => ConnectionStatus;
destroy: () => void;
}
export function initSync(roomId: string): AppSync {
const doc = new Y.Doc();
const meta = doc.getMap<string>("poll-meta");
const options = doc.getMap<OptionRecord>("poll-options");
const votes = doc.getMap<string>("poll-votes");
ensurePollInitialized(meta);
let connectionStatus: ConnectionStatus = navigator.onLine ? "connecting" : "offline";
const provider = new WebrtcProvider(roomId, doc);
const persistence = new IndexeddbPersistence(roomId, doc);
const syncConnectionStatus = (status: ConnectionStatus) => {
connectionStatus = navigator.onLine ? status : "offline";
};
const handleOnline = () => {
syncConnectionStatus(provider.connected ? "connected" : "connecting");
};
const handleOffline = () => {
connectionStatus = "offline";
};
provider.on("status", (event: { connected: boolean }) => {
syncConnectionStatus(event.connected ? "connected" : "connecting");
});
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return {
doc,
meta,
options,
votes,
provider,
persistence,
getConnectionStatus: () => connectionStatus,
destroy: () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
persistence.destroy();
provider.destroy();
doc.destroy();
},
};
}

View File

@@ -1,15 +1,17 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"strict": true, "strict": true,
"target": "esnext",
"noEmit": true, "noEmit": true,
"isolatedModules": true,
"moduleDetection": "force",
"downlevelIteration": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true, "skipLibCheck": true,
"allowSyntheticDefaultImports": true, "noUncheckedIndexedAccess": true,
"useDefineForClassFields": true "exactOptionalPropertyTypes": true,
}, "jsx": "react-jsx"
"include": ["src"] }
} }

10
waku.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import tailwindcss from "@tailwindcss/vite";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
import { defineConfig } from "waku/config";
export default defineConfig({
vite: {
plugins: [tailwindcss(), wasm(), topLevelAwait()],
},
});