Compare commits

..

2 Commits

27 changed files with 699 additions and 1371 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
node_modules
dist dist
node_modules

102
PLAN.md
View File

@@ -1,102 +0,0 @@
# Plan: Combined P2P Polling App
## Overview
Merge the three subfolder projects into a single TypeScript + Yjs + Vite application at the repo root, combining the best features from each.
## Technology Choices
- **Language:** TypeScript (from project 3) with strict mode
- **CRDT:** Yjs + y-webrtc + y-indexeddb (from projects 1 & 3)
- **Build:** Vite (shared by all)
- **Package manager:** npm
## Feature Superset
From **project 1** (group-efa16e66):
- Collaborative title editing via Y.Text (real-time character-level sync)
- Delete options
- Diff-rendering for poll list (reuse DOM elements)
- Peer count display via awareness
- Share section with copy-to-clipboard
From **project 2** (proposal-8835ffc9):
- Deadline/timer system (2-minute voting window with countdown)
- Duplicate detection (case-insensitive)
From **project 3** (proposal-88461784):
- TypeScript types & strict mode
- Modular architecture (state, sync, render, identity, app layers)
- IndexedDB persistence (offline support)
- Online/offline connection tracking
- Input validation (max lengths, empty checks)
- HTML escaping for XSS prevention
- ViewModel pattern for clean render layer
## Data Model (Yjs)
```
Y.Doc
├── poll-meta (Y.Map<string>)
│ └── title → Y.Text (collaborative editing from project 1)
├── poll-options (Y.Map<OptionRecord>)
│ └── [optionId] → { id, label, createdAt, createdBy }
├── poll-votes (Y.Map<string>)
│ └── [userId] → optionId (single vote per user)
└── poll-deadline (Y.Map<any>)
└── deadline → number | null (timestamp, from project 2)
```
## Architecture & File Structure
```
/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts (if needed for any plugins)
├── src/
│ ├── main.ts (entry: mount app)
│ ├── app.ts (orchestrator: init sync, bind events, manage state)
│ ├── identity.ts (getUserId with localStorage persistence)
│ ├── state.ts (types, pure functions, ViewModel creation)
│ ├── sync.ts (Yjs doc, WebRTC provider, IndexedDB, connection status)
│ ├── render.ts (DOM rendering with escapeHtml, diff-rendering for options)
│ ├── components/
│ │ ├── PollTitle.ts (collaborative title input bound to Y.Text)
│ │ ├── PollList.ts (diff-rendered option list with sorting)
│ │ ├── PollOption.ts (single option: vote bar, vote/delete buttons)
│ │ ├── AddOption.ts (input + submit with validation & duplicate check)
│ │ ├── StatusBar.ts (connection status + peer count)
│ │ ├── ShareSection.ts (copy URL to clipboard)
│ │ └── DeadlineTimer.ts (set deadline + countdown display, from project 2)
│ └── styles.css (merged: project 1's design tokens + project 3's glassmorphism)
```
## Implementation Steps
### Step 1: Scaffold root project
- Create `package.json` with dependencies: yjs, y-webrtc, y-indexeddb, vite, typescript
- Create `tsconfig.json` (strict, ES2022, bundler resolution)
- Create `index.html` entry point
- Create `vite.config.ts` if needed
### Step 2: Core layer — identity.ts, state.ts, sync.ts
- `identity.ts`: port from project 3 (getUserId)
- `state.ts`: port types from project 3, add deadline types, add ViewModel with deadline/timer info, add vote percentage calculation from project 1
- `sync.ts`: port from project 3, add Y.Text for title (from project 1), add poll-deadline map, add awareness tracking for peer count (from project 1)
### Step 3: App orchestrator — app.ts, main.ts
- `app.ts`: port from project 3, add deadline handlers, add delete option handler, wire up all components
- `main.ts`: minimal entry that calls initApp
### Step 4: Components
- `PollTitle.ts`: port collaborative Y.Text editing from project 1, add TypeScript types
- `AddOption.ts`: merge project 1 (UI/animation) + project 2 (duplicate detection) + project 3 (validation)
- `PollOption.ts`: port from project 1 (vote bar, percentage, delete button), add TypeScript
- `PollList.ts`: port diff-rendering from project 1, add TypeScript
- `StatusBar.ts`: merge project 1 (peer count) + project 3 (online/offline status)
- `ShareSection.ts`: port from project 1, add TypeScript
- `DeadlineTimer.ts`: new component porting project 2's deadline/countdown logic to Yjs
### Step 5: Styling
- Merge CSS: use project 1's design tokens and typography as base, incorporate project 3's glassmorphism panel effects, add timer-specific styles from project 2
### Step 6: Cleanup
- Remove three subfolders (after confirming with user)
- Update root README.md

View File

@@ -1 +1,44 @@
# P2P Poll App # P2P Poll App
A peer-to-peer polling application where users create and vote on polls without any central server. All data syncs directly between browsers using WebRTC and CRDTs.
## Features
- **Real-time P2P sync** — poll options and votes sync instantly across all connected peers via WebRTC
- **Collaborative poll title** — editable title that syncs between all participants
- **One vote per user** — each peer gets a stable ID, enforcing one vote per person per option
- **Vote/Unvote toggle** — change your mind anytime
- **Connection status** — see when you're connected and how many peers are in the room
- **Shareable polls** — share via URL with `?room=your-poll-name`
- **No backend required** — runs entirely in the browser
## Tech Stack
- [Yjs](https://yjs.dev/) — CRDT library for conflict-free shared state
- [y-webrtc](https://github.com/yjs/y-webrtc) — WebRTC provider for peer-to-peer connections
- [Vite](https://vitejs.dev/) — Development server and build tool
- Vanilla JavaScript — no framework dependencies
## Getting Started
```bash
npm install
npm run dev
```
Open `http://localhost:5173/?room=my-poll` in multiple browser tabs to test.
To test across devices on the same network:
```bash
npm run dev -- --host
```
Then open the URL shown in the terminal on other devices.
## How It Works
1. Each browser tab creates a Yjs document and connects to other peers via WebRTC
2. Poll options and votes are stored in Yjs shared data types (Y.Map)
3. Changes propagate automatically to all connected peers using CRDTs
4. A public signaling server handles peer discovery; all poll data flows directly between browsers

View File

@@ -1,12 +1,13 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Polly — P2P Polls</title> <title>Polly — P2P Polls</title>
</head> <link rel="stylesheet" href="/src/style.css">
<body> </head>
<div id="app"></div> <body>
<script type="module" src="/src/main.ts"></script> <div id="app"></div>
</body> <script type="module" src="/src/main.js"></script>
</body>
</html> </html>

510
package-lock.json generated
View File

@@ -1,26 +1,24 @@
{ {
"name": "polly-p2p-poll", "name": "p2p-poll",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "polly-p2p-poll", "name": "p2p-poll",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"y-indexeddb": "^9.0.12",
"y-webrtc": "^10.3.0", "y-webrtc": "^10.3.0",
"yjs": "^13.6.27" "yjs": "^13.6.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2", "vite": "^6.0.0"
"vite": "^7.1.5"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -35,9 +33,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -52,9 +50,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -69,9 +67,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -86,9 +84,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -103,9 +101,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -120,9 +118,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -137,9 +135,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -154,9 +152,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -171,9 +169,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -188,9 +186,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -205,9 +203,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -222,9 +220,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -239,9 +237,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -256,9 +254,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -273,9 +271,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -290,9 +288,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -307,9 +305,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -324,9 +322,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -341,9 +339,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -358,9 +356,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -375,9 +373,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -392,9 +390,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -409,9 +407,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -426,9 +424,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -443,9 +441,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -460,9 +458,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz",
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -474,9 +472,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz",
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -488,9 +486,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz",
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -502,9 +500,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz",
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -516,9 +514,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz",
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -530,9 +528,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz",
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -544,9 +542,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz",
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -558,9 +556,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz",
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -572,9 +570,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz",
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -586,9 +584,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz",
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -600,9 +598,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz",
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -614,9 +612,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-musl": { "node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz",
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -628,9 +626,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz",
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -642,9 +640,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-musl": { "node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz",
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -656,9 +654,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz",
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -670,9 +668,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz",
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -684,9 +682,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz",
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -698,9 +696,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz",
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -712,9 +710,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz",
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -726,9 +724,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": { "node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz",
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -740,9 +738,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz",
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -754,9 +752,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz",
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -768,9 +766,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz",
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -782,9 +780,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz",
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -796,9 +794,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz",
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -884,9 +882,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.27.7", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -897,32 +895,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7", "@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.27.7", "@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.27.7", "@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.27.7", "@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.27.7", "@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.27.7", "@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.27.7", "@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.27.7", "@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.27.7", "@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.27.7", "@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.27.7", "@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.27.7" "@esbuild/win32-x64": "0.25.12"
} }
}, },
"node_modules/fdir": { "node_modules/fdir": {
@@ -1067,9 +1065,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.9", "version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1139,9 +1137,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.60.1", "version": "4.60.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1155,31 +1153,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm-eabi": "4.60.0",
"@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-android-arm64": "4.60.0",
"@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.0",
"@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.0",
"@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.0",
"@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0",
"@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.0",
"@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.0",
"@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.0",
"@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.0",
"@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.0",
"@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.0",
"@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.0",
"@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.0",
"@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.0",
"@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.0",
"@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.0",
"@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.0",
"@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.0",
"@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.0",
"@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.0",
"@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.0",
"@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.0",
"@rollup/rollup-win32-x64-msvc": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -1252,14 +1250,14 @@
} }
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.16", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fdir": "^6.5.0", "fdir": "^6.5.0",
"picomatch": "^4.0.4" "picomatch": "^4.0.3"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@@ -1268,20 +1266,6 @@
"url": "https://github.com/sponsors/SuperchupuDev" "url": "https://github.com/sponsors/SuperchupuDev"
} }
}, },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -1289,24 +1273,24 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.3.2", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.4.4",
"picomatch": "^4.0.3", "picomatch": "^4.0.2",
"postcss": "^8.5.6", "postcss": "^8.5.3",
"rollup": "^4.43.0", "rollup": "^4.34.9",
"tinyglobby": "^0.2.15" "tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
@@ -1315,14 +1299,14 @@
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0", "jiti": ">=1.21.0",
"less": "^4.0.0", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "^1.70.0", "sass": "*",
"sass-embedded": "^1.70.0", "sass-embedded": "*",
"stylus": ">=0.54.8", "stylus": "*",
"sugarss": "^5.0.0", "sugarss": "*",
"terser": "^5.16.0", "terser": "^5.16.0",
"tsx": "^4.8.1", "tsx": "^4.8.1",
"yaml": "^2.4.2" "yaml": "^2.4.2"
@@ -1385,26 +1369,6 @@
} }
} }
}, },
"node_modules/y-indexeddb": {
"version": "9.0.12",
"resolved": "https://registry.npmjs.org/y-indexeddb/-/y-indexeddb-9.0.12.tgz",
"integrity": "sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==",
"license": "MIT",
"dependencies": {
"lib0": "^0.2.74"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
},
"peerDependencies": {
"yjs": "^13.0.0"
}
},
"node_modules/y-protocols": { "node_modules/y-protocols": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz", "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz",

View File

@@ -1,20 +1,18 @@
{ {
"name": "polly-p2p-poll", "name": "p2p-poll",
"version": "1.0.0",
"private": true, "private": true,
"version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc --noEmit && vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"y-indexeddb": "^9.0.12", "yjs": "^13.6.0",
"y-webrtc": "^10.3.0", "y-webrtc": "^10.3.0"
"yjs": "^13.6.27"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2", "vite": "^6.0.0"
"vite": "^7.1.5"
} }
} }

View File

@@ -1,139 +0,0 @@
import { getUserId } from "./identity";
import {
addOption,
toggleVote,
deleteOption,
setDeadline,
clearDeadline,
createViewModel,
} from "./state";
import { initSync } from "./sync";
import { StatusBar } from "./components/StatusBar";
import { PollTitle } from "./components/PollTitle";
import { AddOption } from "./components/AddOption";
import { PollList } from "./components/PollList";
import { ShareSection } from "./components/ShareSection";
import { DeadlineTimer } from "./components/DeadlineTimer";
const ROOM_PARAM = "room";
function createRoomId(): string {
if (typeof crypto.randomUUID === "function") {
return `poll-${crypto.randomUUID().slice(0, 8)}`;
}
return `poll-${Math.random().toString(36).slice(2, 10)}`;
}
function ensureRoomId(): string {
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): () => void {
const roomId = ensureRoomId();
const userId = getUserId();
const sync = initSync(roomId);
const shareUrl = window.location.href;
// --- Actions ---
const actions = {
addOption: (label: string) => {
const vm = createViewModel(getViewModelParams());
if (vm.votingClosed) return;
return addOption(sync.options, label, userId);
},
toggleVote: (optionId: string) => {
const vm = createViewModel(getViewModelParams());
if (vm.votingClosed) return;
toggleVote(sync.votes, userId, optionId);
},
deleteOption: (optionId: string) => {
deleteOption(sync.options, sync.votes, optionId);
},
startDeadline: (durationMs: number) => {
setDeadline(sync.deadlineMap, durationMs);
},
clearDeadline: () => {
clearDeadline(sync.deadlineMap);
},
};
function getViewModelParams() {
return {
yTitle: sync.yTitle,
options: sync.options,
votes: sync.votes,
deadlineMap: sync.deadlineMap,
roomId,
shareUrl,
connectionStatus: sync.getConnectionStatus(),
peerCount: sync.getPeerCount(),
userId,
};
}
// --- Build UI ---
// Header
const header = document.createElement("header");
header.className = "app-header";
const wordmark = document.createElement("div");
wordmark.className = "app-wordmark";
wordmark.innerHTML = `
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="2" y="4" width="16" height="2.5" rx="1.25" fill="currentColor"/>
<rect x="2" y="8.75" width="11" height="2.5" rx="1.25" fill="currentColor" opacity="0.6"/>
<rect x="2" y="13.5" width="13" height="2.5" rx="1.25" fill="currentColor" opacity="0.35"/>
</svg>
<span>Polly</span>
`;
const statusBar = StatusBar(sync.provider);
header.append(wordmark, statusBar);
// Main card
const card = document.createElement("main");
card.className = "app-card";
const pollTitle = PollTitle(sync.doc, sync.yTitle);
const addOptionComponent = AddOption((label: string) => {
const result = actions.addOption(label);
if (result && !result.ok) return result.error;
return null;
});
const pollList = PollList(sync.options, sync.votes, userId, () => {
const vm = createViewModel(getViewModelParams());
return vm.votingClosed;
}, actions.toggleVote, actions.deleteOption);
const deadlineTimer = DeadlineTimer(
sync.deadlineMap,
actions.startDeadline,
actions.clearDeadline,
);
card.append(pollTitle, addOptionComponent, deadlineTimer, pollList);
// Footer
const footer = document.createElement("footer");
footer.className = "app-footer";
footer.appendChild(ShareSection(roomId));
container.append(header, card, footer);
// --- Cleanup ---
return () => {
sync.destroy();
};
}

View File

@@ -0,0 +1,47 @@
import { addOption } from '../utils/store.js';
export function AddOption() {
const wrapper = document.createElement('div');
wrapper.className = 'add-option-wrapper';
const input = document.createElement('input');
input.type = 'text';
input.className = 'add-option-input';
input.placeholder = 'Add an option…';
input.maxLength = 100;
input.setAttribute('aria-label', 'New poll option');
const btn = document.createElement('button');
btn.className = 'add-option-btn';
btn.setAttribute('aria-label', 'Add option');
// Plus icon
btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
</svg>
<span>Add</span>
`;
wrapper.append(input, btn);
function submit() {
const name = input.value.trim();
if (!name) {
input.focus();
input.classList.add('shake');
input.addEventListener('animationend', () => input.classList.remove('shake'), { once: true });
return;
}
addOption(name);
input.value = '';
input.focus();
}
btn.addEventListener('click', submit);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') submit();
});
return wrapper;
}

View File

@@ -1,66 +0,0 @@
export function AddOption(
onSubmit: (label: string) => string | null,
): HTMLElement {
const wrapper = document.createElement("div");
wrapper.className = "add-option-wrapper";
const input = document.createElement("input");
input.type = "text";
input.className = "add-option-input";
input.placeholder = "Add an option\u2026";
input.maxLength = 100;
input.setAttribute("aria-label", "New poll option");
const btn = document.createElement("button");
btn.className = "add-option-btn";
btn.setAttribute("aria-label", "Add option");
btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
</svg>
<span>Add</span>
`;
const feedback = document.createElement("div");
feedback.className = "add-option-feedback";
feedback.setAttribute("aria-live", "polite");
wrapper.append(input, btn, feedback);
function submit() {
const name = input.value.trim();
if (!name) {
input.focus();
input.classList.add("shake");
input.addEventListener("animationend", () => input.classList.remove("shake"), { once: true });
return;
}
const error = onSubmit(name);
if (error) {
feedback.textContent = error;
feedback.style.display = "";
setTimeout(() => {
feedback.textContent = "";
feedback.style.display = "none";
}, 3000);
return;
}
input.value = "";
feedback.textContent = "";
feedback.style.display = "none";
input.focus();
}
btn.addEventListener("click", submit);
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") submit();
});
input.addEventListener("input", () => {
feedback.textContent = "";
feedback.style.display = "none";
});
return wrapper;
}

View File

@@ -1,86 +0,0 @@
import * as Y from "yjs";
import { getDeadline } from "../state";
const DEADLINE_DURATION_MS = 2 * 60 * 1000; // 2 minutes
export function DeadlineTimer(
deadlineMap: Y.Map<unknown>,
onStartDeadline: (durationMs: number) => void,
onClearDeadline: () => void,
): HTMLElement {
const wrapper = document.createElement("div");
wrapper.className = "deadline-wrapper";
const timerEl = document.createElement("span");
timerEl.className = "deadline-timer";
const startBtn = document.createElement("button");
startBtn.className = "deadline-btn";
startBtn.textContent = "Start 2-min vote";
startBtn.setAttribute("aria-label", "Start a 2-minute voting deadline");
const clearBtn = document.createElement("button");
clearBtn.className = "deadline-btn deadline-btn--clear";
clearBtn.textContent = "Clear";
clearBtn.setAttribute("aria-label", "Remove voting deadline");
wrapper.append(timerEl, startBtn, clearBtn);
let interval: ReturnType<typeof setInterval> | undefined;
function render() {
const deadline = getDeadline(deadlineMap);
const now = Date.now();
if (deadline === null) {
// No deadline set
timerEl.textContent = "";
timerEl.className = "deadline-timer";
startBtn.hidden = false;
clearBtn.hidden = true;
if (interval) {
clearInterval(interval);
interval = undefined;
}
return;
}
startBtn.hidden = true;
clearBtn.hidden = false;
if (now >= deadline) {
// Voting closed
timerEl.textContent = "Voting closed";
timerEl.className = "deadline-timer deadline-timer--closed";
if (interval) {
clearInterval(interval);
interval = undefined;
}
return;
}
// Counting down
const remaining = Math.ceil((deadline - now) / 1000);
const mins = Math.floor(remaining / 60);
const secs = remaining % 60;
timerEl.textContent = `Voting closes in ${mins}:${secs.toString().padStart(2, "0")}`;
timerEl.className = "deadline-timer deadline-timer--active";
if (!interval) {
interval = setInterval(() => render(), 1000);
}
}
startBtn.addEventListener("click", () => {
onStartDeadline(DEADLINE_DURATION_MS);
});
clearBtn.addEventListener("click", () => {
onClearDeadline();
});
deadlineMap.observe(() => render());
render();
return wrapper;
}

View File

@@ -0,0 +1,77 @@
import { yOptions, getEntries, getTotalVotes } from '../utils/store.js';
import { PollOption } from './PollOption.js';
export function PollList() {
const wrapper = document.createElement('div');
wrapper.className = 'poll-list-wrapper';
const meta = document.createElement('div');
meta.className = 'poll-list-meta';
const list = document.createElement('div');
list.className = 'poll-list';
const empty = document.createElement('div');
empty.className = 'poll-list-empty';
empty.innerHTML = `
<div class="empty-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="10" width="24" height="3" rx="1.5" fill="currentColor" opacity="0.15"/>
<rect x="4" y="16" width="18" height="3" rx="1.5" fill="currentColor" opacity="0.1"/>
<rect x="4" y="22" width="21" height="3" rx="1.5" fill="currentColor" opacity="0.07"/>
</svg>
</div>
<p>No options yet — add the first one above.</p>
`;
wrapper.append(meta, list, empty);
function render() {
const entries = getEntries();
const total = getTotalVotes();
// Meta line
if (entries.length > 0) {
meta.textContent = `${entries.length} option${entries.length !== 1 ? 's' : ''} · ${total} vote${total !== 1 ? 's' : ''} total`;
meta.style.display = '';
} else {
meta.style.display = 'none';
}
// Empty state
empty.style.display = entries.length === 0 ? '' : 'none';
// Diff-render: reuse existing rows when possible
const existing = new Map(
[...list.querySelectorAll('.poll-option')].map((el) => [el.dataset.id, el])
);
// Remove stale rows
existing.forEach((el, id) => {
if (!entries.find((e) => e.id === id)) el.remove();
});
// Update or insert rows in sorted order
entries.forEach((entry, i) => {
const newEl = PollOption({ ...entry, totalVotes: total });
const currentEl = list.children[i];
if (!currentEl) {
list.appendChild(newEl);
} else if (currentEl.dataset.id !== entry.id) {
list.insertBefore(newEl, currentEl);
// Remove the now-displaced old element if it was this id
const old = existing.get(entry.id);
if (old && old !== currentEl) old.remove();
} else {
// Replace in-place so vote bar animation triggers
list.replaceChild(newEl, currentEl);
}
});
}
yOptions.observeDeep(() => render());
render();
return wrapper;
}

View File

@@ -1,127 +0,0 @@
import * as Y from "yjs";
import type { OptionRecord } from "../state";
import { PollOption } from "./PollOption";
export function PollList(
yOptions: Y.Map<OptionRecord>,
yVotes: Y.Map<string>,
userId: string,
isVotingClosed: () => boolean,
onVote: (optionId: string) => void,
onDelete: (optionId: string) => void,
): HTMLElement {
const wrapper = document.createElement("div");
wrapper.className = "poll-list-wrapper";
const meta = document.createElement("div");
meta.className = "poll-list-meta";
const list = document.createElement("div");
list.className = "poll-list";
const empty = document.createElement("div");
empty.className = "poll-list-empty";
empty.innerHTML = `
<div class="empty-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="10" width="24" height="3" rx="1.5" fill="currentColor" opacity="0.15"/>
<rect x="4" y="16" width="18" height="3" rx="1.5" fill="currentColor" opacity="0.1"/>
<rect x="4" y="22" width="21" height="3" rx="1.5" fill="currentColor" opacity="0.07"/>
</svg>
</div>
<p>No options yet — add the first one above.</p>
`;
wrapper.append(meta, list, empty);
function getEntries() {
const entries: Array<{
id: string;
name: string;
votes: number;
voted: boolean;
}> = [];
// Tally votes per option
const tally = new Map<string, number>();
for (const optionId of yVotes.values()) {
tally.set(optionId, (tally.get(optionId) ?? 0) + 1);
}
const myVote = yVotes.get(userId) ?? null;
yOptions.forEach((record, id) => {
entries.push({
id,
name: record.label,
votes: tally.get(id) ?? 0,
voted: myVote === id,
});
});
entries.sort((a, b) => b.votes - a.votes || a.name.localeCompare(b.name));
return entries;
}
function getTotalVotes(): number {
return yVotes.size;
}
function render() {
const entries = getEntries();
const total = getTotalVotes();
const votingClosed = isVotingClosed();
// Meta line
if (entries.length > 0) {
meta.textContent = `${entries.length} option${entries.length !== 1 ? "s" : ""} \u00b7 ${total} vote${total !== 1 ? "s" : ""} total`;
meta.style.display = "";
} else {
meta.style.display = "none";
}
// Empty state
empty.style.display = entries.length === 0 ? "" : "none";
// Diff-render: reuse existing rows when possible
const existing = new Map(
[...list.querySelectorAll<HTMLElement>(".poll-option")].map((el) => [
el.dataset.id,
el,
]),
);
// Remove stale rows
existing.forEach((el, id) => {
if (!entries.find((e) => e.id === id)) el.remove();
});
// Update or insert rows in sorted order
entries.forEach((entry, i) => {
const newEl = PollOption({
...entry,
totalVotes: total,
votingClosed,
onVote,
onDelete,
});
const currentEl = list.children[i] as HTMLElement | undefined;
if (!currentEl) {
list.appendChild(newEl);
} else if (currentEl.dataset.id !== entry.id) {
list.insertBefore(newEl, currentEl);
const old = existing.get(entry.id);
if (old && old !== currentEl) old.remove();
} else {
list.replaceChild(newEl, currentEl);
}
});
}
yOptions.observeDeep(() => render());
yVotes.observe(() => render());
render();
return wrapper;
}

View File

@@ -1,21 +1,11 @@
import { escapeHtml } from "../state"; import { toggleVote, deleteOption } from '../utils/store.js';
export interface PollOptionProps { /**
id: string; * @param {{ id: string, name: string, votes: number, voted: boolean, totalVotes: number }} entry
name: string; */
votes: number; export function PollOption({ id, name, votes, voted, totalVotes }) {
voted: boolean; const row = document.createElement('div');
totalVotes: number; row.className = `poll-option${voted ? ' poll-option--voted' : ''}`;
votingClosed: boolean;
onVote: (id: string) => void;
onDelete: (id: string) => void;
}
export function PollOption(props: PollOptionProps): HTMLElement {
const { id, name, votes, voted, totalVotes, votingClosed, onVote, onDelete } = props;
const row = document.createElement("div");
row.className = `poll-option${voted ? " poll-option--voted" : ""}`;
row.dataset.id = id; row.dataset.id = id;
const pct = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0; const pct = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
@@ -26,9 +16,9 @@ export function PollOption(props: PollOptionProps): HTMLElement {
<span class="poll-option__name">${escapeHtml(name)}</span> <span class="poll-option__name">${escapeHtml(name)}</span>
<div class="poll-option__actions"> <div class="poll-option__actions">
<span class="poll-option__pct">${pct}%</span> <span class="poll-option__pct">${pct}%</span>
<span class="poll-option__count">${votes} vote${votes !== 1 ? "s" : ""}</span> <span class="poll-option__count">${votes} vote${votes !== 1 ? 's' : ''}</span>
<button class="poll-option__vote-btn" aria-pressed="${voted}"${votingClosed ? " disabled" : ""}> <button class="poll-option__vote-btn" aria-pressed="${voted}">
${voted ? "Voted" : "Vote"} ${voted ? 'Voted' : 'Vote'}
</button> </button>
<button class="poll-option__delete-btn" aria-label="Remove option"> <button class="poll-option__delete-btn" aria-label="Remove option">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -39,8 +29,16 @@ export function PollOption(props: PollOptionProps): HTMLElement {
</div> </div>
`; `;
row.querySelector(".poll-option__vote-btn")!.addEventListener("click", () => onVote(id)); row.querySelector('.poll-option__vote-btn').addEventListener('click', () => toggleVote(id));
row.querySelector(".poll-option__delete-btn")!.addEventListener("click", () => onDelete(id)); row.querySelector('.poll-option__delete-btn').addEventListener('click', () => deleteOption(id));
return row; return row;
} }
function escapeHtml(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}

View File

@@ -0,0 +1,34 @@
import { ydoc, yTitle } from '../utils/store.js';
export function PollTitle() {
const wrapper = document.createElement('div');
wrapper.className = 'poll-title-wrapper';
const input = document.createElement('input');
input.type = 'text';
input.id = 'poll-title';
input.className = 'poll-title-input';
input.placeholder = 'Untitled Poll';
input.maxLength = 120;
input.setAttribute('aria-label', 'Poll title');
input.value = yTitle.toString();
wrapper.appendChild(input);
// Sync from Yjs → input (only when not focused to avoid cursor jump)
yTitle.observe(() => {
if (document.activeElement !== input) {
input.value = yTitle.toString();
}
});
// Sync from input → Yjs
input.addEventListener('input', () => {
ydoc.transact(() => {
yTitle.delete(0, yTitle.length);
yTitle.insert(0, input.value);
});
});
return wrapper;
}

View File

@@ -1,34 +0,0 @@
import * as Y from "yjs";
export function PollTitle(ydoc: Y.Doc, yTitle: Y.Text): HTMLElement {
const wrapper = document.createElement("div");
wrapper.className = "poll-title-wrapper";
const input = document.createElement("input");
input.type = "text";
input.id = "poll-title";
input.className = "poll-title-input";
input.placeholder = "Untitled Poll";
input.maxLength = 120;
input.setAttribute("aria-label", "Poll title");
input.value = yTitle.toString();
wrapper.appendChild(input);
// Sync from Yjs → input (only when not focused to avoid cursor jump)
yTitle.observe(() => {
if (document.activeElement !== input) {
input.value = yTitle.toString();
}
});
// Sync from input → Yjs
input.addEventListener("input", () => {
ydoc.transact(() => {
yTitle.delete(0, yTitle.length);
yTitle.insert(0, input.value);
});
});
return wrapper;
}

View File

@@ -0,0 +1,38 @@
import { roomName } from '../utils/store.js';
export function ShareSection() {
const url = `${window.location.origin}${window.location.pathname}?room=${encodeURIComponent(roomName)}`;
const section = document.createElement('div');
section.className = 'share-section';
section.innerHTML = `
<p class="share-label">Share this poll</p>
<div class="share-row">
<code class="share-url" title="${url}">${url}</code>
<button class="share-copy-btn">Copy link</button>
</div>
`;
const copyBtn = section.querySelector('.share-copy-btn');
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(url);
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('share-copy-btn--success');
setTimeout(() => {
copyBtn.textContent = 'Copy link';
copyBtn.classList.remove('share-copy-btn--success');
}, 2000);
} catch {
// Fallback: select the text
const range = document.createRange();
range.selectNode(section.querySelector('.share-url'));
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
}
});
return section;
}

View File

@@ -1,39 +0,0 @@
export function ShareSection(roomName: string): HTMLElement {
const url = `${window.location.origin}${window.location.pathname}?room=${encodeURIComponent(roomName)}`;
const section = document.createElement("div");
section.className = "share-section";
section.innerHTML = `
<p class="share-label">Share this poll</p>
<div class="share-row">
<code class="share-url" title="${url}">${url}</code>
<button class="share-copy-btn">Copy link</button>
</div>
`;
const copyBtn = section.querySelector<HTMLButtonElement>(".share-copy-btn")!;
copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(url);
copyBtn.textContent = "Copied!";
copyBtn.classList.add("share-copy-btn--success");
setTimeout(() => {
copyBtn.textContent = "Copy link";
copyBtn.classList.remove("share-copy-btn--success");
}, 2000);
} catch {
// Fallback: select the text
const range = document.createRange();
const urlEl = section.querySelector(".share-url");
if (urlEl) {
range.selectNode(urlEl);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
}
}
});
return section;
}

View File

@@ -0,0 +1,50 @@
import { provider } from '../utils/store.js';
export function StatusBar() {
const el = document.createElement('div');
el.className = 'status-bar';
const dot = document.createElement('span');
dot.className = 'status-dot connecting';
const statusText = document.createElement('span');
statusText.className = 'status-text';
statusText.textContent = 'Connecting';
const divider = document.createElement('span');
divider.className = 'status-divider';
divider.textContent = '·';
const peerText = document.createElement('span');
peerText.className = 'status-peers';
el.append(dot, statusText, divider, peerText);
// --- Connection state ---
let syncTimeout = setTimeout(() => {
statusText.textContent = 'Ready';
dot.className = 'status-dot ready';
}, 3000);
provider.on('synced', ({ synced }) => {
clearTimeout(syncTimeout);
dot.className = `status-dot ${synced ? 'connected' : 'connecting'}`;
statusText.textContent = synced ? 'Connected' : 'Connecting';
});
// --- Peer count ---
function updatePeerCount() {
const total = provider.awareness.getStates().size;
const others = total - 1;
peerText.textContent = others === 0
? 'Only you'
: `${others} other${others !== 1 ? 's' : ''}`;
}
provider.awareness.on('change', updatePeerCount);
updatePeerCount();
return el;
}

View File

@@ -1,67 +0,0 @@
import type { WebrtcProvider } from "y-webrtc";
export function StatusBar(provider: WebrtcProvider): HTMLElement {
const el = document.createElement("div");
el.className = "status-bar";
const dot = document.createElement("span");
dot.className = "status-dot connecting";
const statusText = document.createElement("span");
statusText.className = "status-text";
statusText.textContent = "Connecting";
const divider = document.createElement("span");
divider.className = "status-divider";
divider.textContent = "\u00b7";
const peerText = document.createElement("span");
peerText.className = "status-peers";
el.append(dot, statusText, divider, peerText);
// --- Connection state ---
let syncTimeout: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {
statusText.textContent = "Ready";
dot.className = "status-dot ready";
}, 3000);
provider.on("synced", ({ synced }: { synced: boolean }) => {
if (syncTimeout) {
clearTimeout(syncTimeout);
syncTimeout = undefined;
}
dot.className = `status-dot ${synced ? "connected" : "connecting"}`;
statusText.textContent = synced ? "Connected" : "Connecting";
});
// Online/offline awareness
const handleOffline = () => {
dot.className = "status-dot connecting";
statusText.textContent = "Offline";
};
const handleOnline = () => {
dot.className = "status-dot connecting";
statusText.textContent = "Reconnecting";
};
window.addEventListener("offline", handleOffline);
window.addEventListener("online", handleOnline);
// --- Peer count ---
function updatePeerCount() {
const total = provider.awareness.getStates().size;
const others = total - 1;
peerText.textContent =
others === 0
? "Only you"
: `${others} other${others !== 1 ? "s" : ""}`;
}
provider.awareness.on("change", updatePeerCount);
updatePeerCount();
return el;
}

View File

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

41
src/main.js Normal file
View File

@@ -0,0 +1,41 @@
import { StatusBar } from './components/StatusBar.js';
import { PollTitle } from './components/PollTitle.js';
import { AddOption } from './components/AddOption.js';
import { PollList } from './components/PollList.js';
import { ShareSection } from './components/ShareSection.js';
const app = document.getElementById('app');
// Header: logo + status
const header = document.createElement('header');
header.className = 'app-header';
const wordmark = document.createElement('div');
wordmark.className = 'app-wordmark';
wordmark.innerHTML = `
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="2" y="4" width="16" height="2.5" rx="1.25" fill="currentColor"/>
<rect x="2" y="8.75" width="11" height="2.5" rx="1.25" fill="currentColor" opacity="0.6"/>
<rect x="2" y="13.5" width="13" height="2.5" rx="1.25" fill="currentColor" opacity="0.35"/>
</svg>
<span>Polly</span>
`;
header.append(wordmark, StatusBar());
// Main card
const card = document.createElement('main');
card.className = 'app-card';
card.append(
PollTitle(),
AddOption(),
PollList(),
);
// Footer
const footer = document.createElement('footer');
footer.className = 'app-footer';
footer.appendChild(ShareSection());
app.append(header, card, footer);

View File

@@ -1,9 +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);

View File

@@ -1,207 +0,0 @@
import * as Y from "yjs";
// --- Types ---
export type ConnectionStatus = "connecting" | "connected" | "offline";
export interface OptionRecord {
id: string;
label: string;
createdAt: number;
createdBy: string;
}
export interface PollOptionViewModel extends OptionRecord {
voteCount: number;
isVotedByMe: boolean;
percentage: number;
}
export interface PollViewModel {
title: string;
roomId: string;
shareUrl: string;
connectionStatus: ConnectionStatus;
peerCount: number;
options: PollOptionViewModel[];
totalVotes: number;
myVoteOptionId: string | null;
deadline: number | null;
votingClosed: boolean;
}
// --- Helpers ---
export function createOptionId(): string {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `option-${Math.random().toString(36).slice(2, 10)}`;
}
function normalizeLabel(label: string): string {
return label.trim().replace(/\s+/g, " ");
}
export function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
// --- Title ---
export function getPollTitle(yTitle: Y.Text): string {
const title = yTitle.toString();
return title || "Untitled Poll";
}
// --- Options ---
export function addOption(
options: Y.Map<OptionRecord>,
rawLabel: string,
userId: string,
): { ok: true; optionId: string } | { ok: false; error: string } {
const label = normalizeLabel(rawLabel);
if (!label) {
return { ok: false, error: "Option 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, error: "That option already exists." };
}
const option: OptionRecord = {
id: createOptionId(),
label,
createdAt: Date.now(),
createdBy: userId,
};
options.set(option.id, option);
return { ok: true, optionId: option.id };
}
export function toggleVote(
votes: Y.Map<string>,
userId: string,
optionId: string,
): void {
const current = votes.get(userId);
if (current === optionId) {
votes.delete(userId);
} else {
votes.set(userId, optionId);
}
}
export function deleteOption(
options: Y.Map<OptionRecord>,
votes: Y.Map<string>,
optionId: string,
): void {
options.delete(optionId);
// Clean up votes pointing to this option
for (const [userId, votedOptionId] of votes.entries()) {
if (votedOptionId === optionId) {
votes.delete(userId);
}
}
}
// --- Deadline ---
export function setDeadline(
deadlineMap: Y.Map<unknown>,
durationMs: number,
): void {
deadlineMap.set("deadline", Date.now() + durationMs);
}
export function clearDeadline(deadlineMap: Y.Map<unknown>): void {
deadlineMap.delete("deadline");
}
export function getDeadline(deadlineMap: Y.Map<unknown>): number | null {
const val = deadlineMap.get("deadline");
return typeof val === "number" ? val : null;
}
// --- ViewModel ---
export function createViewModel(params: {
yTitle: Y.Text;
options: Y.Map<OptionRecord>;
votes: Y.Map<string>;
deadlineMap: Y.Map<unknown>;
roomId: string;
shareUrl: string;
connectionStatus: ConnectionStatus;
peerCount: number;
userId: string;
}): PollViewModel {
const {
yTitle,
options,
votes,
deadlineMap,
roomId,
shareUrl,
connectionStatus,
peerCount,
userId,
} = params;
// Tally votes per option
const tally = new Map<string, number>();
for (const optionId of votes.values()) {
tally.set(optionId, (tally.get(optionId) ?? 0) + 1);
}
let totalVotes = 0;
for (const count of tally.values()) {
totalVotes += count;
}
const myVoteOptionId = votes.get(userId) ?? null;
const deadline = getDeadline(deadlineMap);
const votingClosed = deadline !== null && Date.now() >= deadline;
// Sort by votes desc, then alphabetically
const sortedOptions = Array.from(options.values()).sort((a, b) => {
const aVotes = tally.get(a.id) ?? 0;
const bVotes = tally.get(b.id) ?? 0;
if (bVotes !== aVotes) return bVotes - aVotes;
return a.label.localeCompare(b.label);
});
return {
title: getPollTitle(yTitle),
roomId,
shareUrl,
connectionStatus,
peerCount,
myVoteOptionId,
totalVotes,
deadline,
votingClosed,
options: sortedOptions.map((option) => ({
...option,
voteCount: tally.get(option.id) ?? 0,
isVotedByMe: myVoteOptionId === option.id,
percentage:
totalVotes > 0
? Math.round(((tally.get(option.id) ?? 0) / totalVotes) * 100)
: 0,
})),
};
}

View File

@@ -21,7 +21,6 @@
--success: #2D7D46; --success: #2D7D46;
--danger: #C0392B; --danger: #C0392B;
--warning: #8c5300;
--radius-sm: 6px; --radius-sm: 6px;
--radius-md: 10px; --radius-md: 10px;
@@ -47,9 +46,6 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
button, input { font: inherit; }
button { cursor: pointer; }
/* ── Layout ────────────────────────────────────────────── */ /* ── Layout ────────────────────────────────────────────── */
#app { #app {
max-width: 580px; max-width: 580px;
@@ -134,7 +130,6 @@ button { cursor: pointer; }
/* ── Add Option ────────────────────────────────────────── */ /* ── Add Option ────────────────────────────────────────── */
.add-option-wrapper { .add-option-wrapper {
display: flex; display: flex;
flex-wrap: wrap;
gap: 0.625rem; gap: 0.625rem;
padding: 1.25rem 1.75rem; padding: 1.25rem 1.75rem;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
@@ -142,9 +137,9 @@ button { cursor: pointer; }
.add-option-input { .add-option-input {
flex: 1; flex: 1;
min-width: 0;
height: 2.5rem; height: 2.5rem;
padding: 0 0.875rem; padding: 0 0.875rem;
font-family: var(--font-body);
font-size: 0.9rem; font-size: 0.9rem;
color: var(--text-primary); color: var(--text-primary);
background: var(--bg); background: var(--bg);
@@ -173,12 +168,14 @@ button { cursor: pointer; }
gap: 0.375rem; gap: 0.375rem;
height: 2.5rem; height: 2.5rem;
padding: 0 1rem; padding: 0 1rem;
font-family: var(--font-body);
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
color: var(--accent-text); color: var(--accent-text);
background: var(--accent); background: var(--accent);
border: none; border: none;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer;
transition: opacity 0.15s; transition: opacity 0.15s;
white-space: nowrap; white-space: nowrap;
} }
@@ -186,60 +183,6 @@ button { cursor: pointer; }
.add-option-btn:hover { opacity: 0.85; } .add-option-btn:hover { opacity: 0.85; }
.add-option-btn:active { opacity: 0.7; } .add-option-btn:active { opacity: 0.7; }
.add-option-feedback {
width: 100%;
font-size: 0.8rem;
color: var(--danger);
display: none;
}
/* ── Deadline Timer ────────────────────────────────────── */
.deadline-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1.75rem;
border-bottom: 1px solid var(--border);
font-size: 0.85rem;
}
.deadline-timer {
flex: 1;
font-weight: 500;
}
.deadline-timer--active {
color: var(--success);
}
.deadline-timer--closed {
color: var(--danger);
font-weight: 600;
}
.deadline-btn {
height: 2rem;
padding: 0 0.875rem;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-secondary);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
transition: all 0.15s;
white-space: nowrap;
}
.deadline-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.deadline-btn--clear:hover {
border-color: var(--danger);
color: var(--danger);
}
/* ── Poll List ─────────────────────────────────────────── */ /* ── Poll List ─────────────────────────────────────────── */
.poll-list-wrapper { .poll-list-wrapper {
padding: 0.5rem 0; padding: 0.5rem 0;
@@ -333,33 +276,31 @@ button { cursor: pointer; }
.poll-option__vote-btn { .poll-option__vote-btn {
height: 1.875rem; height: 1.875rem;
padding: 0 0.875rem; padding: 0 0.875rem;
font-family: var(--font-body);
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
white-space: nowrap; white-space: nowrap;
background: transparent; background: transparent;
color: var(--text-secondary); color: var(--text-secondary);
border: 1px solid var(--border); border: 1px solid var(--border);
} }
.poll-option__vote-btn:hover:not(:disabled) { .poll-option__vote-btn:hover {
border-color: var(--accent); border-color: var(--accent);
color: var(--accent); color: var(--accent);
} }
.poll-option__vote-btn:disabled {
opacity: 0.5;
cursor: default;
}
.poll-option--voted .poll-option__vote-btn { .poll-option--voted .poll-option__vote-btn {
background: var(--accent); background: var(--accent);
color: var(--accent-text); color: var(--accent-text);
border-color: var(--accent); border-color: var(--accent);
} }
.poll-option--voted .poll-option__vote-btn:hover:not(:disabled) { .poll-option--voted .poll-option__vote-btn:hover {
opacity: 0.8; opacity: 0.8;
} }
@@ -373,6 +314,7 @@ button { cursor: pointer; }
border: none; border: none;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--text-muted); color: var(--text-muted);
cursor: pointer;
opacity: 0; opacity: 0;
transition: opacity 0.15s, color 0.15s, background 0.15s; transition: opacity 0.15s, color 0.15s, background 0.15s;
} }
@@ -431,12 +373,14 @@ button { cursor: pointer; }
.share-copy-btn { .share-copy-btn {
height: 2rem; height: 2rem;
padding: 0 0.875rem; padding: 0 0.875rem;
font-family: var(--font-body);
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
color: var(--text-secondary); color: var(--text-secondary);
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
@@ -461,7 +405,6 @@ button { cursor: pointer; }
.poll-option__content { padding: 0.875rem 1.25rem; } .poll-option__content { padding: 0.875rem 1.25rem; }
.poll-list-meta { padding: 0.5rem 1.25rem 0.625rem; } .poll-list-meta { padding: 0.5rem 1.25rem 0.625rem; }
.poll-list-empty { padding: 2.5rem 1.25rem; } .poll-list-empty { padding: 2.5rem 1.25rem; }
.deadline-wrapper { padding: 0.75rem 1.25rem; }
.poll-option__count { display: none; } .poll-option__count { display: none; }

View File

@@ -1,77 +0,0 @@
import { IndexeddbPersistence } from "y-indexeddb";
import { WebrtcProvider } from "y-webrtc";
import * as Y from "yjs";
import type { ConnectionStatus, OptionRecord } from "./state";
export interface AppSync {
doc: Y.Doc;
yTitle: Y.Text;
options: Y.Map<OptionRecord>;
votes: Y.Map<string>;
deadlineMap: Y.Map<unknown>;
provider: WebrtcProvider;
persistence: IndexeddbPersistence;
getConnectionStatus: () => ConnectionStatus;
getPeerCount: () => number;
destroy: () => void;
}
export function initSync(roomId: string): AppSync {
const doc = new Y.Doc();
const yTitle = doc.getText("poll-title");
const options = doc.getMap<OptionRecord>("poll-options");
const votes = doc.getMap<string>("poll-votes");
const deadlineMap = doc.getMap<unknown>("poll-deadline");
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");
});
provider.on("synced", ({ synced }: { synced: boolean }) => {
if (synced) syncConnectionStatus("connected");
});
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return {
doc,
yTitle,
options,
votes,
deadlineMap,
provider,
persistence,
getConnectionStatus: () => connectionStatus,
getPeerCount: () => {
const total = provider.awareness.getStates().size;
return Math.max(0, total - 1);
},
destroy: () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
persistence.destroy();
provider.destroy();
doc.destroy();
},
};
}

79
src/utils/store.js Normal file
View File

@@ -0,0 +1,79 @@
import * as Y from 'yjs';
import { WebrtcProvider } from 'y-webrtc';
// --- Peer ID (stable across reloads) ---
function getOrCreatePeerId() {
let id = localStorage.getItem('peer-id');
if (!id) {
id = crypto.randomUUID();
localStorage.setItem('peer-id', id);
}
return id;
}
// --- Room name from URL ---
function getRoomName() {
const params = new URLSearchParams(window.location.search);
return params.get('room') || 'default-poll';
}
// --- Yjs setup ---
export const peerId = getOrCreatePeerId();
export const roomName = getRoomName();
export const ydoc = new Y.Doc();
export const provider = new WebrtcProvider(roomName, ydoc);
export const yOptions = ydoc.getMap('poll-options');
export const yTitle = ydoc.getText('poll-title');
// --- Data operations ---
export function addOption(name) {
const id = crypto.randomUUID();
const optionMap = new Y.Map();
optionMap.set('name', name);
optionMap.set('votes', new Y.Map());
yOptions.set(id, optionMap);
}
export function toggleVote(optionId) {
const optionMap = yOptions.get(optionId);
if (!optionMap) return;
const votes = optionMap.get('votes');
if (votes.has(peerId)) {
votes.delete(peerId);
} else {
votes.set(peerId, true);
}
}
export function deleteOption(optionId) {
yOptions.delete(optionId);
}
// --- Derived read helpers ---
export function getEntries() {
const entries = [];
yOptions.forEach((optionMap, id) => {
entries.push({
id,
name: optionMap.get('name'),
votes: optionMap.get('votes').size,
voted: optionMap.get('votes').has(peerId),
});
});
entries.sort((a, b) => b.votes - a.votes || a.name.localeCompare(b.name));
return entries;
}
export function getTotalVotes() {
let total = 0;
yOptions.forEach((optionMap) => {
total += optionMap.get('votes').size;
});
return total;
}

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"useDefineForClassFields": true
},
"include": ["src"]
}