diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d246d8d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Git files +.git +.gitignore + +# Node modules - these are installed within the Docker build context +node_modules + +# Docker specific files (if any, other than Dockerfile itself) +# .dockerignore (to avoid including itself if context changes) + +# Local development environment files +.env +.env*.local + +# Logs and temporary files +logs +*.log +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* + +# OS-specific files +.DS_Store +Thumbs.db + +# IDE configuration +.idea/ +.vscode/ +*.sublime-workspace +*.sublime-project + +# Build output (if you ever build locally before Docker) +dist/ +# If your build output is different, change the line above + +# Coverage reports +coverage/ +.nyc_output/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5613601 --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarnclean + +# dotenv environment variables file +.env +.env*.local +.env.development.local +.env.test.local +.env.production.local + +# parcel-bundler cache files +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build output +.nuxt +dist + +# Svelte Sapper build output +__sapper__ + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Vite local development server cache +.vite + +# Vite build output directory +/dist +# If your build output is different, change the line above to /your-build-output-dir + +# Mac OS system files +.DS_Store +Thumbs.db + +# IDE specific +.idea/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sublime-workspace +*.sublime-project + +# Editor directories and files +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36b0b91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Stage 1: Build the Vue.js application +FROM node:24-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Stage 2: Serve the application with Nginx +FROM nginx:stable-alpine +COPY ./nginx.conf /etc/nginx/conf.d/default.conf + +RUN find /usr/share/nginx/html -mindepth 1 -delete +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README.md b/README.md index ff454c2..9b5ab6e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,139 @@ -# OrreryTimer +# Nexus Timer 🕰️✨ -OrreryTimer is a dynamic multi-player timer designed for games, workshops, or any activity where turns pass sequentially in a circular fashion. It provides a clear visual focus on the current participant, their immediate predecessor and successor, and the direction of play, ensuring everyone stays engaged and aware of the flow. \ No newline at end of file +Nexus Timer is a dynamic multi-player timer designed for games, workshops, or any activity where turns pass sequentially in a circular fashion. It provides a clear visual focus on the current participant and their immediate successor, ensuring everyone stays engaged and aware of who is next. This document serves as a detailed specification for a Progressive Web App (PWA) prototype aimed at game enthusiasts. + +## Core Concept + +Nexus Timer visualizes players in a circular sequence. The **Current Player** is prominently displayed in the top half of the screen, and the **Next Player** is in the bottom half. This clear visual pairing indicates the flow of turns, making it perfect for board games, role-playing games, timed presentations, or any scenario needing structured turn management with individual countdowns. + +## Target Audience + +Game enthusiasts who play turn-based games (board games, tabletop RPGs, card games) and need a visually clear and customizable timer solution. + +## Tech Stack + +* **HTML5:** For structuring the user interface. +* **CSS3:** For styling and visual presentation, including animations. Consider a CSS framework like Tailwind CSS for rapid prototyping. +* **JavaScript:** For application logic, timer functionality, and event handling. +* **Web Audio API:** For audio feedback (ticking sounds, alerts). +* **Browser API:** For capturing Players' photo. +* **Local Storage/IndexedDB:** For persistent storage of player data, timer states, and settings. +* **Service Worker:** Essential for PWA functionality (offline access, push notifications - potential future feature). +* **Manifest File:** Defines the PWA's metadata (name, icons, theme color). +* **Web Framework:** Use Vue.js +* **(Optional) Tailwind CSS:** Utility-first CSS framework for rapid UI development. + +## Hardware Recommendations (Optional Enhancement) + +For an enhanced tactile experience, Nexus Timer supports Smart Buttons based on Bluetooth-connected microcontroller (e.g., XIAO nRF52840) implementing HID (Human Interface Device) protocol. + +* **Buttons:** Connect 3 physical buttons, potentially extended (e.g., via 1.5m wires) for easy player access. +* **Configuration:** + * **Player 1's Button:** Single Click: Emulates a key press (e.g., 'a'). Configure this as Player 1's "Pass Turn / My Pause" hotkey in the app. + * **Player 2's Button:** Single Click: Emulates a key press (e.g., 'b'). Configure as Player 2's "Pass Turn / My Pause" hotkey. + * If Player 3 is Game Admin: + * **Player 3's Button:** Single Click: Emulates a key press (e.g., 'c'). Configure as Player 3's "Pass Turn / My Pause" hotkey. + * **Player 3's Button:** Double Click: Emulates a key press (e.g., 'x'). Configure as the "Global Run All Timers" hotkey in the app. + * **Player 3's Button:** Long Press: Emulates a key press (e.g., 's'). Configure as the "Global Stop/Pause All" hotkey in the app. + +## Key Features + +* **Circular Player Display:** + 1. **Normal Mode (Default):** Central focus on the **Current Player** (top) and **Next Player** (bottom). + 2. **All Timers Running Mode:** List of players with running timers. +* **Individual Player Timers:** + * Customizable countdown timer (MM:SS) for each player. + * Timers continue into **negative time** (e.g., -MM:SS) up to a limit (e.g., -59:59). + * Players reaching max negative time are **skipped** until reset or timer edit. + * Visual feedback: Pulsating effect for active timers (background for positive, text for negative). Skipped players are visually distinct (e.g., greyed out). +* **Two Game Modes:** + 1. **Normal Mode (Default):** + * Only the **Current Player's** timer runs. + * Pass the turn via **Swipe Up** on the Next Player's area or the Current Player's "Pass Turn / My Pause" hotkey. + * 3-seconds ticking sound when the timer starts to alert players about the "Pass Turn". + * Tap Current Player's area or use "Global Stop/Pause All" hotkey to pause/resume their timer without passing the turn. + * To pass the turn: **Swipe Up** on the Next Player's area or have the Current Player press their "Pass Turn / My Pause" hotkey. The current timer pauses, the next player becomes Current, and their timer starts. + * If the current player's timer is paused, and then the turn is passed (via swipe up or hotkey), the next player should become the current player, but their timer should not automatically start. It should remain paused. + 2. **All Timers Running Mode:** + * All active player timers run simultaneously. + * Enter by clicking "All Timers Mode" (starts all timers). + * Continuous ticking sound when active. + * Initially, all players are shown in a list with their photo, name and timer value. + * Tapping on a player in the list pauses its timer. Any player can use their "Pass Turn / My Pause" hotkey to pause their *own* timer. + * Only players with a running timer are shown in the list. + * If all players pause their timers, automatically reverts to Normal Mode. + * Main button: "Stop All Timers" (pauses all, returns to Normal Mode) or "Start All Timers" (resumes all in this mode). Can also be triggered by "Global Stop/Pause All" hotkey. +* **Player Management:** + * Add, edit, and delete players (2-7 players). + * Use device camera (access via browser API) or default avatars for the player's picture. + * Set initial timer values per player (Default: 60:00). + * Assign unique "Pass Turn / My Pause" hotkeys (single keypresses). E.g.: Use the Player's 1 "single click" action to insert the key. + * Assign the "Global Stop/Pause All" hotkey (single keypresses). E.g.: Use the Player's 3 "long press" action to insert the key. + * Assign the "Run All Timers" hotkey (single keypresses). E.g.: Use the Player's 3 "double click" action to insert the key. + * Re-order players (drag-and-drop planned), reverse, shuffle. +* **Intuitive Controls:** + * **Swipe Up:** (On the Next Player's area). Primary gesture for passing turns (Normal Mode). + * **On-Screen Taps:** (On the Current Player's area). For pausing/resuming timers contextually. +* **Audio Feedback:** + * Continuous ticking in "All Timers Running Mode" when active. + * Brief 3-second tick when a timer starts in Normal Mode. Cancel the sound when the timer pauses. + * Global mute option. +* **Visuals:** + * Designed for mobile phone screens (portrait orientation). + * Light/Dark theme options. +* **Persistence:** Player setups, timer states, and settings are saved locally using browser Local Storage. +* **Global Reset:** "Reset Game" button restores all timers to initial values and resets game state. + +## UI/UX Considerations (For AI Generation) + +* **Minimalist Design:** Focus on clarity and ease of use. Avoid clutter. +* **Large, Clear Timers:** Timers should be easily readable at a glance. +* **Color Coding:** Use color to indicate timer state (e.g., green for running, red for negative time, grey for skipped). +* **Responsive Layout:** The UI should adapt to different (mobile phone) screen sizes. +* **Touch-Friendly:** Buttons and interactive elements should be large enough for easy tapping. + +## Data Model (For AI Generation) + +```json +{ + "players": [ + { + "id": "1", + "name": "Player 1", + "avatar": null, + "initialTimerSec": 3600, + "currentTimerSec": 3600, + "hotkey": "a", + "isSkipped": false + }, + { + "id": "2", + "name": "Player 2", + "avatar": null, + "initialTimerSec": 3600, + "currentTimerSec": 3600, + "hotkey": "b", + "isSkipped": false + } + ], + "globalHotkeyStopPause": "s", + "globalHotkeyRunAll": "x", + "currentPlayerIndex": 0, + "gameMode": "normal", // "normal" or "allTimers" + "isMuted": false, + "theme": "dark" +} +``` +## Building for the production +Build the docker image: +```bash +docker build -t nexus-timer . +``` +Run the app in the container: +```bash +docker run --rm -p 8080:80 nexus-timer +``` +Test the web app: +```bash +curl http://localhost:8080/ +``` diff --git a/assets/favicon-16x16.png b/assets/favicon-16x16.png new file mode 100644 index 0000000..8d52034 Binary files /dev/null and b/assets/favicon-16x16.png differ diff --git a/assets/favicon-32x32.png b/assets/favicon-32x32.png new file mode 100644 index 0000000..ec20525 Binary files /dev/null and b/assets/favicon-32x32.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..8be2b52 --- /dev/null +++ b/index.html @@ -0,0 +1,30 @@ + + + + + + + Nexus Timer + + + + + +
+ + + + \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..eb2149a --- /dev/null +++ b/nginx.conf @@ -0,0 +1,12 @@ +server { + listen 80; + listen [::]:80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..955b242 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2281 @@ +{ + "name": "nexus-timer", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nexus-timer", + "version": "0.0.1", + "dependencies": { + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "vuex": "^4.0.2" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.16", + "@vitejs/plugin-vue": "^4.2.3", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.27", + "tailwindcss": "^3.3.3", + "vite": "^4.4.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.150", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz", + "integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vuex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", + "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==", + "dependencies": { + "@vue/devtools-api": "^6.0.0-beta.11" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..23676a9 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "nexus-timer", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "vuex": "^4.0.2" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.16", + "@vitejs/plugin-vue": "^4.2.3", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.27", + "tailwindcss": "^3.3.3", + "vite": "^4.4.5" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..70d778e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, + } \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..d8045d9 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icons/icon-192x192.png b/public/icons/icon-192x192.png new file mode 100644 index 0000000..7fabb38 Binary files /dev/null and b/public/icons/icon-192x192.png differ diff --git a/public/icons/icon-512x512.png b/public/icons/icon-512x512.png new file mode 100644 index 0000000..6c47bef Binary files /dev/null and b/public/icons/icon-512x512.png differ diff --git a/public/icons/maskable-icon-192x192.png b/public/icons/maskable-icon-192x192.png new file mode 100644 index 0000000..7fabb38 Binary files /dev/null and b/public/icons/maskable-icon-192x192.png differ diff --git a/public/icons/maskable-icon-512x512.png b/public/icons/maskable-icon-512x512.png new file mode 100644 index 0000000..6c47bef Binary files /dev/null and b/public/icons/maskable-icon-512x512.png differ diff --git a/public/icons/shortcut-info-96x96.png b/public/icons/shortcut-info-96x96.png new file mode 100644 index 0000000..2fb5877 Binary files /dev/null and b/public/icons/shortcut-info-96x96.png differ diff --git a/public/icons/shortcut-setup-96x96.png b/public/icons/shortcut-setup-96x96.png new file mode 100644 index 0000000..75589b6 Binary files /dev/null and b/public/icons/shortcut-setup-96x96.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..c933fc7 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,57 @@ +{ + "name": "Nexus Timer - Multiplayer Turn Timer", + "short_name": "NexusTimer", + "description": "Dynamic multiplayer timer for games, workshops, or sequential turns. Focuses on current & next player.", // Add a description + "start_url": "/", + "scope": "/", + "display": "standalone", + "display_override": ["window-controls-overlay"], + "background_color": "#1f2937", + "theme_color": "#3b82f6", + "orientation": "portrait", + "version": "0.0.1", + "version_name": "0.0.1", + "lang": "en-US", + "icons": [ + { + "src": "/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/maskable-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/maskable-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "shortcuts": [ + { + "name": "Game Setup", + "short_name": "Setup", + "description": "Configure players and settings", + "url": "/?utm_source=homescreen", + "icons": [{ "src": "/icons/shortcut-setup-96x96.png", "sizes": "96x96" }] + }, + { + "name": "About Nexus Timer", + "short_name": "About", + "description": "Information about the app", + "url": "/info?utm_source=homescreen", + "icons": [{ "src": "/icons/shortcut-info-96x96.png", "sizes": "96x96" }] + } + ] +} \ No newline at end of file diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..1072a56 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,154 @@ +const CACHE_VERSION = 'nexus-timer-cache-v3.3'; +const APP_SHELL_URLS = [ + // '/', // Let NetworkFirst handle '/' + '/manifest.json', + '/favicon.ico', + '/icons/icon-192x192.png', + '/icons/icon-512x512.png', + '/icons/maskable-icon-192x192.png', + '/icons/maskable-icon-512x512.png', + '/icons/shortcut-setup-96x96.png', + '/icons/shortcut-info-96x96.png', + // Add Vite output paths? Usually hard due to hashing. Rely on dynamic caching. +]; + +self.addEventListener('install', event => { + console.log(`[SW ${CACHE_VERSION}] Install`); + event.waitUntil( + caches.open(CACHE_VERSION) + .then(cache => { + console.log(`[SW ${CACHE_VERSION}] Caching app shell essentials`); + return cache.addAll(APP_SHELL_URLS); // Cache core static assets + }) + .then(() => self.skipWaiting()) + ); +}); + +self.addEventListener('activate', event => { + console.log(`[SW ${CACHE_VERSION}] Activate`); + event.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheName !== CACHE_VERSION) { + console.log(`[SW ${CACHE_VERSION}] Deleting old cache: ${cacheName}`); + return caches.delete(cacheName); + } + }) + ); + }).then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', event => { + const { request } = event; + const url = new URL(request.url); + + // Ignore non-GET requests & non-http protocols + if (request.method !== 'GET' || !url.protocol.startsWith('http')) { + return; + } + + // --- Strategy 1: Network First for HTML --- + if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) { + event.respondWith( + fetch(request) + .then(response => { + // If fetch is successful, cache it and return it + if (response.ok) { + const responseClone = response.clone(); + caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); + } + return response; + }) + .catch(async () => { + // If fetch fails, try cache + const cachedResponse = await caches.match(request); + if (cachedResponse) { + return cachedResponse; + } + // If specific request not in cache, try root '/' as SPA fallback + const rootCache = await caches.match('/'); + if (rootCache) { + return rootCache; + } + // Optional: return a proper offline fallback page if available + // return caches.match('/offline.html'); + // Or just let the browser show its offline error + return new Response('Network error and no cache found.', { status: 404, statusText: 'Not Found' }); + }) + ); + return; + } + + // --- Strategy 2: Stale-While-Revalidate for assets --- + if (request.destination === 'style' || + request.destination === 'script' || + request.destination === 'worker' || + request.destination === 'image' || + request.destination === 'font') { + event.respondWith( + caches.open(CACHE_VERSION).then(cache => { // Open cache first + return cache.match(request).then(cachedResponse => { + // Fetch in parallel (don't wait for it here) + const fetchPromise = fetch(request).then(networkResponse => { + // Check if fetch was successful + if (networkResponse.ok) { + // Clone before caching + const responseToCache = networkResponse.clone(); + // Update the cache with the network response + cache.put(request, responseToCache); + } else { + console.warn(`[SW ${CACHE_VERSION}] Fetch for ${request.url} failed with status ${networkResponse.status}`); + } + // Return the original network response for the fetch promise resolution + // This isn't directly used for the *initial* response unless cache misses. + return networkResponse; + }).catch(err => { + console.warn(`[SW ${CACHE_VERSION}] Fetch error for ${request.url}:`, err); + // If fetch fails, we just rely on the cache if it existed. + // Re-throw error so if cache also missed, browser knows resource failed. + throw err; + }); + + // Return the cached response immediately if available. + // If not cached, this strategy requires waiting for the fetch. + // The common implementation returns cache THEN fetches, but if cache misses, + // the user waits for the network. Let's return fetchPromise if no cache. + return cachedResponse || fetchPromise; + + }).catch(err => { + // Error during cache match or subsequent fetch + console.error(`[SW ${CACHE_VERSION}] Error handling fetch for ${request.url}:`, err); + // Fallback to network just in case cache interaction failed? Or let browser handle. + // Let's try network as a last resort if cache fails. + return fetch(request); + }); + }) + ); + return; + } + + // --- Strategy 3: Cache First for others --- + // (e.g., manifest, potentially icons not pre-cached) + event.respondWith( + caches.match(request) + .then(response => { + return response || fetch(request).then(networkResponse => { + if(networkResponse.ok) { + const responseClone = networkResponse.clone(); + caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); + } + return networkResponse; + }); + }) + ); +}); + +// Listener for skipWaiting message +self.addEventListener('message', event => { + if (event.data && event.data.action === 'skipWaiting') { + console.log('[SW] Received skipWaiting message, activating new SW.'); + self.skipWaiting(); + } +}); \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..6a0db8a --- /dev/null +++ b/src/App.vue @@ -0,0 +1,227 @@ + + + + + \ No newline at end of file diff --git a/src/assets/default-avatar.png b/src/assets/default-avatar.png new file mode 100644 index 0000000..6812f6f Binary files /dev/null and b/src/assets/default-avatar.png differ diff --git a/src/assets/tailwind.css b/src/assets/tailwind.css new file mode 100644 index 0000000..6f7e610 --- /dev/null +++ b/src/assets/tailwind.css @@ -0,0 +1,43 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 antialiased select-none; + overscroll-behavior-y: contain; /* Prevents pull-to-refresh on mobile */ +} + +/* Basic button styling */ +.btn { + @apply px-4 py-2 rounded font-semibold focus:outline-none focus:ring-2 focus:ring-opacity-50; +} +.btn-primary { + @apply bg-blue-500 hover:bg-blue-600 text-white focus:ring-blue-400; +} +.btn-secondary { + @apply bg-gray-500 hover:bg-gray-600 text-white focus:ring-gray-400; +} +.btn-danger { + @apply bg-red-500 hover:bg-red-600 text-white focus:ring-red-400; +} +.btn-warning { + @apply bg-yellow-500 hover:bg-yellow-600 text-black focus:ring-yellow-400; +} +.btn-icon { + @apply p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700; +} + +/* Input styling */ +.input-base { + @apply mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm; +} + +/* For preventing text selection during swipes/taps */ +.no-select { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ +} \ No newline at end of file diff --git a/src/components/DefaultAvatarIcon.vue b/src/components/DefaultAvatarIcon.vue new file mode 100644 index 0000000..a67623e --- /dev/null +++ b/src/components/DefaultAvatarIcon.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/components/PlayerDisplay.vue b/src/components/PlayerDisplay.vue new file mode 100644 index 0000000..ea49b63 --- /dev/null +++ b/src/components/PlayerDisplay.vue @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/src/components/PlayerForm.vue b/src/components/PlayerForm.vue new file mode 100644 index 0000000..bdd5453 --- /dev/null +++ b/src/components/PlayerForm.vue @@ -0,0 +1,231 @@ + + + \ No newline at end of file diff --git a/src/components/PlayerListItem.vue b/src/components/PlayerListItem.vue new file mode 100644 index 0000000..de4b31b --- /dev/null +++ b/src/components/PlayerListItem.vue @@ -0,0 +1,70 @@ + + + \ No newline at end of file diff --git a/src/components/TimerDisplay.vue b/src/components/TimerDisplay.vue new file mode 100644 index 0000000..f705405 --- /dev/null +++ b/src/components/TimerDisplay.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..50523cb --- /dev/null +++ b/src/main.js @@ -0,0 +1,26 @@ +// src/main.js +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' // router will be initialized here +import store from './store' // store will be initialized here +import './assets/tailwind.css' + +const app = createApp(App) + +// Dispatch loadState immediately after store is created and before app is mounted +// and before router is fully used by the app. +store.dispatch('loadState').then(() => { + // Now that the state is loaded (or attempted to be loaded), + // we can safely use the router and mount the app. + app.use(router) + app.use(store) // Using store here is fine, it's already created. + app.mount('#app') +}).catch(error => { + console.error("Failed to load initial state for the store:", error); + // Fallback: Mount the app even if state loading fails, guards should handle it. + // Or display an error message to the user. + // For now, let's still try to mount. + app.use(router) + app.use(store) + app.mount('#app') +}); \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..1dd1032 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,59 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import SetupView from '../views/SetupView.vue'; +import GameView from '../views/GameView.vue'; +import InfoView from '../views/InfoView.vue'; +import store from '../store'; + +const routes = [ + { + path: '/', + name: 'Setup', + component: SetupView, + beforeEnter: (to, from, next) => { + // Check if we are navigating FROM the Game view. + // If so, the user explicitly clicked the Setup button, so allow it. + if (from.name === 'Game') { + console.log('Router Guard: Allowing navigation from Game to Setup.'); + next(); // Allow navigation to Setup + return; // Stop further processing of this guard + } + + // Original logic for initial load or other navigations TO Setup: + if (store.state.players && store.state.players.length >= 2) { + // If 2 or more players exist (and not coming from Game), redirect to Game. + console.log('Router Guard: Players found, redirecting to Game (not coming from Game).'); + next({ name: 'Game', replace: true }); + } else { + // Otherwise (fewer than 2 players), allow navigation to the Setup view. + console.log('Router Guard: Not enough players, proceeding to Setup.'); + next(); + } + } + }, + { + path: '/game', + name: 'Game', + component: GameView, + beforeEnter: (to, from, next) => { + // Keep this guard: prevent direct access to /game without enough players + if (!store.state.players || store.state.players.length < 2) { + console.log('Router Guard: Attempted to access Game without enough players, redirecting to Setup.'); + next({ name: 'Setup' }); + } else { + next(); + } + } + }, + { + path: '/info', + name: 'Info', + component: InfoView + } +]; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes +}); + +export default router; \ No newline at end of file diff --git a/src/services/AudioService.js b/src/services/AudioService.js new file mode 100644 index 0000000..ed4ab17 --- /dev/null +++ b/src/services/AudioService.js @@ -0,0 +1,133 @@ +let audioContext; +let tickSoundBuffer; // For short tick +let passTurnSoundBuffer; // For 3s pass turn alert +let isMutedGlobally = false; +let continuousTickInterval = null; // For setInterval based continuous ticking +let passTurnSoundTimeout = null; + +function getAudioContext() { + if (!audioContext && (window.AudioContext || window.webkitAudioContext)) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + return audioContext; +} + +function createBeepBuffer(frequency = 440, duration = 0.1, type = 'sine') { + const ctx = getAudioContext(); + if (!ctx) return null; + const sampleRate = ctx.sampleRate; + const numFrames = duration * sampleRate; + const buffer = ctx.createBuffer(1, numFrames, sampleRate); + const data = buffer.getChannelData(0); + const gain = 0.1; // Reduce gain to make beeps softer + + for (let i = 0; i < numFrames; i++) { + // Simple fade out + const currentGain = gain * (1 - (i / numFrames)); + if (type === 'square') { + data[i] = (Math.sin(2 * Math.PI * frequency * (i / sampleRate)) >= 0 ? 1 : -1) * currentGain; + } else { // sine + data[i] = Math.sin(2 * Math.PI * frequency * (i / sampleRate)) * currentGain; + } + } + return buffer; +} + +async function initSounds() { + const ctx = getAudioContext(); + if (!ctx) return; + // Tick sound (shorter, slightly different pitch) + if (!tickSoundBuffer) { + // Using a square wave for a more 'digital' tick, short duration + tickSoundBuffer = createBeepBuffer(1000, 0.03, 'square'); + } + // Pass turn alert sound (3 beeps) + if (!passTurnSoundBuffer) { + passTurnSoundBuffer = createBeepBuffer(660, 0.08, 'sine'); + } +} +initSounds(); + +function playSoundBuffer(buffer) { + if (isMutedGlobally || !buffer || !audioContext || audioContext.state === 'suspended') return; + const source = audioContext.createBufferSource(); + source.buffer = buffer; + source.connect(audioContext.destination); + source.start(); +} + +export const AudioService = { + setMuted(muted) { + isMutedGlobally = muted; + if (muted) { + this.stopContinuousTick(); + this.cancelPassTurnSound(); + } + }, + + // This is the single, short tick sound for "All Timers Running" mode. + _playSingleTick() { + playSoundBuffer(tickSoundBuffer); + }, + + playPassTurnAlert() { + if (isMutedGlobally || !audioContext || audioContext.state === 'suspended') return; + this.cancelPassTurnSound(); + + let count = 0; + const playAndSchedule = () => { + if (count < 3 && !isMutedGlobally && audioContext.state !== 'suspended') { + playSoundBuffer(passTurnSoundBuffer); + count++; + passTurnSoundTimeout = setTimeout(playAndSchedule, 1000); // Beep every second for 3s + } else { + passTurnSoundTimeout = null; + } + }; + playAndSchedule(); + }, + + cancelPassTurnSound() { + if (passTurnSoundTimeout) { + clearTimeout(passTurnSoundTimeout); + passTurnSoundTimeout = null; + } + }, + + startContinuousTick() { + this.stopContinuousTick(); // Clear any existing interval + if (isMutedGlobally || !audioContext || audioContext.state === 'suspended') return; + + // Play immediately once, then set interval + this._playSingleTick(); + continuousTickInterval = setInterval(() => { + if (!isMutedGlobally && audioContext.state !== 'suspended') { + this._playSingleTick(); + } else { + this.stopContinuousTick(); // Stop if muted or context suspended during interval + } + }, 1000); // Tick every second + }, + + stopContinuousTick() { + if (continuousTickInterval) { + clearInterval(continuousTickInterval); + continuousTickInterval = null; + } + // Ensure no rogue oscillators are playing. + // If an oscillator was ever used directly and not disconnected, it could persist. + // The current implementation relies on BufferSource which stops automatically. + }, + + resumeContext() { + const ctx = getAudioContext(); + if (ctx && ctx.state === 'suspended') { + ctx.resume().then(() => { + console.log("AudioContext resumed successfully."); + initSounds(); // Re-initialize sounds if context was suspended for long + }).catch(e => console.error("Error resuming AudioContext:", e)); + } else if (ctx && !tickSoundBuffer) { // If context was fine but sounds not loaded + initSounds(); + } + } +}; \ No newline at end of file diff --git a/src/services/CameraService.js b/src/services/CameraService.js new file mode 100644 index 0000000..b15a32a --- /dev/null +++ b/src/services/CameraService.js @@ -0,0 +1,69 @@ +export const CameraService = { + async getPhoto() { + return new Promise(async (resolve, reject) => { + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + reject(new Error('Camera API not available.')); + return; + } + + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false }); + + // Create a modal or an overlay to show the video stream and a capture button + const videoElement = document.createElement('video'); + videoElement.srcObject = stream; + videoElement.setAttribute('playsinline', ''); // Required for iOS + videoElement.style.cssText = "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 90%; max-height: 70vh; z-index: 1001; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.2);"; + + const captureButton = document.createElement('button'); + captureButton.textContent = 'Capture'; + captureButton.style.cssText = "position: fixed; bottom: 10%; left: 50%; transform: translateX(-50%); z-index: 1002; padding: 12px 24px; background-color: #3b82f6; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer;"; + + const closeButton = document.createElement('button'); + closeButton.textContent = 'Cancel'; + closeButton.style.cssText = "position: fixed; top: 10px; right: 10px; z-index: 1002; padding: 8px 12px; background-color: #ef4444; color: white; border: none; border-radius: 8px; font-size: 14px; cursor: pointer;"; + + + const overlay = document.createElement('div'); + overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 1000;"; + + document.body.appendChild(overlay); + document.body.appendChild(videoElement); + document.body.appendChild(captureButton); + document.body.appendChild(closeButton); + + videoElement.onloadedmetadata = () => { + videoElement.play(); + }; + + const cleanup = () => { + stream.getTracks().forEach(track => track.stop()); + document.body.removeChild(videoElement); + document.body.removeChild(captureButton); + document.body.removeChild(closeButton); + document.body.removeChild(overlay); + }; + + captureButton.onclick = () => { + const canvas = document.createElement('canvas'); + canvas.width = videoElement.videoWidth; + canvas.height = videoElement.videoHeight; + const context = canvas.getContext('2d'); + context.drawImage(videoElement, 0, 0, canvas.width, canvas.height); + const dataUrl = canvas.toDataURL('image/png'); + cleanup(); + resolve(dataUrl); + }; + + closeButton.onclick = () => { + cleanup(); + reject(new Error('User cancelled photo capture.')); + }; + + } catch (err) { + console.error("Error accessing camera: ", err); + reject(err); + } + }); + } + }; \ No newline at end of file diff --git a/src/services/StorageService.js b/src/services/StorageService.js new file mode 100644 index 0000000..7dbf34d --- /dev/null +++ b/src/services/StorageService.js @@ -0,0 +1,23 @@ +const STORAGE_KEY = 'nexusTimerState'; + +export const StorageService = { + getState() { + const savedState = localStorage.getItem(STORAGE_KEY); + if (savedState) { + try { + return JSON.parse(savedState); + } catch (e) { + console.error("Error parsing saved state from localStorage", e); + localStorage.removeItem(STORAGE_KEY); // Clear corrupted data + return null; + } + } + return null; + }, + saveState(state) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + }, + clearState() { + localStorage.removeItem(STORAGE_KEY); + } +}; \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..f5715ac --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,460 @@ +import { createStore } from 'vuex'; +import { StorageService } from '../services/StorageService'; +import { parseTime, formatTime } from '../utils/timeFormatter'; + +const MAX_NEGATIVE_SECONDS = -(59 * 60 + 59); // -59:59 +const DEFAULT_AVATAR_MARKER = null; // Ensure this is defined + +// Define predefined players +const predefinedPlayers = [ + { + id: 'predefined-1', // Unique ID for predefined player 1 + name: 'Player 1', + avatar: DEFAULT_AVATAR_MARKER, // Or a specific avatar path if you have one + initialTimerSec: 60 * 60, // 60:00 + currentTimerSec: 60 * 60, + hotkey: '1', // Hotkey '1' + isSkipped: false, + isTimerRunning: false, + }, + { + id: 'predefined-2', // Unique ID for predefined player 2 + name: 'Player 2', + avatar: DEFAULT_AVATAR_MARKER, // Or a specific avatar path + initialTimerSec: 60 * 60, // 60:00 + currentTimerSec: 60 * 60, + hotkey: '2', // Hotkey '2' + isSkipped: false, + isTimerRunning: false, + } +]; + +const initialState = { + players: JSON.parse(JSON.stringify(predefinedPlayers)), // Start with predefined players (deep copy) + globalHotkeyStopPause: null, + globalHotkeyRunAll: null, + currentPlayerIndex: 0, + gameMode: 'normal', + isMuted: false, + theme: 'dark', + gameRunning: false, +}; + +export default createStore({ + state: () => { // This function already loads from storage ONCE during store creation + const persistedState = StorageService.getState(); + if (persistedState) { + let playersToUse = persistedState.players; + + if (!playersToUse || (playersToUse.length === 0 && !persistedState.hasOwnProperty('players'))) { + playersToUse = JSON.parse(JSON.stringify(predefinedPlayers)); + } else if (persistedState.hasOwnProperty('players') && playersToUse.length === 0) { + playersToUse = []; + } + + playersToUse = playersToUse.map(p => ({ + ...p, + id: p.id || Date.now().toString() + Math.random(), + avatar: p.avatar === undefined ? DEFAULT_AVATAR_MARKER : p.avatar, + initialTimerSec: typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.initialTimer || "60:00"), + currentTimerSec: typeof p.currentTimerSec === 'number' ? p.currentTimerSec : (typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.currentTimer || p.initialTimer || "60:00")), + isSkipped: p.isSkipped || false, + isTimerRunning: false, + hotkey: p.hotkey || null, + })); + + return { + ...initialState, + ...persistedState, + players: playersToUse, + gameRunning: false, + currentPlayerIndex: persistedState.currentPlayerIndex !== undefined && persistedState.currentPlayerIndex < playersToUse.length ? persistedState.currentPlayerIndex : 0, + }; + } + return JSON.parse(JSON.stringify(initialState)); + }, + mutations: { + SET_PLAYERS(state, players) { + state.players = players.map(p => ({ + ...p, + id: p.id || Date.now().toString() + Math.random(), + avatar: p.avatar || DEFAULT_AVATAR_MARKER, + initialTimerSec: typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.initialTimer || "60:00"), + currentTimerSec: typeof p.currentTimerSec === 'number' ? p.currentTimerSec : (typeof p.initialTimerSec === 'number' ? p.initialTimerSec : parseTime(p.currentTimer || p.initialTimer || "60:00")), + isSkipped: p.isSkipped || false, + isTimerRunning: p.isTimerRunning || false, // Retain running state if explicitly set + hotkey: p.hotkey || null, + })); + }, + ADD_PLAYER(state, player) { + const newPlayer = { + id: Date.now().toString() + Math.random(), // More robust unique ID + name: player.name || `Player ${state.players.length + 1}`, + avatar: player.avatar || DEFAULT_AVATAR_MARKER, + initialTimerSec: player.initialTimerSec || 3600, + currentTimerSec: player.initialTimerSec || 3600, + hotkey: player.hotkey || null, + isSkipped: false, + isTimerRunning: false, + }; + if (state.players.length < 99) { + state.players.push(newPlayer); + } else { + console.warn("Maximum player limit (99) reached."); + alert("Maximum player limit (99) reached."); + } + }, + UPDATE_PLAYER(state, updatedPlayer) { + const index = state.players.findIndex(p => p.id === updatedPlayer.id); + if (index !== -1) { + state.players[index] = { ...state.players[index], ...updatedPlayer }; + } + }, + DELETE_PLAYER(state, playerId) { + state.players = state.players.filter(p => p.id !== playerId); + if (state.currentPlayerIndex >= state.players.length && state.players.length > 0) { + state.currentPlayerIndex = state.players.length - 1; + } else if (state.players.length === 0) { + state.currentPlayerIndex = 0; + } + }, + REORDER_PLAYERS(state, players) { + state.players = players; + }, + SHUFFLE_PLAYERS(state) { + for (let i = state.players.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [state.players[i], state.players[j]] = [state.players[j], state.players[i]]; + } + }, + REVERSE_PLAYERS(state) { + state.players.reverse(); + }, + SET_CURRENT_PLAYER_INDEX(state, index) { + state.currentPlayerIndex = index; + }, + SET_GAME_MODE(state, mode) { + state.gameMode = mode; + }, + SET_IS_MUTED(state, muted) { + state.isMuted = muted; + }, + TOGGLE_THEME(state) { + state.theme = state.theme === 'light' ? 'dark' : 'light'; + }, + SET_GLOBAL_HOTKEY_STOP_PAUSE(state, key) { + state.globalHotkeyStopPause = key; + }, + SET_GLOBAL_HOTKEY_RUN_ALL(state, key) { + state.globalHotkeyRunAll = key; + }, + SET_THEME(state, theme) { + state.theme = theme; + }, + DECREMENT_TIMER(state, { playerIndex }) { + const player = state.players[playerIndex]; + if (player && player.isTimerRunning && !player.isSkipped) { + player.currentTimerSec--; + if (player.currentTimerSec < MAX_NEGATIVE_SECONDS) { + player.currentTimerSec = MAX_NEGATIVE_SECONDS; + player.isSkipped = true; // Auto-skip if max negative time reached + player.isTimerRunning = false; + } + } + }, + RESET_PLAYER_TIMER(state, playerIndex) { + if (state.players[playerIndex]) { + state.players[playerIndex].currentTimerSec = state.players[playerIndex].initialTimerSec; + state.players[playerIndex].isSkipped = false; + state.players[playerIndex].isTimerRunning = false; + } + }, + RESET_ALL_TIMERS(state) { + // When resetting, decide if you want to go back to *only* predefined players + // or reset existing players' timers. The current spec "restores all timers to initial values" + // implies resetting existing players. If it meant reverting to the initial player set, + // this logic would need to change to: + // state.players = JSON.parse(JSON.stringify(predefinedPlayers)); + // For now, sticking to resetting current players' timers: + state.players.forEach(player => { + player.currentTimerSec = player.initialTimerSec; + player.isSkipped = false; + player.isTimerRunning = false; + }); + state.currentPlayerIndex = 0; + state.gameMode = 'normal'; + state.gameRunning = false; + }, + START_PLAYER_TIMER(state, playerIndex) { + if(state.players[playerIndex] && !state.players[playerIndex].isSkipped) { + state.players[playerIndex].isTimerRunning = true; + state.gameRunning = true; + } + }, + PAUSE_PLAYER_TIMER(state, playerIndex) { + if(state.players[playerIndex]) { + state.players[playerIndex].isTimerRunning = false; + } + if (!state.players.some(p => p.isTimerRunning)) { + state.gameRunning = false; + } + }, + PAUSE_ALL_TIMERS(state) { + state.players.forEach(p => p.isTimerRunning = false); + state.gameRunning = false; + }, + SET_GAME_RUNNING(state, isRunning) { + state.gameRunning = isRunning; + }, + }, + actions: { + loadState({ commit, state }) { + // The state initializer already did the main loading from localStorage. + // This action can be used for any *additional* setup after initial hydration + // or to re-apply certain defaults if needed. + // For now, it's mainly a confirmation that persisted state is used. + + // Example: ensure theme is applied if it was loaded + // This is already handled by App.vue's watcher, but could be centralized. + // if (state.theme === 'dark') { + // document.documentElement.classList.add('dark'); + // } else { + // document.documentElement.classList.remove('dark'); + // } + console.log("Store state loaded/initialized."); + // It's good practice for actions to return a Promise if they are async + // or if other parts of the app expect to chain .then() + return Promise.resolve(); // Resolve immediately + }, + saveState({ state }) { + StorageService.saveState({ + players: state.players.map(p => ({ + id: p.id, + name: p.name, + avatar: p.avatar, + initialTimerSec: p.initialTimerSec, + currentTimerSec: p.currentTimerSec, + hotkey: p.hotkey, + isSkipped: p.isSkipped, + })), + globalHotkeyStopPause: state.globalHotkeyStopPause, + globalHotkeyRunAll: state.globalHotkeyRunAll, + currentPlayerIndex: state.currentPlayerIndex, + gameMode: state.gameMode, + isMuted: state.isMuted, + theme: state.theme, + }); + }, + addPlayer({ commit, dispatch }, player) { + commit('ADD_PLAYER', player); + dispatch('saveState'); + }, + updatePlayer({ commit, dispatch }, player) { + commit('UPDATE_PLAYER', player); + dispatch('saveState'); + }, + deletePlayer({ commit, dispatch }, playerId) { + commit('DELETE_PLAYER', playerId); + dispatch('saveState'); + }, + reorderPlayers({commit, dispatch}, players) { + commit('REORDER_PLAYERS', players); + dispatch('saveState'); + }, + shufflePlayers({commit, dispatch}) { + commit('SHUFFLE_PLAYERS'); + dispatch('saveState'); + }, + reversePlayers({commit, dispatch}) { + commit('REVERSE_PLAYERS'); + dispatch('saveState'); + }, + toggleTheme({ commit, dispatch }) { + commit('TOGGLE_THEME'); + dispatch('saveState'); + }, + setGlobalHotkey({ commit, dispatch }, key) { + commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', key); + dispatch('saveState'); + }, + setMuted({ commit, dispatch }, muted) { + commit('SET_IS_MUTED', muted); + dispatch('saveState'); + }, + resetGame({ commit, dispatch }) { // This is for resetting timers during a game session + commit('RESET_ALL_TIMERS'); + dispatch('saveState'); + }, + setGlobalHotkeyStopPause({ commit, dispatch }, key) { // <-- New action + commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', key); + dispatch('saveState'); + }, + setGlobalHotkeyRunAll({ commit, dispatch }, key) { // <-- New action + commit('SET_GLOBAL_HOTKEY_RUN_ALL', key); + dispatch('saveState'); + }, + fullResetApp({ commit, dispatch, state: currentGlobalState }) { + StorageService.clearState(); + const freshInitialState = JSON.parse(JSON.stringify(initialState)); + + commit('SET_PLAYERS', freshInitialState.players); + commit('SET_CURRENT_PLAYER_INDEX', freshInitialState.currentPlayerIndex); + commit('SET_GAME_MODE', freshInitialState.gameMode); + commit('SET_IS_MUTED', freshInitialState.isMuted); + commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', freshInitialState.globalHotkeyStopPause); + commit('SET_GLOBAL_HOTKEY_RUN_ALL', freshInitialState.globalHotkeyRunAll); + + if (currentGlobalState.theme !== freshInitialState.theme) { + commit('SET_THEME', freshInitialState.theme); + } + commit('SET_GAME_RUNNING', false); + dispatch('saveState'); + }, + tick({ commit, state }) { + if (state.gameMode === 'normal') { + if (state.players[state.currentPlayerIndex]?.isTimerRunning) { + commit('DECREMENT_TIMER', { playerIndex: state.currentPlayerIndex }); + } + } else if (state.gameMode === 'allTimers') { + state.players.forEach((player, index) => { + if (player.isTimerRunning) { + commit('DECREMENT_TIMER', { playerIndex: index }); + } + }); + } + }, + passTurn({ commit, state, dispatch }) { + const numPlayers = state.players.length; + if (numPlayers === 0) return; + + const currentIdx = state.currentPlayerIndex; + const currentPlayerTimerWasRunning = state.players[currentIdx]?.isTimerRunning; + + commit('PAUSE_PLAYER_TIMER', currentIdx); + + let nextPlayerIndex = (currentIdx + 1) % numPlayers; + let skippedCount = 0; + while(state.players[nextPlayerIndex]?.isSkipped && skippedCount < numPlayers) { + nextPlayerIndex = (nextPlayerIndex + 1) % numPlayers; + skippedCount++; + } + + if (skippedCount === numPlayers) { + commit('PAUSE_ALL_TIMERS'); + dispatch('saveState'); + return; + } + + commit('SET_CURRENT_PLAYER_INDEX', nextPlayerIndex); + + if (currentPlayerTimerWasRunning && !state.players[nextPlayerIndex].isSkipped) { + commit('START_PLAYER_TIMER', nextPlayerIndex); + } else { + if (state.players[nextPlayerIndex] && !state.players[nextPlayerIndex].isSkipped) { + commit('PAUSE_PLAYER_TIMER', nextPlayerIndex); + } + } + + dispatch('saveState'); + }, + toggleCurrentPlayerTimerNormalMode({ commit, state, dispatch }) { + const player = state.players[state.currentPlayerIndex]; + if (!player) return; + + if (player.isTimerRunning) { + commit('PAUSE_PLAYER_TIMER', state.currentPlayerIndex); + } else if (!player.isSkipped) { + commit('START_PLAYER_TIMER', state.currentPlayerIndex); + } + dispatch('saveState'); + }, + togglePlayerTimerAllTimersMode({ commit, state, dispatch }, playerIndex) { + const player = state.players[playerIndex]; + if (!player) return; + + if (player.isTimerRunning) { + commit('PAUSE_PLAYER_TIMER', playerIndex); + } else if (!player.isSkipped) { + commit('START_PLAYER_TIMER', playerIndex); + } + + const anyTimerRunning = state.players.some(p => p.isTimerRunning && !p.isSkipped); + if (!anyTimerRunning && state.players.length > 0 && state.gameMode === 'allTimers') { + // This auto-revert logic is now in GameView.vue watcher for better control over timing + } + dispatch('saveState'); + }, + globalStopPauseAll({ commit, state, dispatch }) { + if (state.gameMode === 'normal') { + dispatch('toggleCurrentPlayerTimerNormalMode'); + } else { + const anyTimerRunning = state.players.some(p => p.isTimerRunning && !p.isSkipped); + if (anyTimerRunning) { + commit('PAUSE_ALL_TIMERS'); + } else { + state.players.forEach((player, index) => { + if (!player.isSkipped) { + commit('START_PLAYER_TIMER', index); + } + }); + } + } + dispatch('saveState'); + }, + switchToAllTimersMode({ commit, state, dispatch }) { + commit('SET_GAME_MODE', 'allTimers'); + let anyStarted = false; + state.players.forEach((player, index) => { + if (!player.isSkipped) { + commit('START_PLAYER_TIMER', index); + anyStarted = true; + } + }); + if(anyStarted) commit('SET_GAME_RUNNING', true); + else commit('SET_GAME_RUNNING', false); + dispatch('saveState'); + }, + switchToNormalMode({commit, state, dispatch}) { + commit('PAUSE_ALL_TIMERS'); + commit('SET_GAME_MODE', 'normal'); + // Determine current player for normal mode, respecting skips + let currentIdx = state.currentPlayerIndex; + let skippedCount = 0; + while(state.players[currentIdx]?.isSkipped && skippedCount < state.players.length) { + currentIdx = (currentIdx + 1) % state.players.length; + skippedCount++; + } + if (skippedCount < state.players.length) { + commit('SET_CURRENT_PLAYER_INDEX', currentIdx); + // Timer for this player should remain paused as per PAUSE_ALL_TIMERS + } else { + // All players skipped, game is effectively paused. + commit('SET_GAME_RUNNING', false); + } + dispatch('saveState'); + } + }, + getters: { + players: state => state.players, + currentPlayer: state => state.players[state.currentPlayerIndex], + nextPlayer: state => { + if (!state.players || state.players.length < 1) return null; + let nextIndex = (state.currentPlayerIndex + 1) % state.players.length; + let count = 0; + while(state.players[nextIndex]?.isSkipped && count < state.players.length) { + nextIndex = (nextIndex + 1) % state.players.length; + count++; + } + return state.players[nextIndex]; + }, + getPlayerById: (state) => (id) => state.players.find(p => p.id === id), + gameMode: state => state.gameMode, + isMuted: state => state.isMuted, + theme: state => state.theme, + globalHotkeyStopPause: state => state.globalHotkeyStopPause, + globalHotkeyRunAll: state => state.globalHotkeyRunAll, + totalPlayers: state => state.players.length, + gameRunning: state => state.gameRunning, + maxNegativeTimeReached: () => MAX_NEGATIVE_SECONDS, + } +}); \ No newline at end of file diff --git a/src/utils/timeFormatter.js b/src/utils/timeFormatter.js new file mode 100644 index 0000000..e6d5300 --- /dev/null +++ b/src/utils/timeFormatter.js @@ -0,0 +1,32 @@ +export function formatTime(totalSeconds) { + const isNegative = totalSeconds < 0; + if (isNegative) { + totalSeconds = -totalSeconds; + } + + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + const paddedMinutes = String(minutes).padStart(2, '0'); + const paddedSeconds = String(seconds).padStart(2, '0'); + + return `${isNegative ? '-' : ''}${paddedMinutes}:${paddedSeconds}`; + } + + export function parseTime(timeString) { // MM:SS or -MM:SS + if (!timeString || typeof timeString !== 'string') return 0; + const isNegative = timeString.startsWith('-'); + if (isNegative) { + timeString = timeString.substring(1); + } + const parts = timeString.split(':'); + if (parts.length !== 2) return 0; + + const minutes = parseInt(parts[0], 10); + const seconds = parseInt(parts[1], 10); + + if (isNaN(minutes) || isNaN(seconds)) return 0; + + let totalSeconds = (minutes * 60) + seconds; + return isNegative ? -totalSeconds : totalSeconds; + } \ No newline at end of file diff --git a/src/views/GameView.vue b/src/views/GameView.vue new file mode 100644 index 0000000..bf76d63 --- /dev/null +++ b/src/views/GameView.vue @@ -0,0 +1,249 @@ + + + \ No newline at end of file diff --git a/src/views/InfoView.vue b/src/views/InfoView.vue new file mode 100644 index 0000000..144740b --- /dev/null +++ b/src/views/InfoView.vue @@ -0,0 +1,59 @@ + + + \ No newline at end of file diff --git a/src/views/SetupView.vue b/src/views/SetupView.vue new file mode 100644 index 0000000..e130124 --- /dev/null +++ b/src/views/SetupView.vue @@ -0,0 +1,314 @@ + + + \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..a7eeb71 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,29 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + darkMode: 'class', // or 'media' or 'class' + theme: { + extend: { + animation: { + pulsePositive: 'pulsePositive 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', + pulseNegative: 'pulseNegative 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', + }, + keyframes: { + pulsePositive: { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '.7' }, + }, + pulseNegative: { // For text, maybe a color change or slight scale + '0%, 100%': { opacity: '1', transform: 'scale(1)' }, + '50%': { opacity: '.8', transform: 'scale(1.02)' }, + } + } + }, + }, + plugins: [ + require('@tailwindcss/typography'), // Add this line + ], + } \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..a263825 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + server: { + port: 8080 + } +}) \ No newline at end of file