SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
1 h agolast push
JavaScript 56.7% TypeScript 42.6%

SPB Private Cloud v2.0 - Full-featured personal cloud

Multi-user system with roles (admin/user/viewer), TOTP 2FA authentication,
AI-powered search/chat/OCR/auto-tagging via Claude Sonnet 4.5, AES-256
encrypted storage, backup system, notification system, REST API with
API keys, bulk file operations, folder ZIP download, light/dark theme,
and mobile-first responsive design.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
simon-pierre boucher committed 7 mo ago (Feb 18, 2026)

131 changed files +20,001 −0

added .gitignore +30 −0
@@ -0,0 +1,30 @@
1 +# Dependencies
2 +node_modules/
3 +
4 +# Next.js
5 +.next/
6 +out/
7 +
8 +# Environment
9 +.env
10 +.env.local
11 +.env.production
12 +
13 +# Database
14 +prisma/dev.db
15 +prisma/dev.db-journal
16 +
17 +# Uploads & Backups
18 +uploads/
19 +backups/
20 +
21 +# IDE
22 +.vscode/
23 +.idea/
24 +
25 +# OS
26 +.DS_Store
27 +Thumbs.db
28 +
29 +# Claude
30 +.claude/
added CLAUDE.md +307 −0
@@ -0,0 +1,307 @@
1 +Parfait.
2 +Là on parle d’un cloud personnel privé, sérieux, propre, local-first, sécurisé, mobile adaptable — pas un Dropbox cheap.
3 +
4 +Je te fais un CLAUDE.md complet, architecture claire, orienté production, avec accès admin123 (mais sécurisé correctement).
5 +
6 +⸻
7 +
8 +📘 CLAUDE.md
9 +
10 +Projet : SPB Private Cloud
11 +
12 +Owner: Simon-Pierre Boucher
13 +
14 +⸻
15 +
16 +🎯 OBJECTIF
17 +
18 +Construire un cloud personnel privé, accessible via navigateur, avec :
19 + • Upload de tout type de fichier
20 + • Organisation en dossiers
21 + • Partage via liens sécurisés
22 + • Interface moderne
23 + • 100% smartphone adaptable
24 + • Dashboard admin protégé par mot de passe admin123
25 + • Hébergé localement (Mac Studio / NAS / VPS)
26 + • Aucune dépendance à Google / Dropbox
27 +
28 +⸻
29 +
30 +📱 EXIGENCE MOBILE (OBLIGATOIRE)
31 +
32 +Le cloud doit être :
33 + • Parfaitement utilisable sur iPhone
34 + • Mobile-first design
35 + • Drag & drop desktop
36 + • Upload natif mobile
37 + • Preview plein écran mobile
38 + • Boutons large touch-friendly
39 + • Sidebar collapsible mobile
40 +
41 +⸻
42 +
43 +🧱 STACK TECHNIQUE
44 +
45 +Frontend :
46 + • Next.js 14
47 + • TypeScript
48 + • Tailwind CSS
49 + • Framer Motion
50 +
51 +Backend :
52 + • Node.js
53 + • PostgreSQL
54 + • Prisma ORM
55 + • File storage local (ou S3 compatible)
56 +
57 +Auth :
58 + • Password login admin123
59 + • Hash bcrypt
60 + • Session cookie HTTP-only
61 +
62 +⸻
63 +
64 +🌐 ARCHITECTURE
65 +
66 +/app
67 + /login
68 + /dashboard
69 + /files
70 + /folders
71 + /shared
72 + /admin
73 +/lib
74 +/storage
75 +/uploads
76 +/prisma
77 +/components
78 +
79 +
80 +⸻
81 +
82 +🔐 AUTHENTIFICATION
83 +
84 +Page /login
85 + • Input password
86 + • Si password valide → créer session
87 + • Middleware protège toutes routes sauf login
88 +
89 +⚠️ Même si pw simple :
90 + • Hash bcrypt en DB
91 + • Rate limit login
92 + • Session expiration
93 + • Logout automatique
94 +
95 +⸻
96 +
97 +📦 FONCTIONNALITÉS CLOUD
98 +
99 +1️⃣ Gestion de fichiers
100 + • Upload multi-fichiers
101 + • Drag & drop
102 + • Upload mobile caméra
103 + • Rename
104 + • Delete
105 + • Move file
106 + • Duplicate
107 + • File size limit configurable
108 + • Progress bar upload
109 +
110 +⸻
111 +
112 +2️⃣ Gestion de dossiers
113 + • Créer dossier
114 + • Dossier imbriqué
115 + • Breadcrumb navigation
116 + • Drag file vers dossier
117 + • Rename folder
118 + • Delete folder
119 +
120 +⸻
121 +
122 +3️⃣ Preview de fichiers
123 +
124 +Support :
125 + • PDF
126 + • Images
127 + • Vidéo
128 + • Audio
129 + • Markdown
130 + • Code (syntax highlight)
131 + • TXT
132 + • CSV preview
133 +
134 +Mobile :
135 + • Preview full-screen
136 + • Swipe navigation images
137 +
138 +⸻
139 +
140 +4️⃣ Partage sécurisé
141 +
142 +Créer lien :
143 + • Expiration date
144 + • Password optionnel
145 + • Download only
146 + • Preview only
147 + • Désactiver à tout moment
148 +
149 +⸻
150 +
151 +5️⃣ Dashboard admin
152 +
153 +Accessible via /admin
154 + • Storage utilisé
155 + • Nombre fichiers
156 + • Activité récente
157 + • Logs accès
158 + • Gestion liens partagés
159 +
160 +Mobile :
161 + • Cartes verticales
162 + • No horizontal scroll
163 +
164 +⸻
165 +
166 +6️⃣ Recherche
167 + • Search full-text filename
168 + • Search par type
169 + • Search par date
170 + • Search par taille
171 +
172 +⸻
173 +
174 +7️⃣ Sécurité
175 + • Hash password
176 + • CSRF protection
177 + • Rate limiting
178 + • XSS protection
179 + • File type validation
180 + • Antivirus hook (optionnel)
181 + • HTTPS only
182 +
183 +⸻
184 +
185 +🗄️ DATABASE STRUCTURE
186 +
187 +Users
188 +
189 +| id | passwordHash | createdAt |
190 +
191 +⸻
192 +
193 +Files
194 +
195 +| id | name | path | size | mimeType | folderId | createdAt |
196 +
197 +⸻
198 +
199 +Folders
200 +
201 +| id | name | parentId | createdAt |
202 +
203 +⸻
204 +
205 +SharedLinks
206 +
207 +| id | fileId | token | expiresAt | passwordHash | active |
208 +
209 +⸻
210 +
211 +ActivityLogs
212 +
213 +| id | action | fileId | timestamp |
214 +
215 +⸻
216 +
217 +📱 UI STRUCTURE
218 +
219 +Desktop
220 + • Sidebar gauche
221 + • File explorer centre
222 + • Preview panel droite
223 +
224 +Mobile
225 + • Sidebar hidden
226 + • Hamburger menu
227 + • Full width file explorer
228 + • Preview modal full screen
229 +
230 +⸻
231 +
232 +🚀 FONCTIONNALITÉS AVANCÉES (OPTIONNEL)
233 + • Album photo intelligent (face recognition local)
234 + • Tagging automatique
235 + • OCR PDF
236 + • AI search (“Find all files about FOMC”)
237 + • Versioning fichiers
238 + • Recycle bin
239 + • Backup automatique
240 + • Sync multi-Mac
241 + • Accès via Tailscale sécurisé
242 + • Encrypted storage at rest
243 +
244 +⸻
245 +
246 +🧠 POSITIONNEMENT
247 +
248 +Ce cloud doit être :
249 + • Privé
250 + • Souverain
251 + • Minimaliste
252 + • Plus clean que Dropbox
253 + • Plus sécurisé que Google Drive
254 + • Mobile-ready
255 + • Local-first
256 +
257 +⸻
258 +
259 +⚙️ PERFORMANCE
260 + • Chunk upload
261 + • Lazy loading
262 + • Infinite scroll
263 + • Optimized image loading
264 + • Streaming preview
265 +
266 +⸻
267 +
268 +🔮 ÉVOLUTION FUTURE
269 + • Multi-user system
270 + • Permission management
271 + • AI assistant intégré
272 + • Auto organization fichiers
273 + • Vector database index
274 + • Search sémantique
275 +
276 +⸻
277 +
278 +🎨 DESIGN
279 + • Dark mode par défaut
280 + • Noir / gris / blanc
281 + • Typo Inter
282 + • UI minimal
283 + • Micro-animations subtiles
284 +
285 +⸻
286 +
287 +🛡️ NOTE IMPORTANTE
288 +
289 +Même si accès = admin123,
290 +le système doit être codé proprement pour pouvoir :
291 + • Changer mot de passe
292 + • Ajouter 2FA futur
293 + • Ajouter multi-user
294 +
295 +⸻
296 +
297 +Si tu veux maintenant :
298 + 1. 🔥 Je te génère l’architecture complète du projet
299 + 2. 💻 Je te code le système login
300 + 3. 📁 Je te code le file explorer
301 + 4. 📱 Je te fais le layout mobile-first complet
302 + 5. 🧠 On ajoute AI local pour ton cloud.spboucher
303 +
304 +Tu veux le faire :
305 + • Local sur Mac Studio ?
306 + • Sur NAS ?
307 + • Sur VPS public sécurisé ?
added README.md +36 −0
@@ -0,0 +1,36 @@
1 +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 +
3 +## Getting Started
4 +
5 +First, run the development server:
6 +
7 +```bash
8 +npm run dev
9 +# or
10 +yarn dev
11 +# or
12 +pnpm dev
13 +# or
14 +bun dev
15 +```
16 +
17 +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 +
19 +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 +
21 +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22 +
23 +## Learn More
24 +
25 +To learn more about Next.js, take a look at the following resources:
26 +
27 +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 +
30 +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 +
32 +## Deploy on Vercel
33 +
34 +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 +
36 +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
added TODO.md +74 −0
@@ -0,0 +1,74 @@
1 +# SPB Private Cloud - Todo & Upgrades
2 +
3 +## Priorité Haute
4 +
5 +- [x] **Rate limiting login** - Protection brute-force sur `/api/auth/login`
6 +- [ ] **Migration PostgreSQL** - Remplacer SQLite par PostgreSQL pour la production
7 +- [ ] **HTTPS / SSL** - Configurer certificats SSL (Let's Encrypt)
8 +- [ ] **CSRF protection** - Implémenter tokens CSRF sur les formulaires
9 +- [ ] **Validation types fichiers** - Bloquer les fichiers dangereux (.exe, .bat, scripts)
10 +- [ ] **Chunk upload** - Upload par morceaux pour fichiers volumineux (>100MB)
11 +- [ ] **Corbeille (Recycle Bin)** - Soft delete avec récupération sous 30 jours
12 +- [x] **Changement mot de passe** - Interface admin pour modifier le password
13 +
14 +## Priorité Moyenne
15 +
16 +- [x] **2FA (Two-Factor Auth)** - Authentification TOTP (Google Authenticator)
17 +- [ ] **Drag & drop fichiers vers dossiers** - Glisser fichiers entre dossiers
18 +- [x] **Bulk actions** - Sélection multiple + actions groupées (supprimer, déplacer, ZIP, chiffrer)
19 +- [x] **Download dossier en ZIP** - Télécharger un dossier complet en archive
20 +- [ ] **Image thumbnails** - Générer des miniatures pour les images
21 +- [ ] **Video streaming** - Streaming adaptatif pour les vidéos (HLS)
22 +- [ ] **Progress bar réelle** - Upload avec progression réelle via XMLHttpRequest
23 +- [ ] **Infinite scroll** - Pagination infinie
24 +- [ ] **Lazy loading images** - Chargement progressif des images
25 +- [x] **Backup automatique** - Backup DB + fichiers en ZIP
26 +- [x] **Notifications** - Système de notifications avec badge
27 +
28 +## Fonctionnalités Avancées
29 +
30 +- [x] **Multi-user system** - Plusieurs utilisateurs avec comptes séparés
31 +- [x] **Permission management** - Rôles (admin, user, viewer)
32 +- [ ] **Versioning complet** - Historique de toutes les versions avec diff visuel
33 +- [x] **OCR PDF** - Extraction texte des PDF/images via Claude Vision
34 +- [x] **AI Search** - Recherche sémantique via Claude Sonnet 4.5
35 +- [x] **Auto-tagging** - Classification automatique des fichiers via IA
36 +- [ ] **Album photo intelligent** - Regroupement par date/lieu
37 +- [x] **Encrypted storage** - Chiffrement at-rest AES-256-GCM
38 +- [ ] **Sync multi-device** - Synchronisation avec d'autres appareils
39 +- [ ] **Accès Tailscale** - VPN mesh pour accès sécurisé
40 +- [ ] **WebDAV support** - Monter le cloud comme lecteur réseau
41 +- [x] **API publique** - REST API avec clés API `/api/v1/files`
42 +
43 +## UI/UX Améliorations
44 +
45 +- [x] **Mode clair/sombre** - Toggle light/dark theme
46 +- [x] **Assistant IA** - Panel latéral avec recherche IA et chat
47 +- [x] **Cloche notifications** - Badge avec compteur non-lus
48 +- [x] **Gestion utilisateurs** - Interface admin CRUD
49 +- [x] **Gestion clés API** - Interface de création/suppression
50 +- [x] **Configuration 2FA** - Setup QR code + vérification
51 +- [x] **Panel backup** - Interface de gestion des backups
52 +- [ ] **Thèmes personnalisés** - Couleurs accent personnalisables
53 +- [ ] **Raccourcis clavier** - Ctrl+U upload, Ctrl+N nouveau dossier
54 +- [ ] **Drag & drop global** - Déposer fichiers n'importe où
55 +- [ ] **Preview plein écran mobile** - Swipe navigation entre images
56 +- [ ] **Grid size ajustable** - Slider pour taille des vignettes
57 +- [ ] **Tri avancé** - Tri par nom, date, taille, type
58 +- [ ] **Filtres avancés** - Filtrer par type, date, taille, tags
59 +
60 +## Performance
61 +
62 +- [ ] **CDN local** - Cache statique pour fichiers fréquemment accédés
63 +- [ ] **Service Worker** - Cache offline pour l'interface
64 +- [ ] **Image optimization** - Compression automatique des images
65 +- [ ] **Database indexing** - Optimiser les index
66 +- [ ] **Connection pooling** - Pool de connexions DB
67 +
68 +## DevOps
69 +
70 +- [ ] **Docker compose** - Containeriser l'app
71 +- [ ] **CI/CD pipeline** - GitHub Actions
72 +- [ ] **Monitoring** - Health checks, métriques
73 +- [ ] **Logs structurés** - Logging JSON avec rotation
74 +- [ ] **Auto-update** - Mécanisme de mise à jour
added app/admin/activity/page.tsx +92 −0
@@ -0,0 +1,92 @@
1 +"use client";
2 +
3 +import { useEffect, useState } from "react";
4 +import AppShell from "@/components/layout/AppShell";
5 +import ActivityLog from "@/components/admin/ActivityLog";
6 +import Button from "@/components/ui/Button";
7 +
8 +interface Activity {
9 + id: string;
10 + action: string;
11 + details: string | null;
12 + createdAt: string;
13 + file: { name: string } | null;
14 +}
15 +
16 +export default function ActivityPage() {
17 + const [activities, setActivities] = useState<Activity[]>([]);
18 + const [loading, setLoading] = useState(true);
19 + const [total, setTotal] = useState(0);
20 + const [offset, setOffset] = useState(0);
21 + const limit = 50;
22 +
23 + const fetchActivity = async (newOffset = 0) => {
24 + setLoading(true);
25 + try {
26 + const res = await fetch(
27 + `/api/admin/activity?limit=${limit}&offset=${newOffset}`
28 + );
29 + if (res.ok) {
30 + const data = await res.json();
31 + setActivities(data.activities);
32 + setTotal(data.total);
33 + setOffset(newOffset);
34 + }
35 + } finally {
36 + setLoading(false);
37 + }
38 + };
39 +
40 + useEffect(() => {
41 + fetchActivity();
42 + }, []);
43 +
44 + return (
45 + <AppShell>
46 + <div className="p-4 lg:p-6 space-y-6">
47 + <div>
48 + <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Activité</h1>
49 + <p className="text-sm text-gray-500 mt-1">
50 + Historique complet ({total} entrées)
51 + </p>
52 + </div>
53 +
54 + {loading ? (
55 + <div className="flex items-center justify-center h-32">
56 + <div className="w-6 h-6 border-2 border-gray-300 dark:border-white/20 border-t-gray-600 dark:border-t-white rounded-full animate-spin" />
57 + </div>
58 + ) : (
59 + <>
60 + <div className="bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl overflow-hidden shadow-sm dark:shadow-none transition-colors">
61 + <ActivityLog activities={activities} />
62 + </div>
63 +
64 + {total > limit && (
65 + <div className="flex items-center justify-center gap-4">
66 + <Button
67 + variant="ghost"
68 + size="sm"
69 + disabled={offset === 0}
70 + onClick={() => fetchActivity(Math.max(0, offset - limit))}
71 + >
72 + Précédent
73 + </Button>
74 + <span className="text-sm text-gray-500">
75 + {offset + 1}-{Math.min(offset + limit, total)} sur {total}
76 + </span>
77 + <Button
78 + variant="ghost"
79 + size="sm"
80 + disabled={offset + limit >= total}
81 + onClick={() => fetchActivity(offset + limit)}
82 + >
83 + Suivant
84 + </Button>
85 + </div>
86 + )}
87 + </>
88 + )}
89 + </div>
90 + </AppShell>
91 + );
92 +}
added app/admin/api-keys/page.tsx +18 −0
@@ -0,0 +1,18 @@
1 +"use client";
2 +
3 +import AppShell from "@/components/layout/AppShell";
4 +import ApiKeyManager from "@/components/admin/ApiKeyManager";
5 +
6 +export default function ApiKeysPage() {
7 + return (
8 + <AppShell>
9 + <div className="p-4 lg:p-6 space-y-6">
10 + <div>
11 + <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Clés API</h1>
12 + <p className="text-sm text-gray-500 mt-1">Gérez l&apos;accès à l&apos;API REST publique</p>
13 + </div>
14 + <ApiKeyManager />
15 + </div>
16 + </AppShell>
17 + );
18 +}
added app/admin/backup/page.tsx +18 −0
@@ -0,0 +1,18 @@
1 +"use client";
2 +
3 +import AppShell from "@/components/layout/AppShell";
4 +import BackupPanel from "@/components/admin/BackupPanel";
5 +
6 +export default function BackupPage() {
7 + return (
8 + <AppShell>
9 + <div className="p-4 lg:p-6 space-y-6">
10 + <div>
11 + <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Backups</h1>
12 + <p className="text-sm text-gray-500 mt-1">Sauvegardez et restaurez vos données</p>
13 + </div>
14 + <BackupPanel />
15 + </div>
16 + </AppShell>
17 + );
18 +}
added app/admin/page.tsx +88 −0
@@ -0,0 +1,88 @@
1 +"use client";
2 +
3 +import { useEffect, useState } from "react";
4 +import AppShell from "@/components/layout/AppShell";
5 +import StatsCards from "@/components/admin/StatsCards";
6 +import ActivityLog from "@/components/admin/ActivityLog";
7 +import StorageChart from "@/components/admin/StorageChart";
8 +
9 +interface Stats {
10 + fileCount: number;
11 + folderCount: number;
12 + shareCount: number;
13 + storageUsed: number;
14 + diskFileCount: number;
15 + recentActivity: Array<{
16 + id: string;
17 + action: string;
18 + details: string | null;
19 + createdAt: string;
20 + file: { name: string } | null;
21 + }>;
22 + typeBreakdown: Array<{
23 + type: string;
24 + count: number;
25 + size: number;
26 + }>;
27 +}
28 +
29 +export default function AdminPage() {
30 + const [stats, setStats] = useState<Stats | null>(null);
31 + const [loading, setLoading] = useState(true);
32 +
33 + useEffect(() => {
34 + fetch("/api/admin/stats")
35 + .then((res) => res.json())
36 + .then((data) => {
37 + setStats(data);
38 + setLoading(false);
39 + })
40 + .catch(() => setLoading(false));
41 + }, []);
42 +
43 + return (
44 + <AppShell>
45 + <div className="p-3 sm:p-4 lg:p-6 space-y-4 sm:space-y-6">
46 + <div>
47 + <h1 className="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-white">Dashboard</h1>
48 + <p className="text-sm text-gray-500 mt-1">Vue d&apos;ensemble de votre cloud</p>
49 + </div>
50 +
51 + {loading ? (
52 + <div className="flex items-center justify-center h-32">
53 + <div className="w-6 h-6 border-2 border-gray-300 dark:border-white/20 border-t-gray-600 dark:border-t-white rounded-full animate-spin" />
54 + </div>
55 + ) : stats ? (
56 + <>
57 + <StatsCards stats={stats} />
58 +
59 + <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
60 + {/* Storage Chart */}
61 + <div className="bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl p-4 sm:p-6 shadow-sm dark:shadow-none transition-colors">
62 + <h2 className="text-base sm:text-lg font-medium text-gray-900 dark:text-white mb-4">
63 + Répartition du stockage
64 + </h2>
65 + <StorageChart
66 + data={stats.typeBreakdown}
67 + totalSize={stats.storageUsed}
68 + />
69 + </div>
70 +
71 + {/* Recent Activity */}
72 + <div className="bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl p-4 sm:p-6 shadow-sm dark:shadow-none transition-colors">
73 + <h2 className="text-base sm:text-lg font-medium text-gray-900 dark:text-white mb-4">
74 + Activité récente
75 + </h2>
76 + <div className="max-h-80 overflow-y-auto">
77 + <ActivityLog activities={stats.recentActivity} />
78 + </div>
79 + </div>
80 + </div>
81 + </>
82 + ) : (
83 + <p className="text-gray-500">Impossible de charger les stats</p>
84 + )}
85 + </div>
86 + </AppShell>
87 + );
88 +}
added app/admin/settings/page.tsx +34 −0
@@ -0,0 +1,34 @@
1 +"use client";
2 +
3 +import { useEffect, useState } from "react";
4 +import AppShell from "@/components/layout/AppShell";
5 +import TwoFactorSetup from "@/components/admin/TwoFactorSetup";
6 +
7 +export default function SettingsPage() {
8 + const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
9 +
10 + const fetchStatus = async () => {
11 + try {
12 + const res = await fetch("/api/auth/session");
13 + if (res.ok) {
14 + const data = await res.json();
15 + setTwoFactorEnabled(data.twoFactorEnabled || false);
16 + }
17 + } catch {}
18 + };
19 +
20 + useEffect(() => { fetchStatus(); }, []);
21 +
22 + return (
23 + <AppShell>
24 + <div className="p-4 lg:p-6 space-y-6">
25 + <div>
26 + <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Paramètres</h1>
27 + <p className="text-sm text-gray-500 mt-1">Sécurité et préférences</p>
28 + </div>
29 +
30 + <TwoFactorSetup enabled={twoFactorEnabled} onStatusChange={fetchStatus} />
31 + </div>
32 + </AppShell>
33 + );
34 +}
added app/admin/shares/page.tsx +159 −0
@@ -0,0 +1,159 @@
1 +"use client";
2 +
3 +import { useEffect, useState } from "react";
4 +import AppShell from "@/components/layout/AppShell";
5 +import Button from "@/components/ui/Button";
6 +import { formatDate } from "@/lib/utils";
7 +import { motion } from "framer-motion";
8 +import { Link2, Link2Off, Trash2, Copy, ExternalLink } from "lucide-react";
9 +import toast from "react-hot-toast";
10 +
11 +interface SharedLink {
12 + id: string;
13 + token: string;
14 + mode: string;
15 + active: boolean;
16 + expiresAt: string | null;
17 + passwordHash: string | null;
18 + createdAt: string;
19 + file: { name: string; mimeType: string; size: number } | null;
20 +}
21 +
22 +export default function SharesPage() {
23 + const [shares, setShares] = useState<SharedLink[]>([]);
24 + const [loading, setLoading] = useState(true);
25 +
26 + const fetchShares = async () => {
27 + setLoading(true);
28 + try {
29 + const res = await fetch("/api/shares");
30 + if (res.ok) setShares(await res.json());
31 + } finally {
32 + setLoading(false);
33 + }
34 + };
35 +
36 + useEffect(() => {
37 + fetchShares();
38 + }, []);
39 +
40 + const toggleActive = async (id: string, active: boolean) => {
41 + await fetch(`/api/shares/${id}`, {
42 + method: "PATCH",
43 + headers: { "Content-Type": "application/json" },
44 + body: JSON.stringify({ active: !active }),
45 + });
46 + fetchShares();
47 + toast.success(active ? "Lien désactivé" : "Lien activé");
48 + };
49 +
50 + const deleteShare = async (id: string) => {
51 + await fetch(`/api/shares/${id}`, { method: "DELETE" });
52 + fetchShares();
53 + toast.success("Lien supprimé");
54 + };
55 +
56 + const copyLink = (token: string) => {
57 + navigator.clipboard.writeText(`${window.location.origin}/shared/${token}`);
58 + toast.success("Lien copié");
59 + };
60 +
61 + return (
62 + <AppShell>
63 + <div className="p-3 sm:p-4 lg:p-6 space-y-4 sm:space-y-6">
64 + <div>
65 + <h1 className="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-white">Liens partagés</h1>
66 + <p className="text-sm text-gray-500 mt-1">
67 + Gérez vos liens de partage
68 + </p>
69 + </div>
70 +
71 + {loading ? (
72 + <div className="flex items-center justify-center h-32">
73 + <div className="w-6 h-6 border-2 border-gray-300 dark:border-white/20 border-t-gray-600 dark:border-t-white rounded-full animate-spin" />
74 + </div>
75 + ) : shares.length === 0 ? (
76 + <div className="text-center text-gray-500 py-12">
77 + <Link2 className="w-12 h-12 mx-auto mb-4 text-gray-300 dark:text-gray-600" />
78 + <p>Aucun lien de partage</p>
79 + </div>
80 + ) : (
81 + <div className="space-y-3">
82 + {shares.map((share, i) => (
83 + <motion.div
84 + key={share.id}
85 + initial={{ opacity: 0, y: 10 }}
86 + animate={{ opacity: 1, y: 0 }}
87 + transition={{ delay: i * 0.05 }}
88 + className={`bg-white dark:bg-white/5 border rounded-xl p-3.5 sm:p-4 shadow-sm dark:shadow-none transition-colors ${
89 + share.active ? "border-gray-200 dark:border-white/10" : "border-gray-100 dark:border-white/5 opacity-60"
90 + }`}
91 + >
92 + <div className="flex flex-col gap-3">
93 + <div className="flex-1 min-w-0">
94 + <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
95 + {share.file?.name || "Fichier supprimé"}
96 + </p>
97 + <div className="flex flex-wrap gap-x-2 gap-y-1 mt-1">
98 + <span className="text-xs text-gray-500">
99 + {share.mode === "DOWNLOAD" ? "Téléchargement" : "Aperçu"}
100 + </span>
101 + {share.passwordHash && (
102 + <span className="text-xs text-yellow-500">Protégé</span>
103 + )}
104 + {share.expiresAt && (
105 + <span className="text-xs text-gray-500 break-words">
106 + Expire: {formatDate(share.expiresAt)}
107 + </span>
108 + )}
109 + <span className="text-xs text-gray-400 dark:text-gray-600 break-words">
110 + Créé: {formatDate(share.createdAt)}
111 + </span>
112 + </div>
113 + </div>
114 + <div className="flex items-center gap-1.5 sm:gap-2 flex-wrap">
115 + <Button
116 + variant="ghost"
117 + size="sm"
118 + onClick={() => copyLink(share.token)}
119 + >
120 + <Copy className="w-3.5 h-3.5" />
121 + <span className="text-xs sm:hidden">Copier</span>
122 + </Button>
123 + <Button
124 + variant="ghost"
125 + size="sm"
126 + onClick={() =>
127 + window.open(`/shared/${share.token}`, "_blank")
128 + }
129 + >
130 + <ExternalLink className="w-3.5 h-3.5" />
131 + </Button>
132 + <Button
133 + variant="ghost"
134 + size="sm"
135 + onClick={() => toggleActive(share.id, share.active)}
136 + >
137 + {share.active ? (
138 + <Link2Off className="w-3.5 h-3.5" />
139 + ) : (
140 + <Link2 className="w-3.5 h-3.5" />
141 + )}
142 + </Button>
143 + <Button
144 + variant="ghost"
145 + size="sm"
146 + onClick={() => deleteShare(share.id)}
147 + >
148 + <Trash2 className="w-3.5 h-3.5 text-red-400" />
149 + </Button>
150 + </div>
151 + </div>
152 + </motion.div>
153 + ))}
154 + </div>
155 + )}
156 + </div>
157 + </AppShell>
158 + );
159 +}
added app/admin/users/page.tsx +18 −0
@@ -0,0 +1,18 @@
1 +"use client";
2 +
3 +import AppShell from "@/components/layout/AppShell";
4 +import UserManagement from "@/components/admin/UserManagement";
5 +
6 +export default function UsersPage() {
7 + return (
8 + <AppShell>
9 + <div className="p-4 lg:p-6 space-y-6">
10 + <div>
11 + <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Utilisateurs</h1>
12 + <p className="text-sm text-gray-500 mt-1">Gérez les utilisateurs de votre cloud</p>
13 + </div>
14 + <UserManagement />
15 + </div>
16 + </AppShell>
17 + );
18 +}
added app/api/admin/activity/route.ts +20 −0
@@ -0,0 +1,20 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +
4 +export async function GET(request: NextRequest) {
5 + const { searchParams } = new URL(request.url);
6 + const limit = parseInt(searchParams.get("limit") || "50");
7 + const offset = parseInt(searchParams.get("offset") || "0");
8 +
9 + const [activities, total] = await Promise.all([
10 + prisma.activityLog.findMany({
11 + take: limit,
12 + skip: offset,
13 + orderBy: { createdAt: "desc" },
14 + include: { file: { select: { name: true } } },
15 + }),
16 + prisma.activityLog.count(),
17 + ]);
18 +
19 + return NextResponse.json({ activities, total });
20 +}
added app/api/admin/stats/route.ts +41 −0
@@ -0,0 +1,41 @@
1 +import { NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getStorageUsage } from "@/lib/storage";
4 +
5 +export async function GET() {
6 + const [fileCount, folderCount, shareCount, recentActivity, storageUsage] =
7 + await Promise.all([
8 + prisma.file.count(),
9 + prisma.folder.count(),
10 + prisma.sharedLink.count({ where: { active: true } }),
11 + prisma.activityLog.findMany({
12 + take: 20,
13 + orderBy: { createdAt: "desc" },
14 + include: { file: { select: { name: true } } },
15 + }),
16 + getStorageUsage(),
17 + ]);
18 +
19 + // File type breakdown
20 + const filesByType = await prisma.file.groupBy({
21 + by: ["mimeType"],
22 + _count: true,
23 + _sum: { size: true },
24 + });
25 +
26 + const typeBreakdown = filesByType.map((t) => ({
27 + type: t.mimeType,
28 + count: t._count,
29 + size: Number(t._sum.size || 0),
30 + }));
31 +
32 + return NextResponse.json({
33 + fileCount,
34 + folderCount,
35 + shareCount,
36 + storageUsed: storageUsage.totalSize,
37 + diskFileCount: storageUsage.fileCount,
38 + recentActivity,
39 + typeBreakdown,
40 + });
41 +}
added app/api/ai/chat/route.ts +20 −0
@@ -0,0 +1,20 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { aiChat } from "@/lib/ai";
5 +
6 +export async function POST(request: NextRequest) {
7 + const response = NextResponse.next();
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 + if (!session.isLoggedIn) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 +
11 + const { message, context } = await request.json();
12 + if (!message) return NextResponse.json({ error: "message required" }, { status: 400 });
13 +
14 + try {
15 + const reply = await aiChat(message, context);
16 + return NextResponse.json({ reply });
17 + } catch (error) {
18 + return NextResponse.json({ error: "Chat failed", details: (error as Error).message }, { status: 500 });
19 + }
20 +}
added app/api/ai/describe/route.ts +20 −0
@@ -0,0 +1,20 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { aiDescribe } from "@/lib/ai";
5 +
6 +export async function POST(request: NextRequest) {
7 + const response = NextResponse.next();
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 + if (!session.isLoggedIn) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 +
11 + const { fileId } = await request.json();
12 + if (!fileId) return NextResponse.json({ error: "fileId required" }, { status: 400 });
13 +
14 + try {
15 + const description = await aiDescribe(fileId);
16 + return NextResponse.json({ description });
17 + } catch (error) {
18 + return NextResponse.json({ error: "Describe failed", details: (error as Error).message }, { status: 500 });
19 + }
20 +}
added app/api/ai/ocr/route.ts +20 −0
@@ -0,0 +1,20 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { aiOcr } from "@/lib/ai";
5 +
6 +export async function POST(request: NextRequest) {
7 + const response = NextResponse.next();
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 + if (!session.isLoggedIn) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 +
11 + const { fileId } = await request.json();
12 + if (!fileId) return NextResponse.json({ error: "fileId required" }, { status: 400 });
13 +
14 + try {
15 + const text = await aiOcr(fileId);
16 + return NextResponse.json({ text });
17 + } catch (error) {
18 + return NextResponse.json({ error: "OCR failed", details: (error as Error).message }, { status: 500 });
19 + }
20 +}
added app/api/ai/search/route.ts +20 −0
@@ -0,0 +1,20 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { aiSearch } from "@/lib/ai";
5 +
6 +export async function POST(request: NextRequest) {
7 + const response = NextResponse.next();
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 + if (!session.isLoggedIn) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 +
11 + const { query } = await request.json();
12 + if (!query) return NextResponse.json({ error: "Query required" }, { status: 400 });
13 +
14 + try {
15 + const results = await aiSearch(query, session.userId);
16 + return NextResponse.json(results);
17 + } catch (error) {
18 + return NextResponse.json({ error: "AI search failed", details: (error as Error).message }, { status: 500 });
19 + }
20 +}
added app/api/ai/tag/route.ts +20 −0
@@ -0,0 +1,20 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { aiAutoTag } from "@/lib/ai";
5 +
6 +export async function POST(request: NextRequest) {
7 + const response = NextResponse.next();
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 + if (!session.isLoggedIn) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 +
11 + const { fileId } = await request.json();
12 + if (!fileId) return NextResponse.json({ error: "fileId required" }, { status: 400 });
13 +
14 + try {
15 + const tags = await aiAutoTag(fileId);
16 + return NextResponse.json({ tags });
17 + } catch (error) {
18 + return NextResponse.json({ error: "Auto-tag failed", details: (error as Error).message }, { status: 500 });
19 + }
20 +}
added app/api/auth/2fa/disable/route.ts +13 −0
@@ -0,0 +1,13 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +
6 +export async function POST(request: NextRequest) {
7 + const response = NextResponse.next();
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 + if (!session.isLoggedIn || !session.userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 +
11 + await prisma.user.update({ where: { id: session.userId }, data: { twoFactorEnabled: false, twoFactorSecret: null } });
12 + return NextResponse.json({ success: true });
13 +}
added app/api/auth/2fa/setup/route.ts +23 −0
@@ -0,0 +1,23 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +import { authenticator } from "otplib";
6 +import QRCode from "qrcode";
7 +
8 +export async function POST(request: NextRequest) {
9 + const response = NextResponse.next();
10 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
11 + if (!session.isLoggedIn || !session.userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
12 +
13 + const secret = authenticator.generateSecret();
14 + const user = await prisma.user.findUnique({ where: { id: session.userId } });
15 + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 });
16 +
17 + await prisma.user.update({ where: { id: session.userId }, data: { twoFactorSecret: secret } });
18 +
19 + const otpauth = authenticator.keyuri(user.email || user.id, "SPB Cloud", secret);
20 + const qrCodeUrl = await QRCode.toDataURL(otpauth);
21 +
22 + return NextResponse.json({ secret, qrCodeUrl });
23 +}
added app/api/auth/2fa/verify/route.ts +49 −0
@@ -0,0 +1,49 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +import { authenticator } from "otplib";
6 +import { randomBytes } from "crypto";
7 +import { logActivity } from "@/lib/activity";
8 +
9 +export async function POST(request: NextRequest) {
10 + const body = await request.json();
11 + const { token, enable } = body;
12 +
13 + const response = NextResponse.json({ success: true });
14 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
15 +
16 + // Enable 2FA flow (already logged in, setting up)
17 + if (enable && session.isLoggedIn && session.userId) {
18 + const user = await prisma.user.findUnique({ where: { id: session.userId } });
19 + if (!user?.twoFactorSecret) return NextResponse.json({ error: "Setup 2FA first" }, { status: 400 });
20 +
21 + const valid = authenticator.verify({ token, secret: user.twoFactorSecret });
22 + if (!valid) return NextResponse.json({ error: "Code invalide" }, { status: 401 });
23 +
24 + await prisma.user.update({ where: { id: session.userId }, data: { twoFactorEnabled: true } });
25 + return NextResponse.json({ success: true, message: "2FA activ\u00e9" });
26 + }
27 +
28 + // Login 2FA verification flow
29 + if (!session.pendingTwoFactor || !session.pendingUserId) {
30 + return NextResponse.json({ error: "No pending 2FA" }, { status: 400 });
31 + }
32 +
33 + const user = await prisma.user.findUnique({ where: { id: session.pendingUserId } });
34 + if (!user?.twoFactorSecret) return NextResponse.json({ error: "2FA not configured" }, { status: 400 });
35 +
36 + const valid = authenticator.verify({ token, secret: user.twoFactorSecret });
37 + if (!valid) return NextResponse.json({ error: "Code invalide" }, { status: 401 });
38 +
39 + session.isLoggedIn = true;
40 + session.userId = user.id;
41 + session.userRole = user.role;
42 + session.pendingTwoFactor = false;
43 + session.pendingUserId = undefined;
44 + session.csrfToken = randomBytes(32).toString("hex");
45 + await session.save();
46 +
47 + await logActivity("LOGIN", null, "Login with 2FA");
48 + return response;
49 +}
added app/api/auth/login/route.ts +39 −0
@@ -0,0 +1,39 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { verifyPassword } from "@/lib/auth";
5 +import { checkLoginRateLimit } from "@/lib/rate-limit";
6 +import { logActivity } from "@/lib/activity";
7 +import { randomBytes } from "crypto";
8 +
9 +export async function POST(request: NextRequest) {
10 + const ip = request.headers.get("x-forwarded-for") || "127.0.0.1";
11 + const allowed = await checkLoginRateLimit(ip);
12 + if (!allowed) return NextResponse.json({ error: "Trop de tentatives. Réessayez dans 1 minute." }, { status: 429 });
13 +
14 + const body = await request.json();
15 + const { password, email } = body;
16 + if (!password) return NextResponse.json({ error: "Mot de passe requis" }, { status: 400 });
17 +
18 + const { valid, user } = await verifyPassword(password, email || undefined);
19 + if (!valid || !user) return NextResponse.json({ error: "Identifiants incorrects" }, { status: 401 });
20 +
21 + const response = NextResponse.json({ success: true, requires2FA: user.twoFactorEnabled });
22 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
23 +
24 + if (user.twoFactorEnabled) {
25 + session.pendingTwoFactor = true;
26 + session.pendingUserId = user.id;
27 + await session.save();
28 + return response;
29 + }
30 +
31 + session.isLoggedIn = true;
32 + session.userId = user.id;
33 + session.userRole = user.role;
34 + session.csrfToken = randomBytes(32).toString("hex");
35 + await session.save();
36 +
37 + await logActivity("LOGIN", null, `Login from ${ip}`);
38 + return response;
39 +}
added app/api/auth/logout/route.ts +14 −0
@@ -0,0 +1,14 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { logActivity } from "@/lib/activity";
5 +
6 +export async function POST(request: NextRequest) {
7 + const response = NextResponse.json({ success: true });
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 +
10 + await logActivity("LOGOUT");
11 + session.destroy();
12 +
13 + return response;
14 +}
added app/api/auth/session/route.ts +25 −0
@@ -0,0 +1,25 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +
6 +export async function GET(request: NextRequest) {
7 + const response = NextResponse.json({});
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 +
10 + if (!session.isLoggedIn || !session.userId) {
11 + return NextResponse.json({ isLoggedIn: false });
12 + }
13 +
14 + const user = await prisma.user.findUnique({
15 + where: { id: session.userId },
16 + select: { id: true, email: true, name: true, role: true, twoFactorEnabled: true },
17 + });
18 +
19 + return NextResponse.json({
20 + isLoggedIn: true,
21 + userId: session.userId,
22 + userRole: session.userRole,
23 + ...user,
24 + });
25 +}
added app/api/backup/route.ts +38 −0
@@ -0,0 +1,38 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { requireAdmin } from "@/lib/auth";
5 +import { createBackup, listBackups, deleteBackup } from "@/lib/backup";
6 +
7 +export async function GET(request: NextRequest) {
8 + const response = NextResponse.next();
9 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
10 + if (!session.userId || !(await requireAdmin(session.userId))) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
11 +
12 + const backups = await listBackups();
13 + return NextResponse.json(backups);
14 +}
15 +
16 +export async function POST(request: NextRequest) {
17 + const response = NextResponse.next();
18 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
19 + if (!session.userId || !(await requireAdmin(session.userId))) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
20 +
21 + const body = await request.json().catch(() => ({}));
22 + try {
23 + const result = await createBackup(body.type || "full");
24 + return NextResponse.json(result, { status: 201 });
25 + } catch (error) {
26 + return NextResponse.json({ error: (error as Error).message }, { status: 500 });
27 + }
28 +}
29 +
30 +export async function DELETE(request: NextRequest) {
31 + const response = NextResponse.next();
32 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
33 + if (!session.userId || !(await requireAdmin(session.userId))) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
34 +
35 + const { id } = await request.json();
36 + const ok = await deleteBackup(id);
37 + return NextResponse.json({ success: ok });
38 +}
added app/api/files/[id]/annotations/route.ts +31 −0
@@ -0,0 +1,31 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { logActivity } from "@/lib/activity";
4 +
5 +export async function GET(
6 + request: NextRequest,
7 + { params }: { params: { id: string } }
8 +) {
9 + const metadata = await prisma.fileMetadata.findUnique({
10 + where: { fileId: params.id },
11 + });
12 +
13 + return NextResponse.json({ annotations: metadata?.annotations || null });
14 +}
15 +
16 +export async function PUT(
17 + request: NextRequest,
18 + { params }: { params: { id: string } }
19 +) {
20 + const { annotations } = await request.json();
21 +
22 + const metadata = await prisma.fileMetadata.upsert({
23 + where: { fileId: params.id },
24 + create: { fileId: params.id, annotations },
25 + update: { annotations },
26 + });
27 +
28 + await logActivity("ANNOTATE", params.id, "Annotations updated");
29 +
30 + return NextResponse.json(metadata);
31 +}
added app/api/files/[id]/content/route.ts +61 −0
@@ -0,0 +1,61 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getFilePath, saveVersion } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +import { writeFile, stat } from "fs/promises";
6 +import { MAX_VERSIONS_PER_FILE } from "@/lib/config";
7 +
8 +export async function PUT(
9 + request: NextRequest,
10 + { params }: { params: { id: string } }
11 +) {
12 + const file = await prisma.file.findUnique({ where: { id: params.id } });
13 +
14 + if (!file) {
15 + return NextResponse.json({ error: "File not found" }, { status: 404 });
16 + }
17 +
18 + const content = await request.text();
19 + const filePath = getFilePath(file.storagePath);
20 +
21 + // Create version before overwriting
22 + const versionCount = await prisma.fileVersion.count({ where: { fileId: file.id } });
23 + const versionResult = await saveVersion(file.storagePath, file.name);
24 +
25 + await prisma.fileVersion.create({
26 + data: {
27 + fileId: file.id,
28 + versionNum: versionCount + 1,
29 + storagePath: versionResult.storagePath,
30 + size: BigInt(versionResult.size),
31 + changeNote: "Auto-save",
32 + },
33 + });
34 +
35 + // Cleanup old versions if over limit
36 + const versions = await prisma.fileVersion.findMany({
37 + where: { fileId: file.id },
38 + orderBy: { versionNum: "asc" },
39 + });
40 + if (versions.length > MAX_VERSIONS_PER_FILE) {
41 + const toDelete = versions.slice(0, versions.length - MAX_VERSIONS_PER_FILE);
42 + for (const v of toDelete) {
43 + const { deleteVersion } = await import("@/lib/storage");
44 + await deleteVersion(v.storagePath);
45 + await prisma.fileVersion.delete({ where: { id: v.id } });
46 + }
47 + }
48 +
49 + // Write new content
50 + await writeFile(filePath, content, "utf-8");
51 + const newStat = await stat(filePath);
52 +
53 + await prisma.file.update({
54 + where: { id: params.id },
55 + data: { size: BigInt(newStat.size) },
56 + });
57 +
58 + await logActivity("EDIT", file.id, `Edited ${file.name}`);
59 +
60 + return NextResponse.json({ success: true, size: Number(newStat.size) });
61 +}
added app/api/files/[id]/download/route.ts +40 −0
@@ -0,0 +1,40 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getFilePath } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +import { stat } from "fs/promises";
6 +import { createReadStream } from "fs";
7 +
8 +export async function GET(
9 + request: NextRequest,
10 + { params }: { params: { id: string } }
11 +) {
12 + const file = await prisma.file.findUnique({ where: { id: params.id } });
13 +
14 + if (!file) {
15 + return NextResponse.json({ error: "File not found" }, { status: 404 });
16 + }
17 +
18 + const filePath = getFilePath(file.storagePath);
19 + const fileStat = await stat(filePath);
20 + const fileSize = fileStat.size;
21 +
22 + await logActivity("DOWNLOAD", file.id, `Downloaded ${file.name}`);
23 +
24 + const stream = createReadStream(filePath);
25 + const webStream = new ReadableStream({
26 + start(controller) {
27 + stream.on("data", (chunk) => controller.enqueue(chunk));
28 + stream.on("end", () => controller.close());
29 + stream.on("error", (err) => controller.error(err));
30 + },
31 + });
32 +
33 + return new Response(webStream, {
34 + headers: {
35 + "Content-Type": "application/octet-stream",
36 + "Content-Disposition": `attachment; filename="${encodeURIComponent(file.name)}"`,
37 + "Content-Length": String(fileSize),
38 + },
39 + });
40 +}
added app/api/files/[id]/duplicate/route.ts +34 −0
@@ -0,0 +1,34 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { duplicateFile } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +
6 +export async function POST(
7 + request: NextRequest,
8 + { params }: { params: { id: string } }
9 +) {
10 + const file = await prisma.file.findUnique({ where: { id: params.id } });
11 +
12 + if (!file) {
13 + return NextResponse.json({ error: "File not found" }, { status: 404 });
14 + }
15 +
16 + const { storagePath, size } = await duplicateFile(file.storagePath, file.name);
17 +
18 + const duplicated = await prisma.file.create({
19 + data: {
20 + name: `${file.name} (copy)`,
21 + storagePath,
22 + size: BigInt(size),
23 + mimeType: file.mimeType,
24 + folderId: file.folderId,
25 + },
26 + });
27 +
28 + await logActivity("UPLOAD", duplicated.id, `Duplicated ${file.name}`);
29 +
30 + return NextResponse.json(
31 + { ...duplicated, size: Number(duplicated.size) },
32 + { status: 201 }
33 + );
34 +}
added app/api/files/[id]/favorite/route.ts +24 −0
@@ -0,0 +1,24 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { logActivity } from "@/lib/activity";
4 +
5 +export async function POST(
6 + request: NextRequest,
7 + { params }: { params: { id: string } }
8 +) {
9 + const existing = await prisma.fileMetadata.findUnique({
10 + where: { fileId: params.id },
11 + });
12 +
13 + const newFavorite = !(existing?.isFavorite ?? false);
14 +
15 + const metadata = await prisma.fileMetadata.upsert({
16 + where: { fileId: params.id },
17 + create: { fileId: params.id, isFavorite: newFavorite },
18 + update: { isFavorite: newFavorite },
19 + });
20 +
21 + await logActivity("FAVORITE", params.id, newFavorite ? "Added to favorites" : "Removed from favorites");
22 +
23 + return NextResponse.json(metadata);
24 +}
added app/api/files/[id]/metadata/route.ts +39 −0
@@ -0,0 +1,39 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +
4 +export async function GET(
5 + request: NextRequest,
6 + { params }: { params: { id: string } }
7 +) {
8 + let metadata = await prisma.fileMetadata.findUnique({
9 + where: { fileId: params.id },
10 + });
11 +
12 + if (!metadata) {
13 + metadata = await prisma.fileMetadata.create({
14 + data: { fileId: params.id },
15 + });
16 + }
17 +
18 + return NextResponse.json(metadata);
19 +}
20 +
21 +export async function PATCH(
22 + request: NextRequest,
23 + { params }: { params: { id: string } }
24 +) {
25 + const body = await request.json();
26 + const data: Record<string, unknown> = {};
27 +
28 + if (body.description !== undefined) data.description = body.description;
29 + if (body.isFavorite !== undefined) data.isFavorite = body.isFavorite;
30 + if (body.customDates !== undefined) data.customDates = body.customDates;
31 +
32 + const metadata = await prisma.fileMetadata.upsert({
33 + where: { fileId: params.id },
34 + create: { fileId: params.id, ...data },
35 + update: data,
36 + });
37 +
38 + return NextResponse.json(metadata);
39 +}
added app/api/files/[id]/preview/route.ts +38 −0
@@ -0,0 +1,38 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getFilePath } from "@/lib/storage";
4 +import { stat } from "fs/promises";
5 +import { createReadStream } from "fs";
6 +
7 +export async function GET(
8 + request: NextRequest,
9 + { params }: { params: { id: string } }
10 +) {
11 + const file = await prisma.file.findUnique({ where: { id: params.id } });
12 +
13 + if (!file) {
14 + return NextResponse.json({ error: "File not found" }, { status: 404 });
15 + }
16 +
17 + const filePath = getFilePath(file.storagePath);
18 + const fileStat = await stat(filePath);
19 + const fileSize = fileStat.size;
20 +
21 + const stream = createReadStream(filePath);
22 + const webStream = new ReadableStream({
23 + start(controller) {
24 + stream.on("data", (chunk) => controller.enqueue(chunk));
25 + stream.on("end", () => controller.close());
26 + stream.on("error", (err) => controller.error(err));
27 + },
28 + });
29 +
30 + return new Response(webStream, {
31 + headers: {
32 + "Content-Type": file.mimeType,
33 + "Content-Disposition": `inline; filename="${encodeURIComponent(file.name)}"`,
34 + "Content-Length": String(fileSize),
35 + "Cache-Control": "private, max-age=3600",
36 + },
37 + });
38 +}
added app/api/files/[id]/route.ts +68 −0
@@ -0,0 +1,68 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { deleteFile } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +
6 +export async function GET(
7 + request: NextRequest,
8 + { params }: { params: { id: string } }
9 +) {
10 + const file = await prisma.file.findUnique({
11 + where: { id: params.id },
12 + include: { folder: true, sharedLinks: true },
13 + });
14 +
15 + if (!file) {
16 + return NextResponse.json({ error: "File not found" }, { status: 404 });
17 + }
18 +
19 + return NextResponse.json({ ...file, size: Number(file.size) });
20 +}
21 +
22 +export async function PATCH(
23 + request: NextRequest,
24 + { params }: { params: { id: string } }
25 +) {
26 + const body = await request.json();
27 + const { name, folderId } = body;
28 +
29 + const file = await prisma.file.findUnique({ where: { id: params.id } });
30 + if (!file) {
31 + return NextResponse.json({ error: "File not found" }, { status: 404 });
32 + }
33 +
34 + const updateData: Record<string, unknown> = {};
35 +
36 + if (name !== undefined) {
37 + updateData.name = name;
38 + await logActivity("RENAME", params.id, `Renamed to ${name}`);
39 + }
40 +
41 + if (folderId !== undefined) {
42 + updateData.folderId = folderId === "root" ? null : folderId;
43 + await logActivity("MOVE", params.id, `Moved to folder ${folderId}`);
44 + }
45 +
46 + const updated = await prisma.file.update({
47 + where: { id: params.id },
48 + data: updateData,
49 + });
50 +
51 + return NextResponse.json({ ...updated, size: Number(updated.size) });
52 +}
53 +
54 +export async function DELETE(
55 + request: NextRequest,
56 + { params }: { params: { id: string } }
57 +) {
58 + const file = await prisma.file.findUnique({ where: { id: params.id } });
59 + if (!file) {
60 + return NextResponse.json({ error: "File not found" }, { status: 404 });
61 + }
62 +
63 + await deleteFile(file.storagePath);
64 + await prisma.file.delete({ where: { id: params.id } });
65 + await logActivity("DELETE", null, `Deleted ${file.name}`);
66 +
67 + return NextResponse.json({ success: true });
68 +}
added app/api/files/[id]/save-edit/route.ts +78 −0
@@ -0,0 +1,78 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { saveFile, saveVersion } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +import { MAX_VERSIONS_PER_FILE } from "@/lib/config";
6 +
7 +export async function POST(
8 + request: NextRequest,
9 + { params }: { params: { id: string } }
10 +) {
11 + const file = await prisma.file.findUnique({ where: { id: params.id } });
12 +
13 + if (!file) {
14 + return NextResponse.json({ error: "File not found" }, { status: 404 });
15 + }
16 +
17 + const formData = await request.formData();
18 + const blob = formData.get("file") as File;
19 + const asNewVersion = formData.get("asNewVersion") === "true";
20 +
21 + if (!blob) {
22 + return NextResponse.json({ error: "No file provided" }, { status: 400 });
23 + }
24 +
25 + const buffer = Buffer.from(await blob.arrayBuffer());
26 +
27 + if (asNewVersion) {
28 + // Save current as version
29 + const versionCount = await prisma.fileVersion.count({ where: { fileId: file.id } });
30 + const versionResult = await saveVersion(file.storagePath, file.name);
31 +
32 + await prisma.fileVersion.create({
33 + data: {
34 + fileId: file.id,
35 + versionNum: versionCount + 1,
36 + storagePath: versionResult.storagePath,
37 + size: BigInt(versionResult.size),
38 + changeNote: "Before edit",
39 + },
40 + });
41 +
42 + // Cleanup old versions
43 + const versions = await prisma.fileVersion.findMany({
44 + where: { fileId: file.id },
45 + orderBy: { versionNum: "asc" },
46 + });
47 + if (versions.length > MAX_VERSIONS_PER_FILE) {
48 + const toDelete = versions.slice(0, versions.length - MAX_VERSIONS_PER_FILE);
49 + for (const v of toDelete) {
50 + const { deleteVersion } = await import("@/lib/storage");
51 + await deleteVersion(v.storagePath);
52 + await prisma.fileVersion.delete({ where: { id: v.id } });
53 + }
54 + }
55 + }
56 +
57 + // Save edited file
58 + const result = await saveFile(buffer, file.name);
59 +
60 + // Delete old file and update record
61 + const { deleteFile } = await import("@/lib/storage");
62 + await deleteFile(file.storagePath);
63 +
64 + const updated = await prisma.file.update({
65 + where: { id: params.id },
66 + data: {
67 + storagePath: result.storagePath,
68 + size: BigInt(result.size),
69 + },
70 + });
71 +
72 + await logActivity("EDIT", file.id, `Edited ${file.name}`);
73 +
74 + return NextResponse.json({
75 + ...updated,
76 + size: Number(updated.size),
77 + });
78 +}
added app/api/files/[id]/stream/route.ts +69 −0
@@ -0,0 +1,69 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getFilePath } from "@/lib/storage";
4 +import { stat } from "fs/promises";
5 +import { createReadStream } from "fs";
6 +
7 +export async function GET(
8 + request: NextRequest,
9 + { params }: { params: { id: string } }
10 +) {
11 + const file = await prisma.file.findUnique({ where: { id: params.id } });
12 +
13 + if (!file) {
14 + return NextResponse.json({ error: "File not found" }, { status: 404 });
15 + }
16 +
17 + const filePath = getFilePath(file.storagePath);
18 + const fileStat = await stat(filePath);
19 + const fileSize = fileStat.size;
20 +
21 + const range = request.headers.get("range");
22 +
23 + if (range) {
24 + const parts = range.replace(/bytes=/, "").split("-");
25 + const start = parseInt(parts[0], 10);
26 + const end = parts[1]
27 + ? parseInt(parts[1], 10)
28 + : Math.min(start + 1024 * 1024 - 1, fileSize - 1);
29 + const chunkSize = end - start + 1;
30 +
31 + const stream = createReadStream(filePath, { start, end });
32 + const webStream = new ReadableStream({
33 + start(controller) {
34 + stream.on("data", (chunk) => controller.enqueue(chunk));
35 + stream.on("end", () => controller.close());
36 + stream.on("error", (err) => controller.error(err));
37 + },
38 + });
39 +
40 + return new Response(webStream, {
41 + status: 206,
42 + headers: {
43 + "Content-Range": `bytes ${start}-${end}/${fileSize}`,
44 + "Accept-Ranges": "bytes",
45 + "Content-Length": String(chunkSize),
46 + "Content-Type": file.mimeType,
47 + "Cache-Control": "private, max-age=3600",
48 + },
49 + });
50 + }
51 +
52 + const stream = createReadStream(filePath);
53 + const webStream = new ReadableStream({
54 + start(controller) {
55 + stream.on("data", (chunk) => controller.enqueue(chunk));
56 + stream.on("end", () => controller.close());
57 + stream.on("error", (err) => controller.error(err));
58 + },
59 + });
60 +
61 + return new Response(webStream, {
62 + headers: {
63 + "Content-Length": String(fileSize),
64 + "Content-Type": file.mimeType,
65 + "Accept-Ranges": "bytes",
66 + "Cache-Control": "private, max-age=3600",
67 + },
68 + });
69 +}
added app/api/files/[id]/tags/[tagId]/route.ts +22 −0
@@ -0,0 +1,22 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { logActivity } from "@/lib/activity";
4 +
5 +export async function DELETE(
6 + request: NextRequest,
7 + { params }: { params: { id: string; tagId: string } }
8 +) {
9 + const fileTag = await prisma.fileTag.findFirst({
10 + where: { fileId: params.id, tagId: params.tagId },
11 + include: { tag: true },
12 + });
13 +
14 + if (!fileTag) {
15 + return NextResponse.json({ error: "Tag not found" }, { status: 404 });
16 + }
17 +
18 + await prisma.fileTag.delete({ where: { id: fileTag.id } });
19 + await logActivity("TAG_REMOVE", params.id, `Tag removed: ${fileTag.tag.name}`);
20 +
21 + return NextResponse.json({ success: true });
22 +}
added app/api/files/[id]/tags/route.ts +33 −0
@@ -0,0 +1,33 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { logActivity } from "@/lib/activity";
4 +
5 +export async function POST(
6 + request: NextRequest,
7 + { params }: { params: { id: string } }
8 +) {
9 + const { tagId } = await request.json();
10 +
11 + if (!tagId) {
12 + return NextResponse.json({ error: "tagId required" }, { status: 400 });
13 + }
14 +
15 + const fileTag = await prisma.fileTag.create({
16 + data: { fileId: params.id, tagId },
17 + include: { tag: true },
18 + });
19 +
20 + await logActivity("TAG_ADD", params.id, `Tag added: ${fileTag.tag.name}`);
21 + return NextResponse.json(fileTag, { status: 201 });
22 +}
23 +
24 +export async function GET(
25 + request: NextRequest,
26 + { params }: { params: { id: string } }
27 +) {
28 + const tags = await prisma.fileTag.findMany({
29 + where: { fileId: params.id },
30 + include: { tag: true },
31 + });
32 + return NextResponse.json(tags);
33 +}
added app/api/files/[id]/versions/[versionId]/diff/route.ts +41 −0
@@ -0,0 +1,41 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getFilePath, getVersionPath } from "@/lib/storage";
4 +import { readFile } from "fs/promises";
5 +
6 +export async function GET(
7 + request: NextRequest,
8 + { params }: { params: { id: string; versionId: string } }
9 +) {
10 + const file = await prisma.file.findUnique({ where: { id: params.id } });
11 + const version = await prisma.fileVersion.findUnique({
12 + where: { id: params.versionId },
13 + });
14 +
15 + if (!file || !version || version.fileId !== params.id) {
16 + return NextResponse.json({ error: "Not found" }, { status: 404 });
17 + }
18 +
19 + // Only text files can be diffed
20 + if (!file.mimeType.startsWith("text/") && !file.mimeType.includes("json") && !file.mimeType.includes("xml") && !file.mimeType.includes("javascript") && !file.mimeType.includes("typescript")) {
21 + return NextResponse.json({ error: "Not a text file" }, { status: 400 });
22 + }
23 +
24 + const currentContent = await readFile(getFilePath(file.storagePath), "utf-8");
25 + const versionContent = await readFile(getVersionPath(version.storagePath), "utf-8");
26 +
27 + const Diff = await import("diff");
28 + const patch = Diff.createPatch(
29 + file.name,
30 + versionContent,
31 + currentContent,
32 + `Version ${version.versionNum}`,
33 + "Current"
34 + );
35 +
36 + return NextResponse.json({
37 + current: currentContent,
38 + version: versionContent,
39 + diff: patch,
40 + });
41 +}
added app/api/files/[id]/versions/[versionId]/restore/route.ts +45 −0
@@ -0,0 +1,45 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { saveVersion, restoreVersion } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +
6 +export async function POST(
7 + request: NextRequest,
8 + { params }: { params: { id: string; versionId: string } }
9 +) {
10 + const file = await prisma.file.findUnique({ where: { id: params.id } });
11 + const version = await prisma.fileVersion.findUnique({
12 + where: { id: params.versionId },
13 + });
14 +
15 + if (!file || !version || version.fileId !== params.id) {
16 + return NextResponse.json({ error: "Not found" }, { status: 404 });
17 + }
18 +
19 + // Save current as new version first
20 + const versionCount = await prisma.fileVersion.count({ where: { fileId: file.id } });
21 + const currentVersion = await saveVersion(file.storagePath, file.name);
22 +
23 + await prisma.fileVersion.create({
24 + data: {
25 + fileId: file.id,
26 + versionNum: versionCount + 1,
27 + storagePath: currentVersion.storagePath,
28 + size: BigInt(currentVersion.size),
29 + changeNote: "Before restore",
30 + },
31 + });
32 +
33 + // Restore the selected version
34 + await restoreVersion(version.storagePath, file.storagePath);
35 +
36 + // Update file size
37 + await prisma.file.update({
38 + where: { id: params.id },
39 + data: { size: version.size },
40 + });
41 +
42 + await logActivity("VERSION_RESTORE", file.id, `Restored to version ${version.versionNum}`);
43 +
44 + return NextResponse.json({ success: true });
45 +}
added app/api/files/[id]/versions/[versionId]/route.ts +39 −0
@@ -0,0 +1,39 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getVersionPath } from "@/lib/storage";
4 +import { createReadStream } from "fs";
5 +import { stat } from "fs/promises";
6 +
7 +export async function GET(
8 + request: NextRequest,
9 + { params }: { params: { id: string; versionId: string } }
10 +) {
11 + const version = await prisma.fileVersion.findUnique({
12 + where: { id: params.versionId },
13 + include: { file: true },
14 + });
15 +
16 + if (!version || version.fileId !== params.id) {
17 + return NextResponse.json({ error: "Version not found" }, { status: 404 });
18 + }
19 +
20 + const filePath = getVersionPath(version.storagePath);
21 + const fileStat = await stat(filePath);
22 +
23 + const stream = createReadStream(filePath);
24 + const webStream = new ReadableStream({
25 + start(controller) {
26 + stream.on("data", (chunk) => controller.enqueue(chunk));
27 + stream.on("end", () => controller.close());
28 + stream.on("error", (err) => controller.error(err));
29 + },
30 + });
31 +
32 + return new Response(webStream, {
33 + headers: {
34 + "Content-Type": "application/octet-stream",
35 + "Content-Disposition": `attachment; filename="${encodeURIComponent(version.file.name)}"`,
36 + "Content-Length": String(fileStat.size),
37 + },
38 + });
39 +}
added app/api/files/[id]/versions/route.ts +49 −0
@@ -0,0 +1,49 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { saveVersion } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +
6 +export async function GET(
7 + request: NextRequest,
8 + { params }: { params: { id: string } }
9 +) {
10 + const versions = await prisma.fileVersion.findMany({
11 + where: { fileId: params.id },
12 + orderBy: { versionNum: "desc" },
13 + });
14 +
15 + return NextResponse.json(
16 + versions.map((v) => ({ ...v, size: Number(v.size) }))
17 + );
18 +}
19 +
20 +export async function POST(
21 + request: NextRequest,
22 + { params }: { params: { id: string } }
23 +) {
24 + const file = await prisma.file.findUnique({ where: { id: params.id } });
25 +
26 + if (!file) {
27 + return NextResponse.json({ error: "File not found" }, { status: 404 });
28 + }
29 +
30 + const body = await request.json().catch(() => ({}));
31 + const changeNote = body.changeNote || "Manual snapshot";
32 +
33 + const versionCount = await prisma.fileVersion.count({ where: { fileId: file.id } });
34 + const result = await saveVersion(file.storagePath, file.name);
35 +
36 + const version = await prisma.fileVersion.create({
37 + data: {
38 + fileId: file.id,
39 + versionNum: versionCount + 1,
40 + storagePath: result.storagePath,
41 + size: BigInt(result.size),
42 + changeNote,
43 + },
44 + });
45 +
46 + await logActivity("VERSION_CREATE", file.id, `Version ${version.versionNum} created`);
47 +
48 + return NextResponse.json({ ...version, size: Number(version.size) }, { status: 201 });
49 +}
added app/api/files/bulk/route.ts +74 −0
@@ -0,0 +1,74 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +import { deleteFile, getFilePath } from "@/lib/storage";
6 +import { logActivity } from "@/lib/activity";
7 +import { encryptFile, removeEncryption } from "@/lib/encryption";
8 +import JSZip from "jszip";
9 +import { readFileSync, existsSync } from "fs";
10 +
11 +export async function POST(request: NextRequest) {
12 + const response = NextResponse.next();
13 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
14 + if (!session.isLoggedIn) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
15 +
16 + const body = await request.json();
17 + const { action, fileIds } = body as { action: string; fileIds: string[] };
18 +
19 + if (!action || !fileIds?.length) return NextResponse.json({ error: "action and fileIds required" }, { status: 400 });
20 +
21 + if (action === "delete") {
22 + const files = await prisma.file.findMany({ where: { id: { in: fileIds } } });
23 + for (const file of files) {
24 + await deleteFile(file.storagePath);
25 + await prisma.file.delete({ where: { id: file.id } });
26 + await logActivity("DELETE", file.id, `Bulk delete: ${file.name}`);
27 + }
28 + return NextResponse.json({ success: true, count: files.length });
29 + }
30 +
31 + if (action === "move") {
32 + const { targetFolderId } = body;
33 + await prisma.file.updateMany({
34 + where: { id: { in: fileIds } },
35 + data: { folderId: targetFolderId === "root" ? null : targetFolderId },
36 + });
37 + return NextResponse.json({ success: true });
38 + }
39 +
40 + if (action === "download") {
41 + const files = await prisma.file.findMany({ where: { id: { in: fileIds } } });
42 + const zip = new JSZip();
43 + for (const file of files) {
44 + const filePath = getFilePath(file.storagePath);
45 + if (existsSync(filePath)) {
46 + zip.file(file.name, readFileSync(filePath));
47 + }
48 + }
49 + const buffer = await zip.generateAsync({ type: "nodebuffer" });
50 + return new NextResponse(buffer, {
51 + headers: { "Content-Type": "application/zip", "Content-Disposition": `attachment; filename="files.zip"` },
52 + });
53 + }
54 +
55 + if (action === "encrypt") {
56 + let count = 0;
57 + for (const id of fileIds) {
58 + const ok = await encryptFile(id);
59 + if (ok) count++;
60 + }
61 + return NextResponse.json({ success: true, count });
62 + }
63 +
64 + if (action === "decrypt") {
65 + let count = 0;
66 + for (const id of fileIds) {
67 + const ok = await removeEncryption(id);
68 + if (ok) count++;
69 + }
70 + return NextResponse.json({ success: true, count });
71 + }
72 +
73 + return NextResponse.json({ error: "Unknown action" }, { status: 400 });
74 +}
added app/api/files/route.ts +111 −0
@@ -0,0 +1,111 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { saveFile } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +import mime from "mime-types";
6 +
7 +export async function GET(request: NextRequest) {
8 + const { searchParams } = new URL(request.url);
9 + const folderId = searchParams.get("folderId");
10 + const search = searchParams.get("search");
11 + const type = searchParams.get("type");
12 + const sortBy = searchParams.get("sortBy") || "createdAt";
13 + const order = searchParams.get("order") || "desc";
14 + const dateFrom = searchParams.get("dateFrom");
15 + const dateTo = searchParams.get("dateTo");
16 + const sizeMin = searchParams.get("sizeMin");
17 + const sizeMax = searchParams.get("sizeMax");
18 +
19 + const favorite = searchParams.get("favorite");
20 + const tagId = searchParams.get("tagId");
21 +
22 + const where: Record<string, unknown> = {};
23 +
24 + // When filtering by favorite or tag, don't filter by folder
25 + if (favorite === "true") {
26 + where.metadata = { isFavorite: true };
27 + } else if (tagId) {
28 + where.tags = { some: { tagId } };
29 + } else if (folderId === "root" || folderId === null) {
30 + where.folderId = null;
31 + } else if (folderId) {
32 + where.folderId = folderId;
33 + }
34 +
35 + if (search) {
36 + where.name = { contains: search, mode: "insensitive" };
37 + }
38 +
39 + if (type) {
40 + where.mimeType = { startsWith: type };
41 + }
42 +
43 + if (dateFrom || dateTo) {
44 + where.createdAt = {};
45 + if (dateFrom) (where.createdAt as Record<string, unknown>).gte = new Date(dateFrom);
46 + if (dateTo) (where.createdAt as Record<string, unknown>).lte = new Date(dateTo);
47 + }
48 +
49 + if (sizeMin || sizeMax) {
50 + where.size = {};
51 + if (sizeMin) (where.size as Record<string, unknown>).gte = BigInt(sizeMin);
52 + if (sizeMax) (where.size as Record<string, unknown>).lte = BigInt(sizeMax);
53 + }
54 +
55 + const files = await prisma.file.findMany({
56 + where,
57 + orderBy: { [sortBy]: order },
58 + include: {
59 + folder: true,
60 + metadata: true,
61 + tags: { include: { tag: true } },
62 + _count: { select: { versions: true } },
63 + },
64 + });
65 +
66 + const serialized = files.map((f) => ({
67 + ...f,
68 + size: Number(f.size),
69 + }));
70 +
71 + return NextResponse.json(serialized);
72 +}
73 +
74 +export async function POST(request: NextRequest) {
75 + const formData = await request.formData();
76 + const file = formData.get("file") as File | null;
77 + const folderId = formData.get("folderId") as string | null;
78 +
79 + if (!file) {
80 + return NextResponse.json({ error: "No file provided" }, { status: 400 });
81 + }
82 +
83 + const maxSize = parseInt(process.env.MAX_FILE_SIZE || "104857600");
84 + if (file.size > maxSize) {
85 + return NextResponse.json(
86 + { error: `File too large. Max ${Math.round(maxSize / 1024 / 1024)}MB` },
87 + { status: 413 }
88 + );
89 + }
90 +
91 + const buffer = Buffer.from(await file.arrayBuffer());
92 + const mimeType = mime.lookup(file.name) || file.type || "application/octet-stream";
93 + const { storagePath, size } = await saveFile(buffer, file.name);
94 +
95 + const dbFile = await prisma.file.create({
96 + data: {
97 + name: file.name,
98 + storagePath,
99 + size: BigInt(size),
100 + mimeType,
101 + folderId: folderId && folderId !== "root" ? folderId : null,
102 + },
103 + });
104 +
105 + await logActivity("UPLOAD", dbFile.id, `Uploaded ${file.name}`);
106 +
107 + return NextResponse.json(
108 + { ...dbFile, size: Number(dbFile.size) },
109 + { status: 201 }
110 + );
111 +}
added app/api/files/upload-chunk/route.ts +47 −0
@@ -0,0 +1,47 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { saveChunk, assembleChunks } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +import mime from "mime-types";
6 +
7 +export async function POST(request: NextRequest) {
8 + const formData = await request.formData();
9 + const chunk = formData.get("chunk") as File | null;
10 + const uploadId = formData.get("uploadId") as string;
11 + const chunkIndex = parseInt(formData.get("chunkIndex") as string);
12 + const totalChunks = parseInt(formData.get("totalChunks") as string);
13 + const fileName = formData.get("fileName") as string;
14 + const folderId = formData.get("folderId") as string | null;
15 +
16 + if (!chunk || !uploadId || isNaN(chunkIndex) || isNaN(totalChunks) || !fileName) {
17 + return NextResponse.json({ error: "Missing chunk data" }, { status: 400 });
18 + }
19 +
20 + const buffer = Buffer.from(await chunk.arrayBuffer());
21 + await saveChunk(buffer, uploadId, chunkIndex);
22 +
23 + // If this is the last chunk, assemble
24 + if (chunkIndex === totalChunks - 1) {
25 + const { storagePath, size } = await assembleChunks(uploadId, totalChunks, fileName);
26 + const mimeType = mime.lookup(fileName) || "application/octet-stream";
27 +
28 + const dbFile = await prisma.file.create({
29 + data: {
30 + name: fileName,
31 + storagePath,
32 + size: BigInt(size),
33 + mimeType,
34 + folderId: folderId && folderId !== "root" ? folderId : null,
35 + },
36 + });
37 +
38 + await logActivity("UPLOAD", dbFile.id, `Uploaded ${fileName} (chunked)`);
39 +
40 + return NextResponse.json(
41 + { ...dbFile, size: Number(dbFile.size), complete: true },
42 + { status: 201 }
43 + );
44 + }
45 +
46 + return NextResponse.json({ chunkIndex, received: true });
47 +}
added app/api/folders/[id]/download/route.ts +30 −0
@@ -0,0 +1,30 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getFilePath } from "@/lib/storage";
4 +import JSZip from "jszip";
5 +import { readFileSync, existsSync } from "fs";
6 +
7 +async function addFolderToZip(zip: JSZip, folderId: string, prefix: string) {
8 + const files = await prisma.file.findMany({ where: { folderId } });
9 + for (const file of files) {
10 + const filePath = getFilePath(file.storagePath);
11 + if (existsSync(filePath)) zip.file(prefix + file.name, readFileSync(filePath));
12 + }
13 + const subfolders = await prisma.folder.findMany({ where: { parentId: folderId } });
14 + for (const sub of subfolders) {
15 + await addFolderToZip(zip, sub.id, `${prefix}${sub.name}/`);
16 + }
17 +}
18 +
19 +export async function GET(_request: NextRequest, { params }: { params: { id: string } }) {
20 + const folder = await prisma.folder.findUnique({ where: { id: params.id } });
21 + if (!folder) return NextResponse.json({ error: "Not found" }, { status: 404 });
22 +
23 + const zip = new JSZip();
24 + await addFolderToZip(zip, folder.id, "");
25 + const buffer = await zip.generateAsync({ type: "nodebuffer" });
26 +
27 + return new NextResponse(buffer, {
28 + headers: { "Content-Type": "application/zip", "Content-Disposition": `attachment; filename="${folder.name}.zip"` },
29 + });
30 +}
added app/api/folders/[id]/route.ts +83 −0
@@ -0,0 +1,83 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { deleteFile } from "@/lib/storage";
4 +import { logActivity } from "@/lib/activity";
5 +
6 +export async function GET(
7 + request: NextRequest,
8 + { params }: { params: { id: string } }
9 +) {
10 + const folder = await prisma.folder.findUnique({
11 + where: { id: params.id },
12 + include: {
13 + children: {
14 + include: { _count: { select: { files: true, children: true } } },
15 + orderBy: { name: "asc" },
16 + },
17 + files: { orderBy: { createdAt: "desc" } },
18 + parent: true,
19 + },
20 + });
21 +
22 + if (!folder) {
23 + return NextResponse.json({ error: "Folder not found" }, { status: 404 });
24 + }
25 +
26 + return NextResponse.json({
27 + ...folder,
28 + files: folder.files.map((f) => ({ ...f, size: Number(f.size) })),
29 + });
30 +}
31 +
32 +export async function PATCH(
33 + request: NextRequest,
34 + { params }: { params: { id: string } }
35 +) {
36 + const body = await request.json();
37 + const { name } = body;
38 +
39 + if (!name || !name.trim()) {
40 + return NextResponse.json({ error: "Name required" }, { status: 400 });
41 + }
42 +
43 + const folder = await prisma.folder.update({
44 + where: { id: params.id },
45 + data: { name: name.trim() },
46 + });
47 +
48 + return NextResponse.json(folder);
49 +}
50 +
51 +export async function DELETE(
52 + request: NextRequest,
53 + { params }: { params: { id: string } }
54 +) {
55 + // Recursively delete all files in this folder and subfolders
56 + async function deleteRecursive(folderId: string) {
57 + const folder = await prisma.folder.findUnique({
58 + where: { id: folderId },
59 + include: { files: true, children: true },
60 + });
61 +
62 + if (!folder) return;
63 +
64 + // Delete files
65 + for (const file of folder.files) {
66 + await deleteFile(file.storagePath);
67 + }
68 +
69 + // Recurse into children
70 + for (const child of folder.children) {
71 + await deleteRecursive(child.id);
72 + }
73 + }
74 +
75 + await deleteRecursive(params.id);
76 +
77 + // Prisma cascade will handle DB records
78 + await prisma.folder.delete({ where: { id: params.id } });
79 +
80 + await logActivity("DELETE_FOLDER", null, `Deleted folder ${params.id}`);
81 +
82 + return NextResponse.json({ success: true });
83 +}
added app/api/folders/route.ts +64 −0
@@ -0,0 +1,64 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { logActivity } from "@/lib/activity";
4 +
5 +export async function GET(request: NextRequest) {
6 + const { searchParams } = new URL(request.url);
7 + const parentId = searchParams.get("parentId");
8 + const tree = searchParams.get("tree");
9 +
10 + if (tree === "true") {
11 + const folders = await prisma.folder.findMany({
12 + include: {
13 + children: {
14 + include: {
15 + children: {
16 + include: { children: true },
17 + },
18 + },
19 + },
20 + _count: { select: { files: true } },
21 + },
22 + where: { parentId: null },
23 + orderBy: { name: "asc" },
24 + });
25 + return NextResponse.json(folders);
26 + }
27 +
28 + const where: Record<string, unknown> = {};
29 + if (parentId === "root" || parentId === null) {
30 + where.parentId = null;
31 + } else {
32 + where.parentId = parentId;
33 + }
34 +
35 + const folders = await prisma.folder.findMany({
36 + where,
37 + include: {
38 + _count: { select: { files: true, children: true } },
39 + },
40 + orderBy: { name: "asc" },
41 + });
42 +
43 + return NextResponse.json(folders);
44 +}
45 +
46 +export async function POST(request: NextRequest) {
47 + const body = await request.json();
48 + const { name, parentId } = body;
49 +
50 + if (!name || !name.trim()) {
51 + return NextResponse.json({ error: "Folder name required" }, { status: 400 });
52 + }
53 +
54 + const folder = await prisma.folder.create({
55 + data: {
56 + name: name.trim(),
57 + parentId: parentId && parentId !== "root" ? parentId : null,
58 + },
59 + });
60 +
61 + await logActivity("CREATE_FOLDER", null, `Created folder ${name}`);
62 +
63 + return NextResponse.json(folder, { status: 201 });
64 +}
added app/api/keys/[id]/route.ts +13 −0
@@ -0,0 +1,13 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +
6 +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
7 + const response = NextResponse.next();
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 + if (!session.isLoggedIn || !session.userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 +
11 + await prisma.apiKey.delete({ where: { id: params.id, userId: session.userId } });
12 + return NextResponse.json({ success: true });
13 +}
added app/api/keys/route.ts +37 −0
@@ -0,0 +1,37 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +import { randomBytes, createHash } from "crypto";
6 +
7 +export async function GET(request: NextRequest) {
8 + const response = NextResponse.next();
9 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
10 + if (!session.isLoggedIn || !session.userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11 +
12 + const keys = await prisma.apiKey.findMany({
13 + where: { userId: session.userId },
14 + select: { id: true, name: true, keyPrefix: true, permissions: true, lastUsedAt: true, expiresAt: true, createdAt: true },
15 + orderBy: { createdAt: "desc" },
16 + });
17 + return NextResponse.json(keys);
18 +}
19 +
20 +export async function POST(request: NextRequest) {
21 + const response = NextResponse.next();
22 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
23 + if (!session.isLoggedIn || !session.userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
24 +
25 + const { name, permissions, expiresAt } = await request.json();
26 + if (!name) return NextResponse.json({ error: "Name required" }, { status: 400 });
27 +
28 + const rawKey = `spb_${randomBytes(32).toString("hex")}`;
29 + const keyHash = createHash("sha256").update(rawKey).digest("hex");
30 + const keyPrefix = rawKey.substring(0, 12);
31 +
32 + await prisma.apiKey.create({
33 + data: { userId: session.userId, name, keyHash, keyPrefix, permissions: permissions || "read", expiresAt: expiresAt ? new Date(expiresAt) : null },
34 + });
35 +
36 + return NextResponse.json({ key: rawKey, prefix: keyPrefix }, { status: 201 });
37 +}
added app/api/notifications/[id]/route.ts +7 −0
@@ -0,0 +1,7 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { markAsRead } from "@/lib/notifications";
3 +
4 +export async function PATCH(_request: NextRequest, { params }: { params: { id: string } }) {
5 + await markAsRead(params.id);
6 + return NextResponse.json({ success: true });
7 +}
added app/api/notifications/route.ts +35 −0
@@ -0,0 +1,35 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { getNotifications, markAllAsRead, getUnreadCount } from "@/lib/notifications";
5 +
6 +export async function GET(request: NextRequest) {
7 + const response = NextResponse.next();
8 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
9 + if (!session.isLoggedIn || !session.userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10 +
11 + const { searchParams } = new URL(request.url);
12 + const unreadOnly = searchParams.get("unread") === "true";
13 + const countOnly = searchParams.get("count") === "true";
14 +
15 + if (countOnly) {
16 + const count = await getUnreadCount(session.userId);
17 + return NextResponse.json({ count });
18 + }
19 +
20 + const notifications = await getNotifications(session.userId, unreadOnly);
21 + return NextResponse.json(notifications);
22 +}
23 +
24 +export async function POST(request: NextRequest) {
25 + const response = NextResponse.next();
26 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
27 + if (!session.isLoggedIn || !session.userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
28 +
29 + const { action } = await request.json();
30 + if (action === "mark_all_read") {
31 + await markAllAsRead(session.userId);
32 + return NextResponse.json({ success: true });
33 + }
34 + return NextResponse.json({ error: "Unknown action" }, { status: 400 });
35 +}
added app/api/shared/[token]/route.ts +57 −0
@@ -0,0 +1,57 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getFilePath } from "@/lib/storage";
4 +import { readFile } from "fs/promises";
5 +
6 +export async function GET(
7 + request: NextRequest,
8 + { params }: { params: { token: string } }
9 +) {
10 + const share = await prisma.sharedLink.findUnique({
11 + where: { token: params.token },
12 + include: { file: true },
13 + });
14 +
15 + if (!share || !share.active) {
16 + return NextResponse.json({ error: "Link not found or inactive" }, { status: 404 });
17 + }
18 +
19 + if (share.expiresAt && new Date() > share.expiresAt) {
20 + return NextResponse.json({ error: "Link expired" }, { status: 410 });
21 + }
22 +
23 + if (share.passwordHash) {
24 + return NextResponse.json({
25 + requiresPassword: true,
26 + fileName: share.file.name,
27 + fileType: share.file.mimeType,
28 + mode: share.mode,
29 + });
30 + }
31 +
32 + const { searchParams } = new URL(request.url);
33 + const action = searchParams.get("action");
34 +
35 + if (action === "download" || share.mode === "DOWNLOAD") {
36 + const filePath = getFilePath(share.file.storagePath);
37 + const buffer = await readFile(filePath);
38 +
39 + return new NextResponse(buffer, {
40 + headers: {
41 + "Content-Type": "application/octet-stream",
42 + "Content-Disposition": `attachment; filename="${encodeURIComponent(share.file.name)}"`,
43 + },
44 + });
45 + }
46 +
47 + // Preview mode
48 + const filePath = getFilePath(share.file.storagePath);
49 + const buffer = await readFile(filePath);
50 +
51 + return new NextResponse(buffer, {
52 + headers: {
53 + "Content-Type": share.file.mimeType,
54 + "Content-Disposition": `inline; filename="${encodeURIComponent(share.file.name)}"`,
55 + },
56 + });
57 +}
added app/api/shared/[token]/verify/route.ts +47 −0
@@ -0,0 +1,47 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { getFilePath } from "@/lib/storage";
4 +import { readFile } from "fs/promises";
5 +import bcrypt from "bcryptjs";
6 +
7 +export async function POST(
8 + request: NextRequest,
9 + { params }: { params: { token: string } }
10 +) {
11 + const body = await request.json();
12 + const { password } = body;
13 +
14 + const share = await prisma.sharedLink.findUnique({
15 + where: { token: params.token },
16 + include: { file: true },
17 + });
18 +
19 + if (!share || !share.active) {
20 + return NextResponse.json({ error: "Link not found" }, { status: 404 });
21 + }
22 +
23 + if (share.expiresAt && new Date() > share.expiresAt) {
24 + return NextResponse.json({ error: "Link expired" }, { status: 410 });
25 + }
26 +
27 + if (!share.passwordHash) {
28 + return NextResponse.json({ error: "No password required" }, { status: 400 });
29 + }
30 +
31 + const valid = await bcrypt.compare(password, share.passwordHash);
32 + if (!valid) {
33 + return NextResponse.json({ error: "Wrong password" }, { status: 401 });
34 + }
35 +
36 + const filePath = getFilePath(share.file.storagePath);
37 + const buffer = await readFile(filePath);
38 +
39 + const isDownload = share.mode === "DOWNLOAD";
40 +
41 + return new NextResponse(buffer, {
42 + headers: {
43 + "Content-Type": isDownload ? "application/octet-stream" : share.file.mimeType,
44 + "Content-Disposition": `${isDownload ? "attachment" : "inline"}; filename="${encodeURIComponent(share.file.name)}"`,
45 + },
46 + });
47 +}
added app/api/shares/[id]/route.ts +37 −0
@@ -0,0 +1,37 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { logActivity } from "@/lib/activity";
4 +
5 +export async function PATCH(
6 + request: NextRequest,
7 + { params }: { params: { id: string } }
8 +) {
9 + const body = await request.json();
10 + const { active } = body;
11 +
12 + const share = await prisma.sharedLink.update({
13 + where: { id: params.id },
14 + data: { active },
15 + });
16 +
17 + if (!active) {
18 + await logActivity("UNSHARE", share.fileId, "Link deactivated");
19 + }
20 +
21 + return NextResponse.json(share);
22 +}
23 +
24 +export async function DELETE(
25 + request: NextRequest,
26 + { params }: { params: { id: string } }
27 +) {
28 + const share = await prisma.sharedLink.findUnique({ where: { id: params.id } });
29 + if (!share) {
30 + return NextResponse.json({ error: "Share not found" }, { status: 404 });
31 + }
32 +
33 + await prisma.sharedLink.delete({ where: { id: params.id } });
34 + await logActivity("UNSHARE", share.fileId, "Link deleted");
35 +
36 + return NextResponse.json({ success: true });
37 +}
added app/api/shares/route.ts +56 −0
@@ -0,0 +1,56 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { logActivity } from "@/lib/activity";
4 +import bcrypt from "bcryptjs";
5 +
6 +export async function GET() {
7 + const shares = await prisma.sharedLink.findMany({
8 + include: {
9 + file: { select: { name: true, mimeType: true, size: true } },
10 + },
11 + orderBy: { createdAt: "desc" },
12 + });
13 +
14 + const serialized = shares.map((s) => ({
15 + ...s,
16 + file: s.file ? { ...s.file, size: Number(s.file.size) } : null,
17 + }));
18 +
19 + return NextResponse.json(serialized);
20 +}
21 +
22 +export async function POST(request: NextRequest) {
23 + const body = await request.json();
24 + const { fileId, expiresAt, password, mode } = body;
25 +
26 + if (!fileId) {
27 + return NextResponse.json({ error: "fileId required" }, { status: 400 });
28 + }
29 +
30 + const file = await prisma.file.findUnique({ where: { id: fileId } });
31 + if (!file) {
32 + return NextResponse.json({ error: "File not found" }, { status: 404 });
33 + }
34 +
35 + const data: Record<string, unknown> = {
36 + fileId,
37 + mode: mode || "DOWNLOAD",
38 + active: true,
39 + };
40 +
41 + if (expiresAt) {
42 + data.expiresAt = new Date(expiresAt);
43 + }
44 +
45 + if (password) {
46 + data.passwordHash = await bcrypt.hash(password, 10);
47 + }
48 +
49 + const share = await prisma.sharedLink.create({
50 + data: data as Parameters<typeof prisma.sharedLink.create>[0]["data"],
51 + });
52 +
53 + await logActivity("SHARE", fileId, `Shared ${file.name}`);
54 +
55 + return NextResponse.json(share, { status: 201 });
56 +}
added app/api/tags/[id]/route.ts +27 −0
@@ -0,0 +1,27 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +
4 +export async function DELETE(
5 + request: NextRequest,
6 + { params }: { params: { id: string } }
7 +) {
8 + await prisma.tag.delete({ where: { id: params.id } });
9 + return NextResponse.json({ success: true });
10 +}
11 +
12 +export async function PATCH(
13 + request: NextRequest,
14 + { params }: { params: { id: string } }
15 +) {
16 + const { name, color } = await request.json();
17 + const data: Record<string, string> = {};
18 + if (name) data.name = name.trim();
19 + if (color) data.color = color;
20 +
21 + const tag = await prisma.tag.update({
22 + where: { id: params.id },
23 + data,
24 + });
25 +
26 + return NextResponse.json(tag);
27 +}
added app/api/tags/route.ts +24 −0
@@ -0,0 +1,24 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +
4 +export async function GET() {
5 + const tags = await prisma.tag.findMany({
6 + orderBy: { name: "asc" },
7 + include: { _count: { select: { files: true } } },
8 + });
9 + return NextResponse.json(tags);
10 +}
11 +
12 +export async function POST(request: NextRequest) {
13 + const { name, color } = await request.json();
14 +
15 + if (!name || typeof name !== "string" || !name.trim()) {
16 + return NextResponse.json({ error: "Name required" }, { status: 400 });
17 + }
18 +
19 + const tag = await prisma.tag.create({
20 + data: { name: name.trim(), color: color || "#6b7280" },
21 + });
22 +
23 + return NextResponse.json(tag, { status: 201 });
24 +}
added app/api/users/[id]/route.ts +35 −0
@@ -0,0 +1,35 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +import { hashPassword, requireAdmin } from "@/lib/auth";
6 +
7 +export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
8 + const response = NextResponse.next();
9 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
10 + if (!session.userId || !(await requireAdmin(session.userId))) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
11 +
12 + const body = await request.json();
13 + const data: Record<string, unknown> = {};
14 + if (body.name !== undefined) data.name = body.name;
15 + if (body.email !== undefined) data.email = body.email;
16 + if (body.role !== undefined) data.role = body.role;
17 + if (body.password) data.passwordHash = await hashPassword(body.password);
18 +
19 + const user = await prisma.user.update({
20 + where: { id: params.id },
21 + data,
22 + select: { id: true, email: true, name: true, role: true },
23 + });
24 + return NextResponse.json(user);
25 +}
26 +
27 +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
28 + const response = NextResponse.next();
29 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
30 + if (!session.userId || !(await requireAdmin(session.userId))) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
31 + if (params.id === session.userId) return NextResponse.json({ error: "Cannot delete yourself" }, { status: 400 });
32 +
33 + await prisma.user.delete({ where: { id: params.id } });
34 + return NextResponse.json({ success: true });
35 +}
added app/api/users/route.ts +36 −0
@@ -0,0 +1,36 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "@/lib/session";
4 +import { prisma } from "@/lib/prisma";
5 +import { hashPassword, requireAdmin } from "@/lib/auth";
6 +
7 +export async function GET(request: NextRequest) {
8 + const response = NextResponse.next();
9 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
10 + if (!session.userId || !(await requireAdmin(session.userId))) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
11 +
12 + const users = await prisma.user.findMany({
13 + select: { id: true, email: true, name: true, role: true, twoFactorEnabled: true, createdAt: true, _count: { select: { files: true, folders: true } } },
14 + orderBy: { createdAt: "asc" },
15 + });
16 + return NextResponse.json(users);
17 +}
18 +
19 +export async function POST(request: NextRequest) {
20 + const response = NextResponse.next();
21 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
22 + if (!session.userId || !(await requireAdmin(session.userId))) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
23 +
24 + const body = await request.json();
25 + const { email, name, password, role } = body;
26 + if (!email || !password) return NextResponse.json({ error: "Email et mot de passe requis" }, { status: 400 });
27 +
28 + const existing = await prisma.user.findUnique({ where: { email } });
29 + if (existing) return NextResponse.json({ error: "Email d\u00e9j\u00e0 utilis\u00e9" }, { status: 409 });
30 +
31 + const user = await prisma.user.create({
32 + data: { email, name: name || null, passwordHash: await hashPassword(password), role: role || "user" },
33 + select: { id: true, email: true, name: true, role: true, createdAt: true },
34 + });
35 + return NextResponse.json(user, { status: 201 });
36 +}
added app/api/v1/files/route.ts +35 −0
@@ -0,0 +1,35 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { prisma } from "@/lib/prisma";
3 +import { createHash } from "crypto";
4 +
5 +async function validateApiKey(request: NextRequest) {
6 + const auth = request.headers.get("authorization");
7 + if (!auth?.startsWith("Bearer ")) return null;
8 + const rawKey = auth.substring(7);
9 + const keyHash = createHash("sha256").update(rawKey).digest("hex");
10 + const apiKey = await prisma.apiKey.findUnique({ where: { keyHash } });
11 + if (!apiKey) return null;
12 + if (apiKey.expiresAt && apiKey.expiresAt < new Date()) return null;
13 + await prisma.apiKey.update({ where: { id: apiKey.id }, data: { lastUsedAt: new Date() } });
14 + return apiKey;
15 +}
16 +
17 +export async function GET(request: NextRequest) {
18 + const apiKey = await validateApiKey(request);
19 + if (!apiKey) return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
20 +
21 + const { searchParams } = new URL(request.url);
22 + const folderId = searchParams.get("folderId");
23 + const where: Record<string, unknown> = {};
24 + if (folderId && folderId !== "root") where.folderId = folderId;
25 + else where.folderId = null;
26 +
27 + const files = await prisma.file.findMany({
28 + where,
29 + select: { id: true, name: true, size: true, mimeType: true, createdAt: true, folderId: true },
30 + orderBy: { createdAt: "desc" },
31 + take: 100,
32 + });
33 +
34 + return NextResponse.json({ files: files.map((f) => ({ ...f, size: Number(f.size) })) });
35 +}
added app/dashboard/favorites/page.tsx +80 −0
@@ -0,0 +1,80 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useCallback } from "react";
4 +import { Star } from "lucide-react";
5 +import AppShell from "@/components/layout/AppShell";
6 +import FileCard from "@/components/files/FileCard";
7 +import PreviewEngine from "@/components/preview/PreviewEngine";
8 +import type { FileData } from "@/types/files";
9 +
10 +export default function FavoritesPage() {
11 + const [files, setFiles] = useState<FileData[]>([]);
12 + const [previewFile, setPreviewFile] = useState<FileData | null>(null);
13 + const [loading, setLoading] = useState(true);
14 +
15 + const fetchFavorites = useCallback(async () => {
16 + setLoading(true);
17 + try {
18 + const res = await fetch("/api/files?favorite=true");
19 + if (res.ok) setFiles(await res.json());
20 + } catch {}
21 + setLoading(false);
22 + }, []);
23 +
24 + useEffect(() => {
25 + fetchFavorites();
26 + }, [fetchFavorites]);
27 +
28 + return (
29 + <AppShell>
30 + <div className="flex flex-col h-full lg:flex-row">
31 + <div className="flex-1 flex flex-col min-w-0">
32 + <div className="px-3 sm:px-4 lg:px-6 py-3 sm:py-4 border-b border-gray-200 dark:border-white/10">
33 + <h1 className="text-lg font-medium text-gray-900 dark:text-white flex items-center gap-2">
34 + <Star className="w-5 h-5 text-yellow-400" />
35 + Favoris
36 + </h1>
37 + <p className="text-xs text-gray-500 mt-1">{files.length} fichier{files.length !== 1 ? "s" : ""}</p>
38 + </div>
39 +
40 + <div className="flex-1 overflow-auto px-3 sm:px-4 lg:px-6 py-3 sm:py-4">
41 + {loading ? (
42 + <div className="flex items-center justify-center h-32">
43 + <div className="w-6 h-6 border-2 border-gray-300 dark:border-white/20 border-t-gray-600 dark:border-t-white rounded-full animate-spin" />
44 + </div>
45 + ) : files.length === 0 ? (
46 + <div className="flex flex-col items-center justify-center h-64 text-gray-500">
47 + <Star className="w-12 h-12 mb-4 text-gray-300 dark:text-gray-600" />
48 + <p className="text-lg">Aucun favori</p>
49 + <p className="text-sm mt-1 text-center px-4">Ajoutez des fichiers aux favoris pour les retrouver ici</p>
50 + </div>
51 + ) : (
52 + <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2 sm:gap-3">
53 + {files.map((file) => (
54 + <FileCard
55 + key={file.id}
56 + file={file}
57 + onPreview={setPreviewFile}
58 + onRefresh={fetchFavorites}
59 + viewMode="grid"
60 + />
61 + ))}
62 + </div>
63 + )}
64 + </div>
65 + </div>
66 +
67 + {previewFile && (
68 + <div className="fixed inset-0 z-50 lg:relative lg:inset-auto lg:w-[400px] lg:border-l lg:border-gray-200 dark:lg:border-white/10">
69 + <PreviewEngine
70 + file={previewFile}
71 + files={files}
72 + onClose={() => setPreviewFile(null)}
73 + onNavigate={setPreviewFile}
74 + />
75 + </div>
76 + )}
77 + </div>
78 + </AppShell>
79 + );
80 +}
added app/dashboard/folder/[id]/page.tsx +49 −0
@@ -0,0 +1,49 @@
1 +"use client";
2 +
3 +import { useEffect, useState } from "react";
4 +import { useParams } from "next/navigation";
5 +import AppShell from "@/components/layout/AppShell";
6 +import FileExplorer from "@/components/files/FileExplorer";
7 +import Breadcrumb from "@/components/layout/Breadcrumb";
8 +
9 +interface BreadcrumbItem {
10 + id: string;
11 + name: string;
12 +}
13 +
14 +export default function FolderPage() {
15 + const params = useParams();
16 + const folderId = params.id as string;
17 + const [breadcrumbs, setBreadcrumbs] = useState<BreadcrumbItem[]>([]);
18 +
19 + useEffect(() => {
20 + async function fetchBreadcrumbs() {
21 + const res = await fetch(`/api/folders/${folderId}`);
22 + if (res.ok) {
23 + const data = await res.json();
24 + const crumbs: BreadcrumbItem[] = [];
25 +
26 + // Build breadcrumb path
27 + let current = data;
28 + const visited = new Set<string>();
29 + while (current) {
30 + if (visited.has(current.id)) break;
31 + visited.add(current.id);
32 + crumbs.unshift({ id: current.id, name: current.name });
33 + current = current.parent;
34 + }
35 + setBreadcrumbs(crumbs);
36 + }
37 + }
38 + fetchBreadcrumbs();
39 + }, [folderId]);
40 +
41 + return (
42 + <AppShell>
43 + <div className="px-4 lg:px-6 pt-4">
44 + <Breadcrumb items={breadcrumbs} />
45 + </div>
46 + <FileExplorer folderId={folderId} />
47 + </AppShell>
48 + );
49 +}
added app/dashboard/page.tsx +12 −0
@@ -0,0 +1,12 @@
1 +"use client";
2 +
3 +import AppShell from "@/components/layout/AppShell";
4 +import FileExplorer from "@/components/files/FileExplorer";
5 +
6 +export default function DashboardPage() {
7 + return (
8 + <AppShell>
9 + <FileExplorer />
10 + </AppShell>
11 + );
12 +}
added app/fonts/GeistMonoVF.woff +0 −0

Binary file not shown.

added app/fonts/GeistVF.woff +0 −0

Binary file not shown.

added app/globals.css +151 −0
@@ -0,0 +1,151 @@
1 +@tailwind base;
2 +@tailwind components;
3 +@tailwind utilities;
4 +
5 +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
6 +
7 +/* ===== LIGHT THEME (default) ===== */
8 +:root {
9 + --background: #ffffff;
10 + --foreground: #111827;
11 + --bg-sidebar: #f9fafb;
12 + --bg-card: #f3f4f6;
13 + --bg-card-hover: #e5e7eb;
14 + --bg-input: #f3f4f6;
15 + --bg-overlay: rgba(0, 0, 0, 0.4);
16 + --border-primary: #e5e7eb;
17 + --border-secondary: #d1d5db;
18 + --text-primary: #111827;
19 + --text-secondary: #374151;
20 + --text-muted: #6b7280;
21 + --text-dimmed: #9ca3af;
22 + --scrollbar-track: #f3f4f6;
23 + --scrollbar-thumb: #d1d5db;
24 + --scrollbar-thumb-hover: #9ca3af;
25 + --toast-bg: #ffffff;
26 + --toast-color: #111827;
27 + --toast-border: rgba(0, 0, 0, 0.1);
28 +}
29 +
30 +/* ===== DARK THEME ===== */
31 +.dark {
32 + --background: #0a0a0a;
33 + --foreground: #ededed;
34 + --bg-sidebar: #0a0a0a;
35 + --bg-card: rgba(255, 255, 255, 0.05);
36 + --bg-card-hover: rgba(255, 255, 255, 0.1);
37 + --bg-input: rgba(255, 255, 255, 0.05);
38 + --bg-overlay: rgba(0, 0, 0, 0.6);
39 + --border-primary: rgba(255, 255, 255, 0.1);
40 + --border-secondary: rgba(255, 255, 255, 0.2);
41 + --text-primary: #ffffff;
42 + --text-secondary: #e5e7eb;
43 + --text-muted: #9ca3af;
44 + --text-dimmed: #6b7280;
45 + --scrollbar-track: #111827;
46 + --scrollbar-thumb: #374151;
47 + --scrollbar-thumb-hover: #4b5563;
48 + --toast-bg: #1f2937;
49 + --toast-color: #e5e7eb;
50 + --toast-border: rgba(255, 255, 255, 0.1);
51 +}
52 +
53 +body {
54 + color: var(--foreground);
55 + background: var(--background);
56 + font-family: "Inter", system-ui, sans-serif;
57 + transition: background-color 0.3s ease, color 0.3s ease;
58 + -webkit-tap-highlight-color: transparent;
59 + -webkit-text-size-adjust: 100%;
60 +}
61 +
62 +/* ===== MOBILE SAFE AREAS ===== */
63 +.safe-top {
64 + padding-top: env(safe-area-inset-top, 0);
65 +}
66 +
67 +.pb-safe {
68 + padding-bottom: env(safe-area-inset-bottom, 0);
69 +}
70 +
71 +/* ===== TOUCH OPTIMIZATION ===== */
72 +@layer utilities {
73 + .touch-target {
74 + min-width: 44px;
75 + min-height: 44px;
76 + }
77 +}
78 +
79 +/* Prevent text selection on interactive elements on mobile */
80 +button, a, [role="button"] {
81 + -webkit-touch-callout: none;
82 + user-select: none;
83 +}
84 +
85 +/* Active state feedback for mobile */
86 +button:active, a:active, [role="button"]:active {
87 + opacity: 0.85;
88 +}
89 +
90 +/* Custom scrollbar */
91 +::-webkit-scrollbar {
92 + width: 6px;
93 + height: 6px;
94 +}
95 +
96 +@media (max-width: 768px) {
97 + ::-webkit-scrollbar {
98 + width: 3px;
99 + height: 3px;
100 + }
101 +}
102 +
103 +::-webkit-scrollbar-track {
104 + background: var(--scrollbar-track);
105 +}
106 +
107 +::-webkit-scrollbar-thumb {
108 + background: var(--scrollbar-thumb);
109 + border-radius: 3px;
110 +}
111 +
112 +::-webkit-scrollbar-thumb:hover {
113 + background: var(--scrollbar-thumb-hover);
114 +}
115 +
116 +/* File drop zone animations */
117 +.drop-active {
118 + border-color: #3b82f6 !important;
119 + background-color: rgba(59, 130, 246, 0.05) !important;
120 +}
121 +
122 +/* Mobile modal - full viewport */
123 +@media (max-width: 640px) {
124 + .mobile-full-modal {
125 + width: 100vw !important;
126 + max-width: 100vw !important;
127 + min-height: 50vh;
128 + border-radius: 1rem 1rem 0 0 !important;
129 + margin: 0;
130 + position: fixed;
131 + bottom: 0;
132 + left: 0;
133 + right: 0;
134 + }
135 +}
136 +
137 +/* Smooth overscroll behavior */
138 +html {
139 + overscroll-behavior: none;
140 +}
141 +
142 +/* Better select inputs on mobile */
143 +select {
144 + -webkit-appearance: none;
145 + appearance: none;
146 + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
147 + background-position: right 0.5rem center;
148 + background-repeat: no-repeat;
149 + background-size: 1.5em 1.5em;
150 + padding-right: 2.5rem;
151 +}
added app/layout.tsx +43 −0
@@ -0,0 +1,43 @@
1 +import type { Metadata } from "next";
2 +import "./globals.css";
3 +import ThemeProvider from "@/components/theme/ThemeProvider";
4 +import { DynamicToaster } from "@/components/theme/DynamicToaster";
5 +
6 +export const metadata: Metadata = {
7 + title: "SPB Cloud",
8 + description: "Cloud personnel privé",
9 +};
10 +
11 +export default function RootLayout({
12 + children,
13 +}: Readonly<{
14 + children: React.ReactNode;
15 +}>) {
16 + return (
17 + <html lang="fr" className="dark" suppressHydrationWarning>
18 + <head>
19 + <script
20 + dangerouslySetInnerHTML={{
21 + __html: `
22 + (function() {
23 + try {
24 + var theme = localStorage.getItem('spb-theme');
25 + if (theme === 'light') {
26 + document.documentElement.classList.remove('dark');
27 + document.documentElement.classList.add('light');
28 + }
29 + } catch(e) {}
30 + })();
31 + `,
32 + }}
33 + />
34 + </head>
35 + <body className="antialiased">
36 + <ThemeProvider>
37 + {children}
38 + <DynamicToaster />
39 + </ThemeProvider>
40 + </body>
41 + </html>
42 + );
43 +}
added app/login/page.tsx +199 −0
@@ -0,0 +1,199 @@
1 +"use client";
2 +
3 +import { useState } from "react";
4 +import { useRouter } from "next/navigation";
5 +import { motion } from "framer-motion";
6 +import { Cloud, Lock, Eye, EyeOff, Loader2, Mail, Shield } from "lucide-react";
7 +import ThemeToggle from "@/components/theme/ThemeToggle";
8 +
9 +export default function LoginPage() {
10 + const [email, setEmail] = useState("");
11 + const [password, setPassword] = useState("");
12 + const [totpCode, setTotpCode] = useState("");
13 + const [error, setError] = useState("");
14 + const [loading, setLoading] = useState(false);
15 + const [showPassword, setShowPassword] = useState(false);
16 + const [needs2FA, setNeeds2FA] = useState(false);
17 + const router = useRouter();
18 +
19 + const inputClasses = "w-full bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl px-10 py-3.5 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:border-gray-400 dark:focus:border-white/30 transition-colors shadow-sm dark:shadow-none";
20 +
21 + const handleSubmit = async (e: React.FormEvent) => {
22 + e.preventDefault();
23 + setError("");
24 + setLoading(true);
25 +
26 + try {
27 + const res = await fetch("/api/auth/login", {
28 + method: "POST",
29 + headers: { "Content-Type": "application/json" },
30 + body: JSON.stringify({ password, email: email || undefined }),
31 + });
32 +
33 + const data = await res.json();
34 +
35 + if (res.ok) {
36 + if (data.requires2FA) {
37 + setNeeds2FA(true);
38 + } else {
39 + router.push("/dashboard");
40 + }
41 + } else {
42 + setError(data.error || "Erreur de connexion");
43 + }
44 + } catch {
45 + setError("Erreur réseau");
46 + } finally {
47 + setLoading(false);
48 + }
49 + };
50 +
51 + const handleVerify2FA = async (e: React.FormEvent) => {
52 + e.preventDefault();
53 + setError("");
54 + setLoading(true);
55 +
56 + try {
57 + const res = await fetch("/api/auth/2fa/verify", {
58 + method: "POST",
59 + headers: { "Content-Type": "application/json" },
60 + body: JSON.stringify({ token: totpCode }),
61 + });
62 +
63 + if (res.ok) {
64 + router.push("/dashboard");
65 + } else {
66 + const data = await res.json();
67 + setError(data.error || "Code invalide");
68 + }
69 + } catch {
70 + setError("Erreur réseau");
71 + } finally {
72 + setLoading(false);
73 + }
74 + };
75 +
76 + return (
77 + <div className="min-h-screen bg-gray-50 dark:bg-[#0a0a0a] flex items-center justify-center px-4 py-8 transition-colors">
78 + <div className="fixed top-3 right-3 sm:top-4 sm:right-4 z-10">
79 + <ThemeToggle />
80 + </div>
81 +
82 + <motion.div
83 + initial={{ opacity: 0, y: 20 }}
84 + animate={{ opacity: 1, y: 0 }}
85 + transition={{ duration: 0.5 }}
86 + className="w-full max-w-sm px-4 sm:px-0"
87 + >
88 + <div className="text-center mb-8">
89 + <motion.div
90 + initial={{ scale: 0 }}
91 + animate={{ scale: 1 }}
92 + transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
93 + className="inline-flex items-center justify-center w-14 h-14 sm:w-16 sm:h-16 rounded-2xl bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 mb-4"
94 + >
95 + <Cloud className="w-7 h-7 sm:w-8 sm:h-8 text-gray-700 dark:text-white" />
96 + </motion.div>
97 + <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">SPB Cloud</h1>
98 + <p className="text-gray-500 mt-1 text-sm">
99 + {needs2FA ? "Vérification 2FA" : "Cloud personnel privé"}
100 + </p>
101 + </div>
102 +
103 + {!needs2FA ? (
104 + <form onSubmit={handleSubmit} className="space-y-4">
105 + <div className="relative">
106 + <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500" />
107 + <input
108 + type="email"
109 + value={email}
110 + onChange={(e) => setEmail(e.target.value)}
111 + placeholder="Email (optionnel)"
112 + className={inputClasses}
113 + />
114 + </div>
115 +
116 + <div className="relative">
117 + <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500" />
118 + <input
119 + type={showPassword ? "text" : "password"}
120 + value={password}
121 + onChange={(e) => setPassword(e.target.value)}
122 + placeholder="Mot de passe"
123 + className={inputClasses}
124 + autoFocus
125 + />
126 + <button
127 + type="button"
128 + onClick={() => setShowPassword(!showPassword)}
129 + className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
130 + >
131 + {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
132 + </button>
133 + </div>
134 +
135 + {error && (
136 + <motion.p initial={{ opacity: 0, y: -5 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 dark:text-red-400 text-sm text-center">
137 + {error}
138 + </motion.p>
139 + )}
140 +
141 + <motion.button
142 + whileTap={{ scale: 0.98 }}
143 + type="submit"
144 + disabled={loading || !password}
145 + className="w-full bg-gray-900 dark:bg-white text-white dark:text-black font-medium py-3.5 rounded-xl hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
146 + >
147 + {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Connexion"}
148 + </motion.button>
149 + </form>
150 + ) : (
151 + <form onSubmit={handleVerify2FA} className="space-y-4">
152 + <div className="flex justify-center mb-4">
153 + <Shield className="w-12 h-12 text-purple-500" />
154 + </div>
155 + <p className="text-sm text-gray-500 dark:text-gray-400 text-center">
156 + Entrez le code à 6 chiffres de votre app authenticator
157 + </p>
158 + <input
159 + type="text"
160 + value={totpCode}
161 + onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
162 + placeholder="000000"
163 + maxLength={6}
164 + className="w-full bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl px-4 py-3.5 text-gray-900 dark:text-white text-center text-2xl tracking-[0.3em] sm:tracking-[0.5em] font-mono focus:outline-none focus:border-gray-400 dark:focus:border-white/30 shadow-sm dark:shadow-none"
165 + autoFocus
166 + />
167 +
168 + {error && (
169 + <motion.p initial={{ opacity: 0, y: -5 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 dark:text-red-400 text-sm text-center">
170 + {error}
171 + </motion.p>
172 + )}
173 +
174 + <motion.button
175 + whileTap={{ scale: 0.98 }}
176 + type="submit"
177 + disabled={loading || totpCode.length !== 6}
178 + className="w-full bg-gray-900 dark:bg-white text-white dark:text-black font-medium py-3.5 rounded-xl hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
179 + >
180 + {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Vérifier"}
181 + </motion.button>
182 +
183 + <button
184 + type="button"
185 + onClick={() => { setNeeds2FA(false); setTotpCode(""); setError(""); }}
186 + className="w-full text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 py-2"
187 + >
188 + Retour
189 + </button>
190 + </form>
191 + )}
192 +
193 + <p className="text-center text-gray-400 dark:text-gray-600 text-xs mt-6">
194 + SPB Private Cloud v2.0
195 + </p>
196 + </motion.div>
197 + </div>
198 + );
199 +}
added app/page.tsx +5 −0
@@ -0,0 +1,5 @@
1 +import { redirect } from "next/navigation";
2 +
3 +export default function Home() {
4 + redirect("/dashboard");
5 +}
added app/shared/[token]/page.tsx +169 −0
@@ -0,0 +1,169 @@
1 +"use client";
2 +
3 +import { useEffect, useState } from "react";
4 +import { useParams } from "next/navigation";
5 +import { motion } from "framer-motion";
6 +import { Cloud, Download, Lock, AlertCircle, Loader2 } from "lucide-react";
7 +import Button from "@/components/ui/Button";
8 +
9 +export default function SharedPage() {
10 + const params = useParams();
11 + const token = params.token as string;
12 +
13 + const [state, setState] = useState<"loading" | "password" | "preview" | "error">("loading");
14 + const [error, setError] = useState("");
15 + const [fileInfo, setFileInfo] = useState<{
16 + fileName: string;
17 + fileType: string;
18 + mode: string;
19 + } | null>(null);
20 + const [password, setPassword] = useState("");
21 + const [passwordLoading, setPasswordLoading] = useState(false);
22 +
23 + useEffect(() => {
24 + fetch(`/api/shared/${token}`)
25 + .then(async (res) => {
26 + if (!res.ok) {
27 + const data = await res.json();
28 + setError(data.error || "Lien invalide");
29 + setState("error");
30 + return;
31 + }
32 +
33 + const contentType = res.headers.get("content-type") || "";
34 + if (contentType.includes("application/json")) {
35 + const data = await res.json();
36 + if (data.requiresPassword) {
37 + setFileInfo(data);
38 + setState("password");
39 + }
40 + } else {
41 + // Direct file response — redirect to download/preview
42 + setState("preview");
43 + }
44 + })
45 + .catch(() => {
46 + setError("Erreur réseau");
47 + setState("error");
48 + });
49 + }, [token]);
50 +
51 + const handlePasswordSubmit = async (e: React.FormEvent) => {
52 + e.preventDefault();
53 + setPasswordLoading(true);
54 + try {
55 + const res = await fetch(`/api/shared/${token}/verify`, {
56 + method: "POST",
57 + headers: { "Content-Type": "application/json" },
58 + body: JSON.stringify({ password }),
59 + });
60 +
61 + if (!res.ok) {
62 + const data = await res.json();
63 + setError(data.error || "Mot de passe incorrect");
64 + setPasswordLoading(false);
65 + return;
66 + }
67 +
68 + // Download the file
69 + const blob = await res.blob();
70 + const url = URL.createObjectURL(blob);
71 + const a = document.createElement("a");
72 + a.href = url;
73 + a.download = fileInfo?.fileName || "file";
74 + a.click();
75 + URL.revokeObjectURL(url);
76 + setPasswordLoading(false);
77 + } catch {
78 + setError("Erreur réseau");
79 + setPasswordLoading(false);
80 + }
81 + };
82 +
83 + if (state === "loading") {
84 + return (
85 + <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
86 + <Loader2 className="w-8 h-8 text-white animate-spin" />
87 + </div>
88 + );
89 + }
90 +
91 + if (state === "error") {
92 + return (
93 + <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center p-4">
94 + <motion.div
95 + initial={{ opacity: 0, y: 20 }}
96 + animate={{ opacity: 1, y: 0 }}
97 + className="text-center"
98 + >
99 + <AlertCircle className="w-16 h-16 text-red-400 mx-auto mb-4" />
100 + <h1 className="text-xl font-semibold text-white mb-2">Lien invalide</h1>
101 + <p className="text-gray-400">{error}</p>
102 + </motion.div>
103 + </div>
104 + );
105 + }
106 +
107 + if (state === "password") {
108 + return (
109 + <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center p-4">
110 + <motion.div
111 + initial={{ opacity: 0, y: 20 }}
112 + animate={{ opacity: 1, y: 0 }}
113 + className="w-full max-w-sm"
114 + >
115 + <div className="text-center mb-6">
116 + <Lock className="w-12 h-12 text-gray-400 mx-auto mb-3" />
117 + <h1 className="text-xl font-semibold text-white">
118 + Fichier protégé
119 + </h1>
120 + <p className="text-sm text-gray-500 mt-1">{fileInfo?.fileName}</p>
121 + </div>
122 +
123 + <form onSubmit={handlePasswordSubmit} className="space-y-4">
124 + <input
125 + type="password"
126 + value={password}
127 + onChange={(e) => setPassword(e.target.value)}
128 + placeholder="Mot de passe"
129 + className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-white/20"
130 + autoFocus
131 + />
132 + {error && <p className="text-red-400 text-sm text-center">{error}</p>}
133 + <Button type="submit" className="w-full" loading={passwordLoading}>
134 + Accéder
135 + </Button>
136 + </form>
137 + </motion.div>
138 + </div>
139 + );
140 + }
141 +
142 + // Preview state — show the file
143 + return (
144 + <div className="min-h-screen bg-[#0a0a0a] flex flex-col">
145 + <header className="flex items-center justify-between px-4 py-3 border-b border-white/10">
146 + <div className="flex items-center gap-2">
147 + <Cloud className="w-5 h-5 text-white" />
148 + <span className="text-sm text-white font-medium">SPB Cloud</span>
149 + </div>
150 + <Button
151 + size="sm"
152 + onClick={() =>
153 + window.open(`/api/shared/${token}?action=download`, "_blank")
154 + }
155 + >
156 + <Download className="w-4 h-4" />
157 + Télécharger
158 + </Button>
159 + </header>
160 + <main className="flex-1 flex items-center justify-center p-4">
161 + <iframe
162 + src={`/api/shared/${token}`}
163 + className="w-full h-full min-h-[80vh] border-0 rounded-lg"
164 + title="Shared file"
165 + />
166 + </main>
167 + </div>
168 + );
169 +}
added components/admin/ActivityLog.tsx +99 −0
@@ -0,0 +1,99 @@
1 +"use client";
2 +
3 +import { motion } from "framer-motion";
4 +import { formatDate } from "@/lib/utils";
5 +import {
6 + Upload,
7 + Download,
8 + Trash2,
9 + Pencil,
10 + FolderInput,
11 + Share2,
12 + LogIn,
13 + LogOut,
14 + FolderPlus,
15 + FolderMinus,
16 + Link2Off,
17 +} from "lucide-react";
18 +
19 +interface Activity {
20 + id: string;
21 + action: string;
22 + details: string | null;
23 + createdAt: string;
24 + file: { name: string } | null;
25 +}
26 +
27 +interface ActivityLogProps {
28 + activities: Activity[];
29 +}
30 +
31 +const actionIcons: Record<string, React.ComponentType<{ className?: string }>> = {
32 + UPLOAD: Upload,
33 + DOWNLOAD: Download,
34 + DELETE: Trash2,
35 + RENAME: Pencil,
36 + MOVE: FolderInput,
37 + SHARE: Share2,
38 + UNSHARE: Link2Off,
39 + LOGIN: LogIn,
40 + LOGOUT: LogOut,
41 + CREATE_FOLDER: FolderPlus,
42 + DELETE_FOLDER: FolderMinus,
43 +};
44 +
45 +const actionLabels: Record<string, string> = {
46 + UPLOAD: "Upload",
47 + DOWNLOAD: "Téléchargement",
48 + DELETE: "Suppression",
49 + RENAME: "Renommage",
50 + MOVE: "Déplacement",
51 + SHARE: "Partage",
52 + UNSHARE: "Partage retiré",
53 + LOGIN: "Connexion",
54 + LOGOUT: "Déconnexion",
55 + CREATE_FOLDER: "Dossier créé",
56 + DELETE_FOLDER: "Dossier supprimé",
57 +};
58 +
59 +export default function ActivityLog({ activities }: ActivityLogProps) {
60 + if (activities.length === 0) {
61 + return (
62 + <div className="text-center text-gray-500 py-8">
63 + Aucune activité récente
64 + </div>
65 + );
66 + }
67 +
68 + return (
69 + <div className="space-y-1">
70 + {activities.map((activity, i) => {
71 + const Icon = actionIcons[activity.action] || Upload;
72 + return (
73 + <motion.div
74 + key={activity.id}
75 + initial={{ opacity: 0, x: -10 }}
76 + animate={{ opacity: 1, x: 0 }}
77 + transition={{ delay: i * 0.03 }}
78 + className="flex items-center gap-3 px-3 py-3.5 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg transition-colors"
79 + >
80 + <Icon className="w-4 h-4 text-gray-400 dark:text-gray-500 flex-shrink-0" />
81 + <div className="flex-1 min-w-0">
82 + <p className="text-sm text-gray-600 dark:text-gray-300">
83 + <span className="text-gray-500 dark:text-gray-400">
84 + {actionLabels[activity.action] || activity.action}
85 + </span>
86 + {activity.details && (
87 + <span className="text-gray-400 dark:text-gray-500 ml-1">— {activity.details}</span>
88 + )}
89 + </p>
90 + </div>
91 + <span className="text-xs text-gray-400 dark:text-gray-600 flex-shrink-0 hidden sm:block">
92 + {formatDate(activity.createdAt)}
93 + </span>
94 + </motion.div>
95 + );
96 + })}
97 + </div>
98 + );
99 +}
added components/admin/ApiKeyManager.tsx +158 −0
@@ -0,0 +1,158 @@
1 +"use client";
2 +
3 +import { useState, useEffect } from "react";
4 +import { motion } from "framer-motion";
5 +import { Key, Trash2, Copy, Plus } from "lucide-react";
6 +import Button from "../ui/Button";
7 +import Modal from "../ui/Modal";
8 +import { formatDate } from "@/lib/utils";
9 +import toast from "react-hot-toast";
10 +
11 +interface ApiKeyData {
12 + id: string;
13 + name: string;
14 + keyPrefix: string;
15 + permissions: string;
16 + lastUsedAt: string | null;
17 + expiresAt: string | null;
18 + createdAt: string;
19 +}
20 +
21 +export default function ApiKeyManager() {
22 + const [keys, setKeys] = useState<ApiKeyData[]>([]);
23 + const [loading, setLoading] = useState(true);
24 + const [showCreate, setShowCreate] = useState(false);
25 + const [newKey, setNewKey] = useState<string | null>(null);
26 + const [form, setForm] = useState({ name: "", permissions: "read" });
27 +
28 + const fetchKeys = async () => {
29 + const res = await fetch("/api/keys");
30 + if (res.ok) setKeys(await res.json());
31 + setLoading(false);
32 + };
33 +
34 + useEffect(() => { fetchKeys(); }, []);
35 +
36 + const createKey = async () => {
37 + const res = await fetch("/api/keys", {
38 + method: "POST",
39 + headers: { "Content-Type": "application/json" },
40 + body: JSON.stringify(form),
41 + });
42 + if (res.ok) {
43 + const data = await res.json();
44 + setNewKey(data.key);
45 + setForm({ name: "", permissions: "read" });
46 + fetchKeys();
47 + }
48 + };
49 +
50 + const deleteKey = async (id: string) => {
51 + if (!confirm("Supprimer cette clé API ?")) return;
52 + await fetch(`/api/keys/${id}`, { method: "DELETE" });
53 + toast.success("Clé supprimée");
54 + fetchKeys();
55 + };
56 +
57 + const copyKey = (key: string) => {
58 + navigator.clipboard.writeText(key);
59 + toast.success("Clé copiée");
60 + };
61 +
62 + const inputClasses = "w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-4 py-3 text-gray-900 dark:text-white text-sm focus:outline-none";
63 +
64 + return (
65 + <div className="space-y-4">
66 + <div className="flex items-center justify-between">
67 + <h2 className="text-lg font-medium text-gray-900 dark:text-white flex items-center gap-2">
68 + <Key className="w-5 h-5" /> Clés API
69 + </h2>
70 + <Button size="sm" onClick={() => setShowCreate(true)}>
71 + <Plus className="w-4 h-4" /> Nouvelle clé
72 + </Button>
73 + </div>
74 +
75 + <p className="text-xs text-gray-500 break-words">
76 + Utilisez les clés API pour accéder à l&apos;API REST : <code className="bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded inline-block overflow-x-auto max-w-full">GET /api/v1/files</code> avec header <code className="bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded inline-block overflow-x-auto max-w-full">Authorization: Bearer spb_...</code>
77 + </p>
78 +
79 + {loading ? (
80 + <div className="flex justify-center py-8">
81 + <div className="w-6 h-6 border-2 border-gray-300 dark:border-white/20 border-t-gray-600 dark:border-t-white rounded-full animate-spin" />
82 + </div>
83 + ) : keys.length === 0 ? (
84 + <div className="text-center text-gray-500 py-8">
85 + <Key className="w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600" />
86 + <p>Aucune clé API</p>
87 + </div>
88 + ) : (
89 + <div className="space-y-2">
90 + {keys.map((k, i) => (
91 + <motion.div
92 + key={k.id}
93 + initial={{ opacity: 0, y: 5 }}
94 + animate={{ opacity: 1, y: 0 }}
95 + transition={{ delay: i * 0.03 }}
96 + className="flex flex-col sm:flex-row sm:items-center gap-3 p-3 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl transition-colors"
97 + >
98 + <div className="flex items-start sm:items-center gap-3 flex-1 min-w-0">
99 + <Key className="w-4 h-4 text-gray-400 flex-shrink-0 mt-0.5 sm:mt-0" />
100 + <div className="flex-1 min-w-0">
101 + <p className="text-sm font-medium text-gray-900 dark:text-white">{k.name}</p>
102 + <p className="text-xs text-gray-500 break-all">
103 + <code>{k.keyPrefix}...</code> • {k.permissions}
104 + {k.lastUsedAt && ` • Dernier usage: ${formatDate(k.lastUsedAt)}`}
105 + </p>
106 + </div>
107 + </div>
108 + <button
109 + onClick={() => deleteKey(k.id)}
110 + className="min-w-[44px] min-h-[44px] flex items-center justify-center p-2.5 text-gray-400 hover:text-red-500 rounded-lg transition-colors self-end sm:self-auto"
111 + >
112 + <Trash2 className="w-4 h-4" />
113 + </button>
114 + </motion.div>
115 + ))}
116 + </div>
117 + )}
118 +
119 + {/* Create Modal */}
120 + <Modal isOpen={showCreate} onClose={() => { setShowCreate(false); setNewKey(null); }} title="Nouvelle clé API">
121 + {newKey ? (
122 + <div className="space-y-4">
123 + <p className="text-sm text-gray-600 dark:text-gray-400">
124 + Voici votre clé API. Copiez-la maintenant, elle ne sera plus affichée.
125 + </p>
126 + <div className="flex items-center gap-2">
127 + <code className="flex-1 bg-gray-100 dark:bg-white/10 px-3 py-2 rounded-lg text-xs font-mono text-gray-700 dark:text-gray-300 break-all">
128 + {newKey}
129 + </code>
130 + <button
131 + onClick={() => copyKey(newKey)}
132 + className="min-w-[44px] min-h-[44px] flex items-center justify-center p-2.5 text-gray-400 hover:text-gray-700 dark:hover:text-white rounded-lg transition-colors"
133 + >
134 + <Copy className="w-4 h-4" />
135 + </button>
136 + </div>
137 + <Button onClick={() => { setShowCreate(false); setNewKey(null); }} className="w-full">
138 + Fermer
139 + </Button>
140 + </div>
141 + ) : (
142 + <div className="space-y-3">
143 + <input type="text" placeholder="Nom de la clé" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputClasses} />
144 + <select value={form.permissions} onChange={(e) => setForm({ ...form, permissions: e.target.value })} className={inputClasses}>
145 + <option value="read">Lecture seule</option>
146 + <option value="write">Lecture + Écriture</option>
147 + <option value="admin">Admin</option>
148 + </select>
149 + <div className="flex gap-2 justify-end pt-2">
150 + <Button variant="ghost" onClick={() => setShowCreate(false)}>Annuler</Button>
151 + <Button onClick={createKey} disabled={!form.name}>Créer</Button>
152 + </div>
153 + </div>
154 + )}
155 + </Modal>
156 + </div>
157 + );
158 +}
added components/admin/BackupPanel.tsx +139 −0
@@ -0,0 +1,139 @@
1 +"use client";
2 +
3 +import { useState, useEffect } from "react";
4 +import { motion } from "framer-motion";
5 +import { HardDrive, Download, Trash2, RefreshCw, CheckCircle, XCircle, Loader2 } from "lucide-react";
6 +import Button from "../ui/Button";
7 +import { formatFileSize, formatDate } from "@/lib/utils";
8 +import toast from "react-hot-toast";
9 +
10 +interface BackupData {
11 + id: string;
12 + status: string;
13 + type: string;
14 + size: number;
15 + path: string | null;
16 + startedAt: string;
17 + completedAt: string | null;
18 +}
19 +
20 +export default function BackupPanel() {
21 + const [backups, setBackups] = useState<BackupData[]>([]);
22 + const [loading, setLoading] = useState(true);
23 + const [creating, setCreating] = useState(false);
24 +
25 + const fetchBackups = async () => {
26 + const res = await fetch("/api/backup");
27 + if (res.ok) setBackups(await res.json());
28 + setLoading(false);
29 + };
30 +
31 + useEffect(() => { fetchBackups(); }, []);
32 +
33 + const createBackup = async (type: "full" | "incremental") => {
34 + setCreating(true);
35 + try {
36 + const res = await fetch("/api/backup", {
37 + method: "POST",
38 + headers: { "Content-Type": "application/json" },
39 + body: JSON.stringify({ type }),
40 + });
41 + if (res.ok) {
42 + toast.success("Backup créé avec succès");
43 + fetchBackups();
44 + } else {
45 + toast.error("Échec du backup");
46 + }
47 + } catch {
48 + toast.error("Erreur");
49 + }
50 + setCreating(false);
51 + };
52 +
53 + const deleteBackup = async (id: string) => {
54 + if (!confirm("Supprimer ce backup ?")) return;
55 + const res = await fetch("/api/backup", {
56 + method: "DELETE",
57 + headers: { "Content-Type": "application/json" },
58 + body: JSON.stringify({ id }),
59 + });
60 + if (res.ok) {
61 + toast.success("Backup supprimé");
62 + fetchBackups();
63 + }
64 + };
65 +
66 + const statusIcon = (status: string) => {
67 + if (status === "completed") return <CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0" />;
68 + if (status === "failed") return <XCircle className="w-4 h-4 text-red-500 flex-shrink-0" />;
69 + return <Loader2 className="w-4 h-4 text-blue-500 animate-spin flex-shrink-0" />;
70 + };
71 +
72 + return (
73 + <div className="space-y-4">
74 + <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
75 + <h2 className="text-lg font-medium text-gray-900 dark:text-white flex items-center gap-2">
76 + <HardDrive className="w-5 h-5" /> Backups
77 + </h2>
78 + <div className="flex flex-col sm:flex-row gap-2">
79 + <Button size="sm" variant="secondary" onClick={() => createBackup("incremental")} loading={creating}>
80 + Incrémental
81 + </Button>
82 + <Button size="sm" onClick={() => createBackup("full")} loading={creating}>
83 + <Download className="w-4 h-4" /> Backup complet
84 + </Button>
85 + </div>
86 + </div>
87 +
88 + {loading ? (
89 + <div className="flex justify-center py-8">
90 + <div className="w-6 h-6 border-2 border-gray-300 dark:border-white/20 border-t-gray-600 dark:border-t-white rounded-full animate-spin" />
91 + </div>
92 + ) : backups.length === 0 ? (
93 + <div className="text-center text-gray-500 py-8">
94 + <HardDrive className="w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600" />
95 + <p>Aucun backup</p>
96 + </div>
97 + ) : (
98 + <div className="space-y-2">
99 + {backups.map((backup, i) => (
100 + <motion.div
101 + key={backup.id}
102 + initial={{ opacity: 0, y: 5 }}
103 + animate={{ opacity: 1, y: 0 }}
104 + transition={{ delay: i * 0.03 }}
105 + className="flex flex-col sm:flex-row sm:items-center gap-3 p-3 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl transition-colors"
106 + >
107 + <div className="flex items-start sm:items-center gap-3 flex-1 min-w-0">
108 + {statusIcon(backup.status)}
109 + <div className="flex-1 min-w-0">
110 + <p className="text-sm text-gray-900 dark:text-white break-words">
111 + Backup {backup.type} - {formatDate(backup.startedAt)}
112 + </p>
113 + <p className="text-xs text-gray-500 break-words">
114 + {backup.size > 0 ? formatFileSize(backup.size) : "—"}
115 + {backup.completedAt && ` • Terminé: ${formatDate(backup.completedAt)}`}
116 + </p>
117 + </div>
118 + </div>
119 + <div className="flex gap-1 pl-7 sm:pl-0">
120 + <button
121 + onClick={() => fetchBackups()}
122 + className="min-w-[44px] min-h-[44px] flex items-center justify-center p-2.5 text-gray-400 hover:text-gray-700 dark:hover:text-white rounded-lg transition-colors"
123 + >
124 + <RefreshCw className="w-4 h-4" />
125 + </button>
126 + <button
127 + onClick={() => deleteBackup(backup.id)}
128 + className="min-w-[44px] min-h-[44px] flex items-center justify-center p-2.5 text-gray-400 hover:text-red-500 rounded-lg transition-colors"
129 + >
130 + <Trash2 className="w-4 h-4" />
131 + </button>
132 + </div>
133 + </motion.div>
134 + ))}
135 + </div>
136 + )}
137 + </div>
138 + );
139 +}
added components/admin/StatsCards.tsx +79 −0
@@ -0,0 +1,79 @@
1 +"use client";
2 +
3 +import { motion } from "framer-motion";
4 +import { HardDrive, FileText, Share2, Folder } from "lucide-react";
5 +import { formatFileSize } from "@/lib/utils";
6 +
7 +interface StatsData {
8 + fileCount: number;
9 + folderCount: number;
10 + shareCount: number;
11 + storageUsed: number;
12 +}
13 +
14 +interface StatsCardsProps {
15 + stats: StatsData;
16 +}
17 +
18 +const cards = [
19 + {
20 + key: "storageUsed",
21 + label: "Stockage utilisé",
22 + icon: HardDrive,
23 + format: (v: number) => formatFileSize(v),
24 + color: "text-blue-500 dark:text-blue-400",
25 + bgColor: "bg-blue-100 dark:bg-blue-500/10",
26 + },
27 + {
28 + key: "fileCount",
29 + label: "Fichiers",
30 + icon: FileText,
31 + format: (v: number) => String(v),
32 + color: "text-green-500 dark:text-green-400",
33 + bgColor: "bg-green-100 dark:bg-green-500/10",
34 + },
35 + {
36 + key: "folderCount",
37 + label: "Dossiers",
38 + icon: Folder,
39 + format: (v: number) => String(v),
40 + color: "text-purple-500 dark:text-purple-400",
41 + bgColor: "bg-purple-100 dark:bg-purple-500/10",
42 + },
43 + {
44 + key: "shareCount",
45 + label: "Liens actifs",
46 + icon: Share2,
47 + format: (v: number) => String(v),
48 + color: "text-orange-500 dark:text-orange-400",
49 + bgColor: "bg-orange-100 dark:bg-orange-500/10",
50 + },
51 +];
52 +
53 +export default function StatsCards({ stats }: StatsCardsProps) {
54 + return (
55 + <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
56 + {cards.map((card, i) => (
57 + <motion.div
58 + key={card.key}
59 + initial={{ opacity: 0, y: 10 }}
60 + animate={{ opacity: 1, y: 0 }}
61 + transition={{ delay: i * 0.1 }}
62 + className="bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl p-3.5 sm:p-4 shadow-sm dark:shadow-none transition-colors"
63 + >
64 + <div className="flex items-center gap-3">
65 + <div className={`p-2 sm:p-2.5 rounded-lg ${card.bgColor}`}>
66 + <card.icon className={`w-4 h-4 sm:w-5 sm:h-5 ${card.color}`} />
67 + </div>
68 + <div>
69 + <p className="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-white">
70 + {card.format(stats[card.key as keyof StatsData] as number)}
71 + </p>
72 + <p className="text-[11px] sm:text-xs text-gray-500">{card.label}</p>
73 + </div>
74 + </div>
75 + </motion.div>
76 + ))}
77 + </div>
78 + );
79 +}
added components/admin/StorageChart.tsx +97 −0
@@ -0,0 +1,97 @@
1 +"use client";
2 +
3 +import { motion } from "framer-motion";
4 +import { formatFileSize } from "@/lib/utils";
5 +
6 +interface TypeBreakdown {
7 + type: string;
8 + count: number;
9 + size: number;
10 +}
11 +
12 +interface StorageChartProps {
13 + data: TypeBreakdown[];
14 + totalSize: number;
15 +}
16 +
17 +const typeColors: Record<string, string> = {
18 + "image/": "bg-blue-500",
19 + "video/": "bg-purple-500",
20 + "audio/": "bg-pink-500",
21 + "application/pdf": "bg-red-500",
22 + "text/": "bg-green-500",
23 + "application/": "bg-yellow-500",
24 +};
25 +
26 +function getColor(type: string): string {
27 + for (const [prefix, color] of Object.entries(typeColors)) {
28 + if (type.startsWith(prefix)) return color;
29 + }
30 + return "bg-gray-500";
31 +}
32 +
33 +function getTypeLabel(type: string): string {
34 + if (type.startsWith("image/")) return "Images";
35 + if (type.startsWith("video/")) return "Vidéos";
36 + if (type.startsWith("audio/")) return "Audio";
37 + if (type === "application/pdf") return "PDF";
38 + if (type.startsWith("text/")) return "Texte";
39 + return type.split("/")[1]?.toUpperCase() || "Autre";
40 +}
41 +
42 +export default function StorageChart({ data, totalSize }: StorageChartProps) {
43 + // Group by category
44 + const grouped = data.reduce<Record<string, { size: number; count: number }>>(
45 + (acc, item) => {
46 + const label = getTypeLabel(item.type);
47 + if (!acc[label]) acc[label] = { size: 0, count: 0 };
48 + acc[label].size += item.size;
49 + acc[label].count += item.count;
50 + return acc;
51 + },
52 + {}
53 + );
54 +
55 + const entries = Object.entries(grouped).sort(([, a], [, b]) => b.size - a.size);
56 +
57 + return (
58 + <div className="space-y-4">
59 + {/* Bar */}
60 + <div className="h-4 rounded-full bg-gray-200 dark:bg-white/5 overflow-hidden flex">
61 + {entries.map(([label, { size }]) => {
62 + const pct = totalSize > 0 ? (size / totalSize) * 100 : 0;
63 + if (pct < 1) return null;
64 + const matchType = data.find((d) => getTypeLabel(d.type) === label)?.type || "";
65 + return (
66 + <motion.div
67 + key={label}
68 + initial={{ width: 0 }}
69 + animate={{ width: `${pct}%` }}
70 + transition={{ duration: 0.5, delay: 0.2 }}
71 + className={`h-full ${getColor(matchType)}`}
72 + title={`${label}: ${formatFileSize(size)}`}
73 + />
74 + );
75 + })}
76 + </div>
77 +
78 + {/* Legend */}
79 + <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
80 + {entries.map(([label, { size, count }]) => {
81 + const matchType = data.find((d) => getTypeLabel(d.type) === label)?.type || "";
82 + return (
83 + <div key={label} className="flex items-center gap-2">
84 + <div className={`w-3 h-3 rounded-full ${getColor(matchType)}`} />
85 + <div>
86 + <p className="text-xs text-gray-600 dark:text-gray-300">{label}</p>
87 + <p className="text-xs text-gray-500">
88 + {formatFileSize(size)} ({count})
89 + </p>
90 + </div>
91 + </div>
92 + );
93 + })}
94 + </div>
95 + </div>
96 + );
97 +}
added components/admin/TwoFactorSetup.tsx +149 −0
@@ -0,0 +1,149 @@
1 +"use client";
2 +
3 +import { useState } from "react";
4 +import { Shield, ShieldOff, ShieldCheck } from "lucide-react";
5 +import Button from "../ui/Button";
6 +import toast from "react-hot-toast";
7 +
8 +interface TwoFactorSetupProps {
9 + enabled: boolean;
10 + onStatusChange: () => void;
11 +}
12 +
13 +export default function TwoFactorSetup({ enabled, onStatusChange }: TwoFactorSetupProps) {
14 + const [qrCode, setQrCode] = useState<string | null>(null);
15 + const [secret, setSecret] = useState<string | null>(null);
16 + const [token, setToken] = useState("");
17 + const [loading, setLoading] = useState(false);
18 + const [step, setStep] = useState<"idle" | "setup" | "verify">("idle");
19 +
20 + const startSetup = async () => {
21 + setLoading(true);
22 + try {
23 + const res = await fetch("/api/auth/2fa/setup", { method: "POST" });
24 + if (res.ok) {
25 + const data = await res.json();
26 + setQrCode(data.qrCodeUrl);
27 + setSecret(data.secret);
28 + setStep("setup");
29 + }
30 + } catch {
31 + toast.error("Erreur");
32 + }
33 + setLoading(false);
34 + };
35 +
36 + const verifyAndEnable = async () => {
37 + setLoading(true);
38 + try {
39 + const res = await fetch("/api/auth/2fa/verify", {
40 + method: "POST",
41 + headers: { "Content-Type": "application/json" },
42 + body: JSON.stringify({ token, enable: true }),
43 + });
44 + if (res.ok) {
45 + toast.success("2FA activé !");
46 + setStep("idle");
47 + setQrCode(null);
48 + setToken("");
49 + onStatusChange();
50 + } else {
51 + const data = await res.json();
52 + toast.error(data.error || "Code invalide");
53 + }
54 + } catch {
55 + toast.error("Erreur");
56 + }
57 + setLoading(false);
58 + };
59 +
60 + const disable2FA = async () => {
61 + if (!confirm("Désactiver l'authentification à deux facteurs ?")) return;
62 + const res = await fetch("/api/auth/2fa/disable", { method: "POST" });
63 + if (res.ok) {
64 + toast.success("2FA désactivé");
65 + onStatusChange();
66 + }
67 + };
68 +
69 + const inputClasses = "w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-4 py-3 text-gray-900 dark:text-white text-sm focus:outline-none text-center tracking-[0.3em] sm:tracking-[0.5em] text-xl";
70 +
71 + return (
72 + <div className="bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl p-4 sm:p-6 transition-colors">
73 + <div className="flex items-center gap-3 mb-4">
74 + {enabled ? (
75 + <ShieldCheck className="w-6 h-6 text-green-500 flex-shrink-0" />
76 + ) : (
77 + <Shield className="w-6 h-6 text-gray-400 flex-shrink-0" />
78 + )}
79 + <div>
80 + <h3 className="text-sm font-medium text-gray-900 dark:text-white">
81 + Authentification à deux facteurs (2FA)
82 + </h3>
83 + <p className="text-xs text-gray-500 mt-0.5">
84 + {enabled ? "Activé - Votre compte est protégé" : "Désactivé - Activez pour plus de sécurité"}
85 + </p>
86 + </div>
87 + </div>
88 +
89 + {step === "idle" && !enabled && (
90 + <Button size="sm" onClick={startSetup} loading={loading} className="w-full sm:w-auto">
91 + <Shield className="w-4 h-4" /> Activer 2FA
92 + </Button>
93 + )}
94 +
95 + {step === "idle" && enabled && (
96 + <Button size="sm" variant="danger" onClick={disable2FA} className="w-full sm:w-auto">
97 + <ShieldOff className="w-4 h-4" /> Désactiver 2FA
98 + </Button>
99 + )}
100 +
101 + {step === "setup" && qrCode && (
102 + <div className="space-y-4">
103 + <p className="text-sm text-gray-600 dark:text-gray-400">
104 + Scannez ce QR code avec votre app authenticator (Google Authenticator, Authy, etc.)
105 + </p>
106 + <div className="flex justify-center">
107 + <img src={qrCode} alt="QR Code 2FA" className="w-44 h-44 sm:w-48 sm:h-48 rounded-lg" />
108 + </div>
109 + {secret && (
110 + <div className="text-center">
111 + <p className="text-xs text-gray-500 mb-1">Clé manuelle :</p>
112 + <code className="text-xs bg-gray-100 dark:bg-white/10 px-3 py-1 rounded font-mono text-gray-700 dark:text-gray-300 break-all inline-block max-w-full">
113 + {secret}
114 + </code>
115 + </div>
116 + )}
117 + <Button size="sm" onClick={() => setStep("verify")} className="w-full">
118 + Suivant
119 + </Button>
120 + </div>
121 + )}
122 +
123 + {step === "verify" && (
124 + <div className="space-y-4">
125 + <p className="text-sm text-gray-600 dark:text-gray-400">
126 + Entrez le code à 6 chiffres de votre app authenticator
127 + </p>
128 + <input
129 + type="text"
130 + value={token}
131 + onChange={(e) => setToken(e.target.value.replace(/\D/g, "").slice(0, 6))}
132 + placeholder="000000"
133 + maxLength={6}
134 + className={inputClasses}
135 + autoFocus
136 + />
137 + <div className="flex flex-col sm:flex-row gap-2">
138 + <Button variant="ghost" onClick={() => { setStep("idle"); setQrCode(null); }} className="w-full sm:flex-1">
139 + Annuler
140 + </Button>
141 + <Button onClick={verifyAndEnable} loading={loading} disabled={token.length !== 6} className="w-full sm:flex-1">
142 + Vérifier & Activer
143 + </Button>
144 + </div>
145 + </div>
146 + )}
147 + </div>
148 + );
149 +}
added components/admin/UserManagement.tsx +197 −0
@@ -0,0 +1,197 @@
1 +"use client";
2 +
3 +import { useState, useEffect } from "react";
4 +import { motion } from "framer-motion";
5 +import { UserPlus, Trash2, Shield, Edit2, Users } from "lucide-react";
6 +import Button from "../ui/Button";
7 +import Modal from "../ui/Modal";
8 +import toast from "react-hot-toast";
9 +
10 +interface UserData {
11 + id: string;
12 + email: string | null;
13 + name: string | null;
14 + role: string;
15 + twoFactorEnabled: boolean;
16 + createdAt: string;
17 + _count?: { files: number; folders: number };
18 +}
19 +
20 +export default function UserManagement() {
21 + const [users, setUsers] = useState<UserData[]>([]);
22 + const [loading, setLoading] = useState(true);
23 + const [showCreate, setShowCreate] = useState(false);
24 + const [editUser, setEditUser] = useState<UserData | null>(null);
25 + const [form, setForm] = useState({ email: "", name: "", password: "", role: "user" });
26 +
27 + const fetchUsers = async () => {
28 + const res = await fetch("/api/users");
29 + if (res.ok) setUsers(await res.json());
30 + setLoading(false);
31 + };
32 +
33 + useEffect(() => { fetchUsers(); }, []);
34 +
35 + const handleCreate = async () => {
36 + const res = await fetch("/api/users", {
37 + method: "POST",
38 + headers: { "Content-Type": "application/json" },
39 + body: JSON.stringify(form),
40 + });
41 + if (res.ok) {
42 + toast.success("Utilisateur créé");
43 + setShowCreate(false);
44 + setForm({ email: "", name: "", password: "", role: "user" });
45 + fetchUsers();
46 + } else {
47 + const data = await res.json();
48 + toast.error(data.error || "Erreur");
49 + }
50 + };
51 +
52 + const handleUpdate = async () => {
53 + if (!editUser) return;
54 + const data: Record<string, string> = {};
55 + if (form.name) data.name = form.name;
56 + if (form.email) data.email = form.email;
57 + if (form.role) data.role = form.role;
58 + if (form.password) data.password = form.password;
59 +
60 + const res = await fetch(`/api/users/${editUser.id}`, {
61 + method: "PATCH",
62 + headers: { "Content-Type": "application/json" },
63 + body: JSON.stringify(data),
64 + });
65 + if (res.ok) {
66 + toast.success("Utilisateur modifié");
67 + setEditUser(null);
68 + fetchUsers();
69 + }
70 + };
71 +
72 + const handleDelete = async (id: string) => {
73 + if (!confirm("Supprimer cet utilisateur ?")) return;
74 + const res = await fetch(`/api/users/${id}`, { method: "DELETE" });
75 + if (res.ok) {
76 + toast.success("Utilisateur supprimé");
77 + fetchUsers();
78 + } else {
79 + const data = await res.json();
80 + toast.error(data.error || "Erreur");
81 + }
82 + };
83 +
84 + const inputClasses = "w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-4 py-3 text-gray-900 dark:text-white text-sm focus:outline-none focus:border-gray-400 dark:focus:border-white/20";
85 +
86 + const roleColors: Record<string, string> = {
87 + admin: "bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400",
88 + user: "bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400",
89 + viewer: "bg-gray-100 text-gray-700 dark:bg-gray-500/20 dark:text-gray-400",
90 + };
91 +
92 + return (
93 + <div className="space-y-4">
94 + <div className="flex items-center justify-between">
95 + <h2 className="text-lg font-medium text-gray-900 dark:text-white flex items-center gap-2">
96 + <Users className="w-5 h-5" /> Utilisateurs
97 + </h2>
98 + <Button size="sm" onClick={() => setShowCreate(true)}>
99 + <UserPlus className="w-4 h-4" /> Ajouter
100 + </Button>
101 + </div>
102 +
103 + {loading ? (
104 + <div className="flex justify-center py-8">
105 + <div className="w-6 h-6 border-2 border-gray-300 dark:border-white/20 border-t-gray-600 dark:border-t-white rounded-full animate-spin" />
106 + </div>
107 + ) : (
108 + <div className="space-y-2">
109 + {users.map((user, i) => (
110 + <motion.div
111 + key={user.id}
112 + initial={{ opacity: 0, y: 5 }}
113 + animate={{ opacity: 1, y: 0 }}
114 + transition={{ delay: i * 0.05 }}
115 + className="flex flex-col sm:flex-row sm:items-center gap-3 p-4 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl transition-colors"
116 + >
117 + {/* First line: avatar + name/email */}
118 + <div className="flex items-center gap-3 flex-1 min-w-0">
119 + <div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-white/10 flex items-center justify-center text-sm font-medium text-gray-600 dark:text-gray-300 flex-shrink-0">
120 + {(user.name || user.email || "U")[0].toUpperCase()}
121 + </div>
122 + <div className="flex-1 min-w-0">
123 + <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
124 + {user.name || user.email || user.id}
125 + </p>
126 + <p className="text-xs text-gray-500 truncate">{user.email}</p>
127 + </div>
128 + </div>
129 + {/* Second line on mobile: role badge + file count + actions */}
130 + <div className="flex items-center gap-2 sm:gap-3 pl-13 sm:pl-0">
131 + <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${roleColors[user.role] || roleColors.user}`}>
132 + {user.role}
133 + </span>
134 + {user.twoFactorEnabled && (
135 + <Shield className="w-4 h-4 text-green-500 flex-shrink-0" title="2FA activé" />
136 + )}
137 + <span className="text-xs text-gray-500 hidden sm:block">
138 + {user._count?.files || 0} fichiers
139 + </span>
140 + <div className="flex gap-1 ml-auto">
141 + <button
142 + onClick={() => { setEditUser(user); setForm({ email: user.email || "", name: user.name || "", password: "", role: user.role }); }}
143 + className="min-w-[44px] min-h-[44px] flex items-center justify-center p-2.5 text-gray-400 hover:text-gray-700 dark:hover:text-white rounded-lg transition-colors"
144 + >
145 + <Edit2 className="w-4 h-4" />
146 + </button>
147 + <button
148 + onClick={() => handleDelete(user.id)}
149 + className="min-w-[44px] min-h-[44px] flex items-center justify-center p-2.5 text-gray-400 hover:text-red-500 rounded-lg transition-colors"
150 + >
151 + <Trash2 className="w-4 h-4" />
152 + </button>
153 + </div>
154 + </div>
155 + </motion.div>
156 + ))}
157 + </div>
158 + )}
159 +
160 + {/* Create Modal */}
161 + <Modal isOpen={showCreate} onClose={() => setShowCreate(false)} title="Nouvel utilisateur">
162 + <div className="space-y-3">
163 + <input type="email" placeholder="Email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className={inputClasses} />
164 + <input type="text" placeholder="Nom" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputClasses} />
165 + <input type="password" placeholder="Mot de passe" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} className={inputClasses} />
166 + <select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })} className={inputClasses}>
167 + <option value="user">Utilisateur</option>
168 + <option value="viewer">Lecteur</option>
169 + <option value="admin">Admin</option>
170 + </select>
171 + <div className="flex gap-2 justify-end pt-2">
172 + <Button variant="ghost" onClick={() => setShowCreate(false)}>Annuler</Button>
173 + <Button onClick={handleCreate}>Créer</Button>
174 + </div>
175 + </div>
176 + </Modal>
177 +
178 + {/* Edit Modal */}
179 + <Modal isOpen={!!editUser} onClose={() => setEditUser(null)} title="Modifier utilisateur">
180 + <div className="space-y-3">
181 + <input type="email" placeholder="Email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className={inputClasses} />
182 + <input type="text" placeholder="Nom" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputClasses} />
183 + <input type="password" placeholder="Nouveau mot de passe (vide = inchangé)" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} className={inputClasses} />
184 + <select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })} className={inputClasses}>
185 + <option value="user">Utilisateur</option>
186 + <option value="viewer">Lecteur</option>
187 + <option value="admin">Admin</option>
188 + </select>
189 + <div className="flex gap-2 justify-end pt-2">
190 + <Button variant="ghost" onClick={() => setEditUser(null)}>Annuler</Button>
191 + <Button onClick={handleUpdate}>Sauvegarder</Button>
192 + </div>
193 + </div>
194 + </Modal>
195 + </div>
196 + );
197 +}
added components/ai/AiPanel.tsx +240 −0
@@ -0,0 +1,240 @@
1 +"use client";
2 +
3 +import { useState } from "react";
4 +import { motion, AnimatePresence } from "framer-motion";
5 +import { Brain, Search, Tag, FileText, MessageSquare, Sparkles, X, Loader2 } from "lucide-react";
6 +import Button from "../ui/Button";
7 +
8 +interface AiResult {
9 + fileId: string;
10 + name: string;
11 + relevance: string;
12 + snippet: string;
13 +}
14 +
15 +export default function AiPanel() {
16 + const [open, setOpen] = useState(false);
17 + const [tab, setTab] = useState<"search" | "chat">("search");
18 + const [query, setQuery] = useState("");
19 + const [chatMessage, setChatMessage] = useState("");
20 + const [searching, setSearching] = useState(false);
21 + const [chatting, setChatting] = useState(false);
22 + const [results, setResults] = useState<AiResult[]>([]);
23 + const [answer, setAnswer] = useState("");
24 + const [chatHistory, setChatHistory] = useState<Array<{ role: string; content: string }>>([]);
25 +
26 + const handleSearch = async () => {
27 + if (!query.trim()) return;
28 + setSearching(true);
29 + setResults([]);
30 + setAnswer("");
31 + try {
32 + const res = await fetch("/api/ai/search", {
33 + method: "POST",
34 + headers: { "Content-Type": "application/json" },
35 + body: JSON.stringify({ query }),
36 + });
37 + if (res.ok) {
38 + const data = await res.json();
39 + setResults(data.results || []);
40 + setAnswer(data.answer || "");
41 + }
42 + } catch {}
43 + setSearching(false);
44 + };
45 +
46 + const handleChat = async () => {
47 + if (!chatMessage.trim()) return;
48 + const msg = chatMessage;
49 + setChatMessage("");
50 + setChatHistory((prev) => [...prev, { role: "user", content: msg }]);
51 + setChatting(true);
52 + try {
53 + const res = await fetch("/api/ai/chat", {
54 + method: "POST",
55 + headers: { "Content-Type": "application/json" },
56 + body: JSON.stringify({ message: msg }),
57 + });
58 + if (res.ok) {
59 + const data = await res.json();
60 + setChatHistory((prev) => [...prev, { role: "assistant", content: data.reply }]);
61 + }
62 + } catch {}
63 + setChatting(false);
64 + };
65 +
66 + const relevanceColors: Record<string, string> = {
67 + high: "bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400",
68 + medium: "bg-yellow-100 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-400",
69 + low: "bg-gray-100 text-gray-600 dark:bg-gray-500/20 dark:text-gray-400",
70 + };
71 +
72 + const inputClasses = "w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-4 py-3 text-gray-900 dark:text-white text-sm focus:outline-none focus:border-gray-400 dark:focus:border-white/20 placeholder-gray-400 dark:placeholder-gray-500";
73 +
74 + return (
75 + <>
76 + {/* AI Toggle Button - positioned above mobile safe area */}
77 + <button
78 + onClick={() => setOpen(true)}
79 + className="fixed bottom-20 right-4 sm:bottom-6 sm:right-6 z-40 w-14 h-14 rounded-full bg-purple-600 hover:bg-purple-700 text-white shadow-lg flex items-center justify-center transition-colors"
80 + title="Assistant IA"
81 + >
82 + <Brain className="w-6 h-6" />
83 + </button>
84 +
85 + {/* AI Panel */}
86 + <AnimatePresence>
87 + {open && (
88 + <>
89 + <motion.div
90 + initial={{ opacity: 0 }}
91 + animate={{ opacity: 1 }}
92 + exit={{ opacity: 0 }}
93 + className="fixed inset-0 bg-black/30 z-40 lg:hidden"
94 + onClick={() => setOpen(false)}
95 + />
96 + <motion.div
97 + initial={{ x: 400, opacity: 0 }}
98 + animate={{ x: 0, opacity: 1 }}
99 + exit={{ x: 400, opacity: 0 }}
100 + transition={{ type: "spring", damping: 25, stiffness: 300 }}
101 + className="fixed right-0 top-0 bottom-0 w-full sm:w-96 z-50 bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-white/10 flex flex-col shadow-2xl pb-[env(safe-area-inset-bottom)]"
102 + >
103 + {/* Header */}
104 + <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-white/10 pt-[max(0.75rem,env(safe-area-inset-top))]">
105 + <h2 className="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2">
106 + <Sparkles className="w-4 h-4 text-purple-500" /> Assistant IA
107 + </h2>
108 + <button
109 + onClick={() => setOpen(false)}
110 + className="min-w-[44px] min-h-[44px] flex items-center justify-center p-2 text-gray-400 hover:text-gray-700 dark:hover:text-white"
111 + >
112 + <X className="w-5 h-5" />
113 + </button>
114 + </div>
115 +
116 + {/* Tabs */}
117 + <div className="flex border-b border-gray-200 dark:border-white/10">
118 + <button
119 + onClick={() => setTab("search")}
120 + className={`flex-1 px-4 py-3 text-sm font-medium transition-colors flex items-center justify-center gap-1.5 ${tab === "search" ? "text-purple-600 dark:text-purple-400 border-b-2 border-purple-500" : "text-gray-500"}`}
121 + >
122 + <Search className="w-4 h-4" /> Recherche IA
123 + </button>
124 + <button
125 + onClick={() => setTab("chat")}
126 + className={`flex-1 px-4 py-3 text-sm font-medium transition-colors flex items-center justify-center gap-1.5 ${tab === "chat" ? "text-purple-600 dark:text-purple-400 border-b-2 border-purple-500" : "text-gray-500"}`}
127 + >
128 + <MessageSquare className="w-4 h-4" /> Chat
129 + </button>
130 + </div>
131 +
132 + {/* Content */}
133 + <div className="flex-1 overflow-auto p-4">
134 + {tab === "search" ? (
135 + <div className="space-y-4">
136 + <div className="flex gap-2">
137 + <input
138 + type="text"
139 + value={query}
140 + onChange={(e) => setQuery(e.target.value)}
141 + onKeyDown={(e) => e.key === "Enter" && handleSearch()}
142 + placeholder="Rechercher avec l'IA..."
143 + className={`${inputClasses} flex-1`}
144 + />
145 + <Button size="sm" onClick={handleSearch} loading={searching}>
146 + <Search className="w-4 h-4" />
147 + </Button>
148 + </div>
149 +
150 + {answer && (
151 + <div className="bg-purple-50 dark:bg-purple-500/10 border border-purple-200 dark:border-purple-500/20 rounded-lg p-3.5">
152 + <p className="text-sm text-purple-800 dark:text-purple-300">{answer}</p>
153 + </div>
154 + )}
155 +
156 + {results.map((r) => (
157 + <div key={r.fileId} className="flex items-start gap-3 p-3.5 bg-gray-50 dark:bg-white/5 rounded-lg">
158 + <FileText className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
159 + <div className="flex-1 min-w-0">
160 + <div className="flex items-center gap-2">
161 + <p className="text-sm font-medium text-gray-900 dark:text-white truncate">{r.name}</p>
162 + <span className={`px-1.5 py-0.5 rounded text-[10px] font-medium flex-shrink-0 ${relevanceColors[r.relevance] || relevanceColors.low}`}>
163 + {r.relevance}
164 + </span>
165 + </div>
166 + <p className="text-xs text-gray-500 mt-0.5">{r.snippet}</p>
167 + </div>
168 + </div>
169 + ))}
170 +
171 + {/* Quick actions */}
172 + <div className="pt-2 space-y-2">
173 + <p className="text-xs text-gray-500 font-medium uppercase tracking-wider">Actions rapides</p>
174 + <div className="grid grid-cols-2 gap-2">
175 + <button onClick={() => { setQuery("documents récents"); handleSearch(); }} className="text-left p-3 text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-white/5 rounded-lg hover:bg-gray-100 dark:hover:bg-white/10">
176 + <FileText className="w-3 h-3 mb-1" /> Documents récents
177 + </button>
178 + <button onClick={() => { setQuery("images et photos"); handleSearch(); }} className="text-left p-3 text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-white/5 rounded-lg hover:bg-gray-100 dark:hover:bg-white/10">
179 + <Tag className="w-3 h-3 mb-1" /> Images & Photos
180 + </button>
181 + </div>
182 + </div>
183 + </div>
184 + ) : (
185 + <div className="flex flex-col h-full">
186 + <div className="flex-1 space-y-3 mb-4">
187 + {chatHistory.length === 0 && (
188 + <div className="text-center text-gray-500 py-8">
189 + <Brain className="w-8 h-8 mx-auto mb-2 text-gray-300 dark:text-gray-600" />
190 + <p className="text-sm">Posez une question sur vos fichiers</p>
191 + </div>
192 + )}
193 + {chatHistory.map((msg, i) => (
194 + <div key={i} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
195 + <div className={`max-w-[80%] px-3 py-2 rounded-lg text-sm ${
196 + msg.role === "user"
197 + ? "bg-purple-600 text-white"
198 + : "bg-gray-100 dark:bg-white/10 text-gray-900 dark:text-gray-200"
199 + }`}>
200 + {msg.content}
201 + </div>
202 + </div>
203 + ))}
204 + {chatting && (
205 + <div className="flex justify-start">
206 + <div className="bg-gray-100 dark:bg-white/10 px-3 py-2 rounded-lg">
207 + <Loader2 className="w-4 h-4 animate-spin text-purple-500" />
208 + </div>
209 + </div>
210 + )}
211 + </div>
212 + </div>
213 + )}
214 + </div>
215 +
216 + {/* Chat input */}
217 + {tab === "chat" && (
218 + <div className="p-4 border-t border-gray-200 dark:border-white/10">
219 + <div className="flex gap-2">
220 + <input
221 + type="text"
222 + value={chatMessage}
223 + onChange={(e) => setChatMessage(e.target.value)}
224 + onKeyDown={(e) => e.key === "Enter" && handleChat()}
225 + placeholder="Votre message..."
226 + className={`${inputClasses} flex-1`}
227 + />
228 + <Button size="sm" onClick={handleChat} loading={chatting}>
229 + <MessageSquare className="w-4 h-4" />
230 + </Button>
231 + </div>
232 + </div>
233 + )}
234 + </motion.div>
235 + </>
236 + )}
237 + </AnimatePresence>
238 + </>
239 + );
240 +}
added components/files/BulkActions.tsx +127 −0
@@ -0,0 +1,127 @@
1 +"use client";
2 +
3 +import { useState } from "react";
4 +import { Trash2, FolderInput, Download, Lock, Unlock, X, Sparkles } from "lucide-react";
5 +import Button from "../ui/Button";
6 +import Modal from "../ui/Modal";
7 +import toast from "react-hot-toast";
8 +
9 +interface BulkActionsProps {
10 + selectedIds: string[];
11 + onClear: () => void;
12 + onRefresh: () => void;
13 +}
14 +
15 +export default function BulkActions({ selectedIds, onClear, onRefresh }: BulkActionsProps) {
16 + const [moveModal, setMoveModal] = useState(false);
17 + const [folders, setFolders] = useState<Array<{ id: string; name: string }>>([]);
18 + const [loading, setLoading] = useState(false);
19 +
20 + const bulkAction = async (action: string, extra?: Record<string, unknown>) => {
21 + setLoading(true);
22 + try {
23 + const res = await fetch("/api/files/bulk", {
24 + method: "POST",
25 + headers: { "Content-Type": "application/json" },
26 + body: JSON.stringify({ action, fileIds: selectedIds, ...extra }),
27 + });
28 +
29 + if (action === "download" && res.ok) {
30 + const blob = await res.blob();
31 + const url = URL.createObjectURL(blob);
32 + const a = document.createElement("a");
33 + a.href = url;
34 + a.download = "files.zip";
35 + a.click();
36 + URL.revokeObjectURL(url);
37 + toast.success("Téléchargement lancé");
38 + } else if (res.ok) {
39 + const data = await res.json();
40 + toast.success(`Action terminée (${data.count || selectedIds.length} fichiers)`);
41 + onRefresh();
42 + onClear();
43 + } else {
44 + toast.error("Erreur");
45 + }
46 + } catch {
47 + toast.error("Erreur");
48 + }
49 + setLoading(false);
50 + };
51 +
52 + const openMoveModal = async () => {
53 + const res = await fetch("/api/folders?tree=true");
54 + if (res.ok) setFolders(await res.json());
55 + setMoveModal(true);
56 + };
57 +
58 + const handleMove = async (targetFolderId: string) => {
59 + await bulkAction("move", { targetFolderId });
60 + setMoveModal(false);
61 + };
62 +
63 + const aiTagAll = async () => {
64 + setLoading(true);
65 + for (const id of selectedIds) {
66 + try {
67 + await fetch("/api/ai/tag", {
68 + method: "POST",
69 + headers: { "Content-Type": "application/json" },
70 + body: JSON.stringify({ fileId: id }),
71 + });
72 + } catch {}
73 + }
74 + toast.success("Tags IA générés");
75 + setLoading(false);
76 + onRefresh();
77 + };
78 +
79 + if (selectedIds.length === 0) return null;
80 +
81 + return (
82 + <>
83 + <div className="flex items-center gap-1.5 sm:gap-2 px-4 py-2 bg-blue-50 dark:bg-blue-500/10 border-b border-blue-200 dark:border-blue-500/20 overflow-x-auto">
84 + <span className="text-sm text-blue-700 dark:text-blue-300 font-medium whitespace-nowrap flex-shrink-0">
85 + {selectedIds.length} sélectionné{selectedIds.length > 1 ? "s" : ""}
86 + </span>
87 + <div className="flex-1 min-w-0" />
88 + <div className="flex items-center gap-1.5 flex-nowrap flex-shrink-0">
89 + <Button size="sm" variant="ghost" onClick={() => bulkAction("download")} loading={loading} className="min-h-[40px] whitespace-nowrap">
90 + <Download className="w-3.5 h-3.5" /> <span className="hidden sm:inline">ZIP</span>
91 + </Button>
92 + <Button size="sm" variant="ghost" onClick={openMoveModal} className="min-h-[40px] whitespace-nowrap">
93 + <FolderInput className="w-3.5 h-3.5" /> <span className="hidden sm:inline">Déplacer</span>
94 + </Button>
95 + <Button size="sm" variant="ghost" onClick={() => bulkAction("encrypt")} loading={loading} className="min-h-[40px] whitespace-nowrap">
96 + <Lock className="w-3.5 h-3.5" /> <span className="hidden sm:inline">Chiffrer</span>
97 + </Button>
98 + <Button size="sm" variant="ghost" onClick={() => bulkAction("decrypt")} loading={loading} className="min-h-[40px] whitespace-nowrap">
99 + <Unlock className="w-3.5 h-3.5" /> <span className="hidden sm:inline">Déchiffrer</span>
100 + </Button>
101 + <Button size="sm" variant="ghost" onClick={aiTagAll} loading={loading} className="min-h-[40px] whitespace-nowrap">
102 + <Sparkles className="w-3.5 h-3.5" /> <span className="hidden sm:inline">Tags IA</span>
103 + </Button>
104 + <Button size="sm" variant="danger" onClick={() => { if (confirm(`Supprimer ${selectedIds.length} fichier(s) ?`)) bulkAction("delete"); }} loading={loading} className="min-h-[40px] whitespace-nowrap">
105 + <Trash2 className="w-3.5 h-3.5" /> <span className="hidden sm:inline">Supprimer</span>
106 + </Button>
107 + </div>
108 + <button onClick={onClear} className="min-w-[44px] min-h-[44px] flex items-center justify-center text-gray-400 hover:text-gray-700 dark:hover:text-white flex-shrink-0">
109 + <X className="w-4 h-4" />
110 + </button>
111 + </div>
112 +
113 + <Modal isOpen={moveModal} onClose={() => setMoveModal(false)} title="Déplacer vers">
114 + <div className="space-y-1 max-h-60 overflow-y-auto">
115 + <button onClick={() => handleMove("root")} className="w-full text-left px-3 py-2.5 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg">
116 + Racine
117 + </button>
118 + {folders.map((f: { id: string; name: string }) => (
119 + <button key={f.id} onClick={() => handleMove(f.id)} className="w-full text-left px-3 py-2.5 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg">
120 + {f.name}
121 + </button>
122 + ))}
123 + </div>
124 + </Modal>
125 + </>
126 + );
127 +}
added components/files/FileActions.tsx +348 −0
@@ -0,0 +1,348 @@
1 +"use client";
2 +
3 +import { useState } from "react";
4 +import { motion } from "framer-motion";
5 +import {
6 + Download,
7 + Pencil,
8 + Trash2,
9 + Copy,
10 + Share2,
11 + FolderInput,
12 +} from "lucide-react";
13 +import toast from "react-hot-toast";
14 +import Modal from "../ui/Modal";
15 +import Button from "../ui/Button";
16 +
17 +interface FileData {
18 + id: string;
19 + name: string;
20 + size: number;
21 + mimeType: string;
22 + folderId?: string | null;
23 +}
24 +
25 +interface FileActionsProps {
26 + file: FileData;
27 + onClose: () => void;
28 + onRefresh: () => void;
29 +}
30 +
31 +export default function FileActions({ file, onClose, onRefresh }: FileActionsProps) {
32 + const [renameModal, setRenameModal] = useState(false);
33 + const [shareModal, setShareModal] = useState(false);
34 + const [moveModal, setMoveModal] = useState(false);
35 + const [deleteModal, setDeleteModal] = useState(false);
36 + const [newName, setNewName] = useState(file.name);
37 + const [sharePassword, setSharePassword] = useState("");
38 + const [shareExpiry, setShareExpiry] = useState("");
39 + const [shareMode, setShareMode] = useState<"DOWNLOAD" | "PREVIEW">("DOWNLOAD");
40 + const [loading, setLoading] = useState(false);
41 + const [folders, setFolders] = useState<{ id: string; name: string }[]>([]);
42 +
43 + const handleDownload = () => {
44 + window.open(`/api/files/${file.id}/download`, "_blank");
45 + onClose();
46 + };
47 +
48 + const handleRename = async () => {
49 + setLoading(true);
50 + try {
51 + const res = await fetch(`/api/files/${file.id}`, {
52 + method: "PATCH",
53 + headers: { "Content-Type": "application/json" },
54 + body: JSON.stringify({ name: newName }),
55 + });
56 + if (res.ok) {
57 + toast.success("Fichier renommé");
58 + onRefresh();
59 + }
60 + } catch {
61 + toast.error("Erreur");
62 + } finally {
63 + setLoading(false);
64 + setRenameModal(false);
65 + onClose();
66 + }
67 + };
68 +
69 + const handleDuplicate = async () => {
70 + try {
71 + const res = await fetch(`/api/files/${file.id}/duplicate`, {
72 + method: "POST",
73 + });
74 + if (res.ok) {
75 + toast.success("Fichier dupliqué");
76 + onRefresh();
77 + }
78 + } catch {
79 + toast.error("Erreur");
80 + }
81 + onClose();
82 + };
83 +
84 + const handleDelete = async () => {
85 + setLoading(true);
86 + try {
87 + const res = await fetch(`/api/files/${file.id}`, {
88 + method: "DELETE",
89 + });
90 + if (res.ok) {
91 + toast.success("Fichier supprimé");
92 + onRefresh();
93 + }
94 + } catch {
95 + toast.error("Erreur");
96 + } finally {
97 + setLoading(false);
98 + setDeleteModal(false);
99 + onClose();
100 + }
101 + };
102 +
103 + const [shareLink, setShareLink] = useState<string | null>(null);
104 +
105 + const copyToClipboard = async (text: string) => {
106 + try {
107 + await navigator.clipboard.writeText(text);
108 + return true;
109 + } catch {
110 + // Fallback for non-HTTPS contexts
111 + const textarea = document.createElement("textarea");
112 + textarea.value = text;
113 + textarea.style.position = "fixed";
114 + textarea.style.opacity = "0";
115 + document.body.appendChild(textarea);
116 + textarea.select();
117 + try {
118 + document.execCommand("copy");
119 + return true;
120 + } catch {
121 + return false;
122 + } finally {
123 + document.body.removeChild(textarea);
124 + }
125 + }
126 + };
127 +
128 + const handleShare = async () => {
129 + setLoading(true);
130 + try {
131 + const res = await fetch("/api/shares", {
132 + method: "POST",
133 + headers: { "Content-Type": "application/json" },
134 + body: JSON.stringify({
135 + fileId: file.id,
136 + password: sharePassword || undefined,
137 + expiresAt: shareExpiry || undefined,
138 + mode: shareMode,
139 + }),
140 + });
141 + if (res.ok) {
142 + const data = await res.json();
143 + const url = `${window.location.origin}/shared/${data.token}`;
144 + setShareLink(url);
145 + const copied = await copyToClipboard(url);
146 + if (copied) {
147 + toast.success("Lien copié dans le presse-papiers");
148 + } else {
149 + toast.success("Lien de partage créé");
150 + }
151 + } else {
152 + const data = await res.json();
153 + toast.error(data.error || "Erreur lors du partage");
154 + }
155 + } catch (err) {
156 + toast.error("Erreur réseau: " + (err as Error).message);
157 + } finally {
158 + setLoading(false);
159 + }
160 + };
161 +
162 + const handleMove = async (targetFolderId: string) => {
163 + try {
164 + const res = await fetch(`/api/files/${file.id}`, {
165 + method: "PATCH",
166 + headers: { "Content-Type": "application/json" },
167 + body: JSON.stringify({ folderId: targetFolderId }),
168 + });
169 + if (res.ok) {
170 + toast.success("Fichier déplacé");
171 + onRefresh();
172 + }
173 + } catch {
174 + toast.error("Erreur");
175 + }
176 + setMoveModal(false);
177 + onClose();
178 + };
179 +
180 + const loadFolders = async () => {
181 + const res = await fetch("/api/folders?tree=true");
182 + if (res.ok) {
183 + const data = await res.json();
184 + setFolders(data);
185 + }
186 + };
187 +
188 + const actions = [
189 + { icon: Download, label: "Télécharger", action: handleDownload },
190 + { icon: Pencil, label: "Renommer", action: () => setRenameModal(true) },
191 + { icon: Copy, label: "Dupliquer", action: handleDuplicate },
192 + {
193 + icon: FolderInput,
194 + label: "Déplacer",
195 + action: () => {
196 + loadFolders();
197 + setMoveModal(true);
198 + },
199 + },
200 + { icon: Share2, label: "Partager", action: () => setShareModal(true) },
201 + { icon: Trash2, label: "Supprimer", action: () => setDeleteModal(true), danger: true },
202 + ];
203 +
204 + const inputClasses = "w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-4 py-3 text-gray-900 dark:text-white text-sm focus:outline-none focus:border-gray-400 dark:focus:border-white/20";
205 +
206 + return (
207 + <>
208 + <motion.div
209 + initial={{ opacity: 0, scale: 0.95 }}
210 + animate={{ opacity: 1, scale: 1 }}
211 + className="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-white/10 rounded-xl shadow-2xl z-50 py-1 overflow-hidden max-h-[70vh] overflow-auto"
212 + onClick={(e) => e.stopPropagation()}
213 + >
214 + {actions.map((action) => (
215 + <button
216 + key={action.label}
217 + onClick={(e) => {
218 + e.stopPropagation();
219 + action.action();
220 + }}
221 + className={`flex items-center gap-3 w-full px-4 py-3 text-sm transition-colors ${
222 + (action as { danger?: boolean }).danger
223 + ? "text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10"
224 + : "text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5"
225 + }`}
226 + >
227 + <action.icon className="w-4 h-4" />
228 + {action.label}
229 + </button>
230 + ))}
231 + </motion.div>
232 +
233 + {/* Rename Modal */}
234 + <Modal isOpen={renameModal} onClose={() => setRenameModal(false)} title="Renommer">
235 + <input
236 + type="text"
237 + value={newName}
238 + onChange={(e) => setNewName(e.target.value)}
239 + className={`${inputClasses} mb-4`}
240 + autoFocus
241 + />
242 + <div className="flex gap-2 justify-end">
243 + <Button variant="ghost" onClick={() => setRenameModal(false)}>Annuler</Button>
244 + <Button onClick={handleRename} loading={loading}>Renommer</Button>
245 + </div>
246 + </Modal>
247 +
248 + {/* Delete Modal */}
249 + <Modal isOpen={deleteModal} onClose={() => setDeleteModal(false)} title="Supprimer">
250 + <p className="text-gray-500 dark:text-gray-400 text-sm mb-4">
251 + Supprimer <strong className="text-gray-900 dark:text-white">{file.name}</strong> ? Cette action est irréversible.
252 + </p>
253 + <div className="flex gap-2 justify-end">
254 + <Button variant="ghost" onClick={() => setDeleteModal(false)}>Annuler</Button>
255 + <Button variant="danger" onClick={handleDelete} loading={loading}>Supprimer</Button>
256 + </div>
257 + </Modal>
258 +
259 + {/* Share Modal */}
260 + <Modal isOpen={shareModal} onClose={() => { setShareModal(false); setShareLink(null); onClose(); }} title="Partager">
261 + {shareLink ? (
262 + <div className="space-y-4">
263 + <p className="text-sm text-green-600 dark:text-green-400 font-medium">Lien de partage créé !</p>
264 + <div className="flex items-center gap-2">
265 + <input
266 + type="text"
267 + value={shareLink}
268 + readOnly
269 + className={`${inputClasses} text-xs font-mono`}
270 + onClick={(e) => (e.target as HTMLInputElement).select()}
271 + />
272 + <Button
273 + size="sm"
274 + onClick={() => {
275 + copyToClipboard(shareLink);
276 + toast.success("Lien copié !");
277 + }}
278 + >
279 + Copier
280 + </Button>
281 + </div>
282 + <Button variant="ghost" onClick={() => { setShareModal(false); setShareLink(null); onClose(); }} className="w-full">
283 + Fermer
284 + </Button>
285 + </div>
286 + ) : (
287 + <div className="space-y-4">
288 + <div>
289 + <label className="text-xs text-gray-500 dark:text-gray-400 block mb-1">Mode</label>
290 + <select
291 + value={shareMode}
292 + onChange={(e) => setShareMode(e.target.value as "DOWNLOAD" | "PREVIEW")}
293 + className={inputClasses}
294 + >
295 + <option value="DOWNLOAD">Téléchargement</option>
296 + <option value="PREVIEW">Aperçu seulement</option>
297 + </select>
298 + </div>
299 + <div>
300 + <label className="text-xs text-gray-500 dark:text-gray-400 block mb-1">Mot de passe (optionnel)</label>
301 + <input
302 + type="password"
303 + value={sharePassword}
304 + onChange={(e) => setSharePassword(e.target.value)}
305 + placeholder="Laisser vide pour pas de mot de passe"
306 + className={inputClasses}
307 + />
308 + </div>
309 + <div>
310 + <label className="text-xs text-gray-500 dark:text-gray-400 block mb-1">Expiration (optionnel)</label>
311 + <input
312 + type="datetime-local"
313 + value={shareExpiry}
314 + onChange={(e) => setShareExpiry(e.target.value)}
315 + className={inputClasses}
316 + />
317 + </div>
318 + <div className="flex gap-2 justify-end">
319 + <Button variant="ghost" onClick={() => { setShareModal(false); onClose(); }}>Annuler</Button>
320 + <Button onClick={handleShare} loading={loading}>Créer le lien</Button>
321 + </div>
322 + </div>
323 + )}
324 + </Modal>
325 +
326 + {/* Move Modal */}
327 + <Modal isOpen={moveModal} onClose={() => setMoveModal(false)} title="Déplacer vers">
328 + <div className="space-y-1 max-h-60 overflow-y-auto">
329 + <button
330 + onClick={() => handleMove("root")}
331 + className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg"
332 + >
333 + Racine
334 + </button>
335 + {folders.map((folder: { id: string; name: string }) => (
336 + <button
337 + key={folder.id}
338 + onClick={() => handleMove(folder.id)}
339 + className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg"
340 + >
341 + {folder.name}
342 + </button>
343 + ))}
344 + </div>
345 + </Modal>
346 + </>
347 + );
348 +}
added components/files/FileCard.tsx +151 −0
@@ -0,0 +1,151 @@
1 +"use client";
2 +
3 +import { motion } from "framer-motion";
4 +import {
5 + FileText,
6 + Image,
7 + Video,
8 + Music,
9 + FileCode,
10 + FileSpreadsheet,
11 + File,
12 + Archive,
13 + MoreVertical,
14 +} from "lucide-react";
15 +import { formatFileSize, formatDate, getFileIcon } from "@/lib/utils";
16 +import { useState, useRef, useEffect } from "react";
17 +import FileActions from "./FileActions";
18 +
19 +interface FileData {
20 + id: string;
21 + name: string;
22 + size: number;
23 + mimeType: string;
24 + createdAt: string;
25 + folderId?: string | null;
26 +}
27 +
28 +interface FileCardProps {
29 + file: FileData;
30 + onPreview: (file: FileData) => void;
31 + onRefresh: () => void;
32 + viewMode: "grid" | "list";
33 +}
34 +
35 +const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
36 + image: Image,
37 + video: Video,
38 + audio: Music,
39 + pdf: FileText,
40 + code: FileCode,
41 + spreadsheet: FileSpreadsheet,
42 + archive: Archive,
43 + document: FileText,
44 + file: File,
45 + presentation: FileText,
46 +};
47 +
48 +export default function FileCard({ file, onPreview, onRefresh, viewMode }: FileCardProps) {
49 + const [showActions, setShowActions] = useState(false);
50 + const menuRef = useRef<HTMLDivElement>(null);
51 +
52 + const iconType = getFileIcon(file.mimeType);
53 + const Icon = iconMap[iconType] || File;
54 +
55 + useEffect(() => {
56 + function handleClick(e: MouseEvent) {
57 + if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
58 + setShowActions(false);
59 + }
60 + }
61 + document.addEventListener("mousedown", handleClick);
62 + return () => document.removeEventListener("mousedown", handleClick);
63 + }, []);
64 +
65 + const isImage = file.mimeType.startsWith("image/");
66 +
67 + if (viewMode === "list") {
68 + return (
69 + <motion.div
70 + initial={{ opacity: 0 }}
71 + animate={{ opacity: 1 }}
72 + className="flex items-center gap-3 px-3 py-3.5 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg cursor-pointer group transition-colors"
73 + onClick={() => onPreview(file)}
74 + >
75 + <Icon className="w-5 h-5 text-gray-400 flex-shrink-0" />
76 + <span className="text-sm text-gray-700 dark:text-gray-200 flex-1 truncate">{file.name}</span>
77 + <span className="text-sm text-gray-500 hidden sm:block">{formatFileSize(file.size)}</span>
78 + <span className="text-xs text-gray-500 hidden md:block">{formatDate(file.createdAt)}</span>
79 + <div className="relative" ref={menuRef}>
80 + <button
81 + onClick={(e) => {
82 + e.stopPropagation();
83 + setShowActions(!showActions);
84 + }}
85 + className="min-w-[44px] min-h-[44px] flex items-center justify-center text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
86 + >
87 + <MoreVertical className="w-4 h-4" />
88 + </button>
89 + {showActions && (
90 + <FileActions
91 + file={file}
92 + onClose={() => setShowActions(false)}
93 + onRefresh={onRefresh}
94 + />
95 + )}
96 + </div>
97 + </motion.div>
98 + );
99 + }
100 +
101 + return (
102 + <motion.div
103 + initial={{ opacity: 0, scale: 0.95 }}
104 + animate={{ opacity: 1, scale: 1 }}
105 + className="group relative bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl hover:border-gray-300 dark:hover:border-white/20 transition-colors cursor-pointer shadow-sm dark:shadow-none"
106 + onClick={() => onPreview(file)}
107 + >
108 + {/* Preview thumbnail */}
109 + <div className="aspect-square flex items-center justify-center bg-gray-50 dark:bg-white/[0.02] relative rounded-t-xl overflow-hidden">
110 + {isImage ? (
111 + <img
112 + src={`/api/files/${file.id}/preview`}
113 + alt={file.name}
114 + className="w-full h-full object-cover"
115 + loading="lazy"
116 + />
117 + ) : (
118 + <Icon className="w-12 h-12 text-gray-300 dark:text-gray-600" />
119 + )}
120 +
121 + {/* Actions button */}
122 + <div className="absolute top-2 right-2" ref={menuRef}>
123 + <button
124 + onClick={(e) => {
125 + e.stopPropagation();
126 + setShowActions(!showActions);
127 + }}
128 + className="p-2 bg-white/80 dark:bg-black/50 rounded-lg text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white sm:opacity-0 sm:group-hover:opacity-100 transition-opacity backdrop-blur-sm min-w-[40px] min-h-[40px] flex items-center justify-center"
129 + >
130 + <MoreVertical className="w-4 h-4" />
131 + </button>
132 + {showActions && (
133 + <FileActions
134 + file={file}
135 + onClose={() => setShowActions(false)}
136 + onRefresh={onRefresh}
137 + />
138 + )}
139 + </div>
140 + </div>
141 +
142 + {/* Info */}
143 + <div className="p-3">
144 + <p className="text-sm text-gray-700 dark:text-gray-200 truncate">{file.name}</p>
145 + <p className="text-sm text-gray-500 mt-1">
146 + {formatFileSize(file.size)}
147 + </p>
148 + </div>
149 + </motion.div>
150 + );
151 +}
added components/files/FileExplorer.tsx +233 −0
@@ -0,0 +1,233 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useCallback } from "react";
4 +import { Grid, List, Plus, Upload, CheckSquare, Square } from "lucide-react";
5 +import FileCard from "./FileCard";
6 +import FileUpload from "./FileUpload";
7 +import PreviewEngine from "../preview/PreviewEngine";
8 +import FolderCard from "../folders/FolderCard";
9 +import CreateFolderModal from "../folders/CreateFolderModal";
10 +import SearchBar from "../ui/SearchBar";
11 +import Button from "../ui/Button";
12 +import BulkActions from "./BulkActions";
13 +import { motion, AnimatePresence } from "framer-motion";
14 +
15 +interface FileData {
16 + id: string;
17 + name: string;
18 + size: number;
19 + mimeType: string;
20 + createdAt: string;
21 + folderId?: string | null;
22 +}
23 +
24 +interface FolderData {
25 + id: string;
26 + name: string;
27 + parentId: string | null;
28 + createdAt: string;
29 + _count?: { files: number; children: number };
30 +}
31 +
32 +interface FileExplorerProps {
33 + folderId?: string | null;
34 +}
35 +
36 +export default function FileExplorer({ folderId }: FileExplorerProps) {
37 + const [files, setFiles] = useState<FileData[]>([]);
38 + const [folders, setFolders] = useState<FolderData[]>([]);
39 + const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
40 + const [showUpload, setShowUpload] = useState(false);
41 + const [showCreateFolder, setShowCreateFolder] = useState(false);
42 + const [previewFile, setPreviewFile] = useState<FileData | null>(null);
43 + const [searchQuery, setSearchQuery] = useState("");
44 + const [loading, setLoading] = useState(true);
45 + const [selectedIds, setSelectedIds] = useState<string[]>([]);
46 + const [selectMode, setSelectMode] = useState(false);
47 +
48 + const fetchData = useCallback(async () => {
49 + setLoading(true);
50 + try {
51 + const params = new URLSearchParams();
52 + if (folderId) params.set("folderId", folderId);
53 + else params.set("folderId", "root");
54 + if (searchQuery) params.set("search", searchQuery);
55 +
56 + const [filesRes, foldersRes] = await Promise.all([
57 + fetch(`/api/files?${params}`),
58 + fetch(`/api/folders?parentId=${folderId || "root"}`),
59 + ]);
60 +
61 + if (filesRes.ok) setFiles(await filesRes.json());
62 + if (foldersRes.ok) setFolders(await foldersRes.json());
63 + } catch (err) {
64 + console.error("Failed to fetch data:", err);
65 + } finally {
66 + setLoading(false);
67 + }
68 + }, [folderId, searchQuery]);
69 +
70 + useEffect(() => { fetchData(); }, [fetchData]);
71 +
72 + const handleSearch = useCallback((query: string) => { setSearchQuery(query); }, []);
73 +
74 + const toggleSelect = (id: string) => {
75 + setSelectedIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
76 + };
77 +
78 + const selectAll = () => {
79 + if (selectedIds.length === files.length) setSelectedIds([]);
80 + else setSelectedIds(files.map((f) => f.id));
81 + };
82 +
83 + const handleFileClick = (file: FileData) => {
84 + if (selectMode) {
85 + toggleSelect(file.id);
86 + } else {
87 + setPreviewFile(file);
88 + }
89 + };
90 +
91 + return (
92 + <div className="flex flex-col h-full lg:flex-row overflow-x-hidden">
93 + <div className="flex-1 flex flex-col min-w-0 overflow-x-hidden">
94 + {/* Toolbar */}
95 + <div className="px-4 lg:px-6 py-4 border-b border-gray-200 dark:border-white/10 flex flex-col sm:flex-row items-start sm:items-center gap-3">
96 + <div className="flex-1 w-full sm:w-auto">
97 + <SearchBar onSearch={handleSearch} placeholder="Rechercher des fichiers..." />
98 + </div>
99 + <div className="flex items-center gap-2 w-full sm:w-auto justify-between sm:justify-end">
100 + <div className="flex items-center gap-1 bg-gray-100 dark:bg-white/5 rounded-lg p-1">
101 + <button
102 + onClick={() => setViewMode("grid")}
103 + className={`min-w-[40px] min-h-[40px] flex items-center justify-center rounded transition-colors ${viewMode === "grid" ? "bg-white dark:bg-white/10 text-gray-900 dark:text-white shadow-sm dark:shadow-none" : "text-gray-400"}`}
104 + >
105 + <Grid className="w-4 h-4" />
106 + </button>
107 + <button
108 + onClick={() => setViewMode("list")}
109 + className={`min-w-[40px] min-h-[40px] flex items-center justify-center rounded transition-colors ${viewMode === "list" ? "bg-white dark:bg-white/10 text-gray-900 dark:text-white shadow-sm dark:shadow-none" : "text-gray-400"}`}
110 + >
111 + <List className="w-4 h-4" />
112 + </button>
113 + </div>
114 + <Button
115 + variant={selectMode ? "secondary" : "ghost"}
116 + size="sm"
117 + onClick={() => { setSelectMode(!selectMode); setSelectedIds([]); }}
118 + className="min-w-[40px] min-h-[40px]"
119 + >
120 + <CheckSquare className="w-4 h-4" />
121 + <span className="hidden sm:inline">Sélection</span>
122 + </Button>
123 + <Button variant="ghost" size="sm" onClick={() => setShowCreateFolder(true)} className="min-w-[40px] min-h-[40px]">
124 + <Plus className="w-4 h-4" />
125 + <span className="hidden sm:inline">Dossier</span>
126 + </Button>
127 + <Button size="sm" onClick={() => setShowUpload(!showUpload)} className="min-w-[40px] min-h-[40px]">
128 + <Upload className="w-4 h-4" />
129 + <span className="hidden sm:inline">Upload</span>
130 + </Button>
131 + </div>
132 + </div>
133 +
134 + {/* Bulk Actions Bar */}
135 + <BulkActions selectedIds={selectedIds} onClear={() => { setSelectedIds([]); setSelectMode(false); }} onRefresh={fetchData} />
136 +
137 + {/* Select All */}
138 + {selectMode && files.length > 0 && (
139 + <div className="px-4 lg:px-6 py-2 border-b border-gray-100 dark:border-white/5">
140 + <button onClick={selectAll} className="flex items-center gap-2 text-sm sm:text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 min-h-[40px]">
141 + {selectedIds.length === files.length ? <CheckSquare className="w-4 h-4 sm:w-3.5 sm:h-3.5" /> : <Square className="w-4 h-4 sm:w-3.5 sm:h-3.5" />}
142 + {selectedIds.length === files.length ? "Tout désélectionner" : "Tout sélectionner"}
143 + </button>
144 + </div>
145 + )}
146 +
147 + {/* Upload Zone */}
148 + <AnimatePresence>
149 + {showUpload && (
150 + <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="px-4 lg:px-6 pt-4">
151 + <FileUpload folderId={folderId} onUploadComplete={fetchData} />
152 + </motion.div>
153 + )}
154 + </AnimatePresence>
155 +
156 + {/* Content */}
157 + <div className="flex-1 overflow-auto overflow-x-hidden px-4 lg:px-6 py-4">
158 + {loading ? (
159 + <div className="flex items-center justify-center h-32">
160 + <div className="w-6 h-6 border-2 border-gray-300 dark:border-white/20 border-t-gray-600 dark:border-t-white rounded-full animate-spin" />
161 + </div>
162 + ) : (
163 + <>
164 + {folders.length > 0 && (
165 + <div className="mb-6">
166 + <h3 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">Dossiers</h3>
167 + <div className={viewMode === "grid" ? "grid grid-cols-2 gap-2 sm:grid-cols-3 sm:gap-3 lg:grid-cols-4 xl:grid-cols-5" : "space-y-1"}>
168 + {folders.map((folder) => (
169 + <FolderCard key={folder.id} folder={folder} onRefresh={fetchData} viewMode={viewMode} />
170 + ))}
171 + </div>
172 + </div>
173 + )}
174 +
175 + {files.length > 0 && (
176 + <div>
177 + <h3 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
178 + Fichiers ({files.length})
179 + </h3>
180 + <div className={viewMode === "grid" ? "grid grid-cols-2 gap-2 sm:grid-cols-3 sm:gap-3 lg:grid-cols-4 xl:grid-cols-5" : "space-y-1"}>
181 + {files.map((file) => (
182 + <div key={file.id} className="relative">
183 + {selectMode && (
184 + <button
185 + onClick={(e) => { e.stopPropagation(); toggleSelect(file.id); }}
186 + className="absolute top-2 left-2 z-10 p-1.5 bg-white/80 dark:bg-black/50 rounded-md backdrop-blur-sm min-w-[36px] min-h-[36px] flex items-center justify-center"
187 + >
188 + {selectedIds.includes(file.id) ? (
189 + <CheckSquare className="w-4 h-4 text-blue-500" />
190 + ) : (
191 + <Square className="w-4 h-4 text-gray-400" />
192 + )}
193 + </button>
194 + )}
195 + <div className={selectedIds.includes(file.id) ? "ring-2 ring-blue-500 rounded-xl" : ""}>
196 + <FileCard
197 + file={file}
198 + onPreview={handleFileClick}
199 + onRefresh={fetchData}
200 + viewMode={viewMode}
201 + />
202 + </div>
203 + </div>
204 + ))}
205 + </div>
206 + </div>
207 + )}
208 +
209 + {folders.length === 0 && files.length === 0 && (
210 + <div className="flex flex-col items-center justify-center h-64 text-gray-500">
211 + <Upload className="w-12 h-12 mb-4 text-gray-300 dark:text-gray-600" />
212 + <p className="text-lg">Aucun fichier</p>
213 + <p className="text-sm mt-1">Uploadez vos premiers fichiers</p>
214 + <Button className="mt-4" size="sm" onClick={() => setShowUpload(true)}>
215 + <Upload className="w-4 h-4" /> Upload
216 + </Button>
217 + </div>
218 + )}
219 + </>
220 + )}
221 + </div>
222 + </div>
223 +
224 + {previewFile && (
225 + <div className="fixed inset-0 z-50 lg:relative lg:inset-auto lg:w-[400px] lg:border-l lg:border-gray-200 dark:lg:border-white/10">
226 + <PreviewEngine file={previewFile} files={files} onClose={() => setPreviewFile(null)} onNavigate={setPreviewFile} />
227 + </div>
228 + )}
229 +
230 + <CreateFolderModal isOpen={showCreateFolder} onClose={() => setShowCreateFolder(false)} parentId={folderId || null} onCreated={fetchData} />
231 + </div>
232 + );
233 +}
added components/files/FilePreview.tsx +281 −0
@@ -0,0 +1,281 @@
1 +"use client";
2 +
3 +import { useState, useEffect } from "react";
4 +import { motion, AnimatePresence } from "framer-motion";
5 +import { X, Download, ChevronLeft, ChevronRight, Maximize2, Minimize2 } from "lucide-react";
6 +import { isPreviewable, getLanguageFromMime, formatFileSize, formatDate } from "@/lib/utils";
7 +import dynamic from "next/dynamic";
8 +
9 +const SyntaxHighlighter = dynamic(
10 + () => import("react-syntax-highlighter").then((mod) => mod.Light),
11 + { ssr: false }
12 +);
13 +
14 +interface FileData {
15 + id: string;
16 + name: string;
17 + size: number;
18 + mimeType: string;
19 + createdAt: string;
20 +}
21 +
22 +interface FilePreviewProps {
23 + file: FileData | null;
24 + files?: FileData[];
25 + onClose: () => void;
26 + onNavigate?: (file: FileData) => void;
27 +}
28 +
29 +export default function FilePreview({ file, files = [], onClose, onNavigate }: FilePreviewProps) {
30 + const [textContent, setTextContent] = useState<string>("");
31 + const [fullscreen, setFullscreen] = useState(false);
32 + const [loading, setLoading] = useState(false);
33 +
34 + const currentIndex = files.findIndex((f) => f.id === file?.id);
35 +
36 + useEffect(() => {
37 + if (!file) return;
38 +
39 + const isText =
40 + file.mimeType.startsWith("text/") ||
41 + file.mimeType.includes("json") ||
42 + file.mimeType.includes("xml") ||
43 + file.mimeType.includes("javascript") ||
44 + file.mimeType.includes("typescript") ||
45 + file.mimeType.includes("markdown");
46 +
47 + if (isText) {
48 + setLoading(true);
49 + fetch(`/api/files/${file.id}/preview`)
50 + .then((res) => res.text())
51 + .then((text) => {
52 + setTextContent(text);
53 + setLoading(false);
54 + })
55 + .catch(() => setLoading(false));
56 + }
57 +
58 + if (file.mimeType === "text/csv") {
59 + setLoading(true);
60 + fetch(`/api/files/${file.id}/preview`)
61 + .then((res) => res.text())
62 + .then((text) => {
63 + setTextContent(text);
64 + setLoading(false);
65 + })
66 + .catch(() => setLoading(false));
67 + }
68 + }, [file]);
69 +
70 + if (!file) return null;
71 +
72 + const handlePrev = () => {
73 + if (currentIndex > 0 && onNavigate) onNavigate(files[currentIndex - 1]);
74 + };
75 +
76 + const handleNext = () => {
77 + if (currentIndex < files.length - 1 && onNavigate) onNavigate(files[currentIndex + 1]);
78 + };
79 +
80 + const renderPreview = () => {
81 + if (!isPreviewable(file.mimeType)) {
82 + return (
83 + <div className="flex flex-col items-center justify-center h-full text-gray-400">
84 + <p className="text-lg mb-2">Aperçu non disponible</p>
85 + <p className="text-sm text-gray-500">{file.mimeType}</p>
86 + <button
87 + onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}
88 + className="mt-4 px-4 py-2 bg-white/10 rounded-lg text-sm hover:bg-white/20 transition-colors"
89 + >
90 + Télécharger
91 + </button>
92 + </div>
93 + );
94 + }
95 +
96 + if (file.mimeType.startsWith("image/")) {
97 + return (
98 + <div className="flex items-center justify-center h-full">
99 + <img
100 + src={`/api/files/${file.id}/preview`}
101 + alt={file.name}
102 + className="max-w-full max-h-full object-contain"
103 + />
104 + </div>
105 + );
106 + }
107 +
108 + if (file.mimeType.startsWith("video/")) {
109 + return (
110 + <div className="flex items-center justify-center h-full">
111 + <video
112 + src={`/api/files/${file.id}/preview`}
113 + controls
114 + className="max-w-full max-h-full"
115 + >
116 + Your browser does not support video.
117 + </video>
118 + </div>
119 + );
120 + }
121 +
122 + if (file.mimeType.startsWith("audio/")) {
123 + return (
124 + <div className="flex items-center justify-center h-full">
125 + <audio src={`/api/files/${file.id}/preview`} controls className="w-full max-w-md">
126 + Your browser does not support audio.
127 + </audio>
128 + </div>
129 + );
130 + }
131 +
132 + if (file.mimeType === "application/pdf") {
133 + return (
134 + <iframe
135 + src={`/api/files/${file.id}/preview`}
136 + className="w-full h-full border-0"
137 + title={file.name}
138 + />
139 + );
140 + }
141 +
142 + if (file.mimeType === "text/csv") {
143 + if (loading) return <div className="text-gray-400 p-4">Chargement...</div>;
144 + const rows = textContent.split("\n").map((row) => row.split(","));
145 + return (
146 + <div className="overflow-auto h-full p-4">
147 + <table className="w-full text-sm border-collapse">
148 + <thead>
149 + <tr>
150 + {rows[0]?.map((header, i) => (
151 + <th key={i} className="border border-white/10 px-3 py-2 text-left text-gray-300 bg-white/5">
152 + {header}
153 + </th>
154 + ))}
155 + </tr>
156 + </thead>
157 + <tbody>
158 + {rows.slice(1).map((row, i) => (
159 + <tr key={i}>
160 + {row.map((cell, j) => (
161 + <td key={j} className="border border-white/10 px-3 py-2 text-gray-400">
162 + {cell}
163 + </td>
164 + ))}
165 + </tr>
166 + ))}
167 + </tbody>
168 + </table>
169 + </div>
170 + );
171 + }
172 +
173 + // Text / Code / Markdown
174 + if (loading) return <div className="text-gray-400 p-4">Chargement...</div>;
175 +
176 + const isCode =
177 + file.mimeType.includes("javascript") ||
178 + file.mimeType.includes("typescript") ||
179 + file.mimeType.includes("json") ||
180 + file.mimeType.includes("xml") ||
181 + file.mimeType.includes("css") ||
182 + file.mimeType.includes("html");
183 +
184 + if (isCode) {
185 + const language = getLanguageFromMime(file.mimeType);
186 + return (
187 + <div className="overflow-auto h-full p-4">
188 + <SyntaxHighlighter
189 + language={language}
190 + customStyle={{
191 + background: "transparent",
192 + padding: 0,
193 + margin: 0,
194 + fontSize: "0.875rem",
195 + }}
196 + >
197 + {textContent}
198 + </SyntaxHighlighter>
199 + </div>
200 + );
201 + }
202 +
203 + return (
204 + <div className="overflow-auto h-full p-6">
205 + <pre className="text-sm text-gray-300 whitespace-pre-wrap font-mono">
206 + {textContent}
207 + </pre>
208 + </div>
209 + );
210 + };
211 +
212 + return (
213 + <AnimatePresence>
214 + <motion.div
215 + initial={{ opacity: 0 }}
216 + animate={{ opacity: 1 }}
217 + exit={{ opacity: 0 }}
218 + className={`fixed inset-0 z-50 flex flex-col bg-[#0a0a0a] ${
219 + fullscreen ? "" : "lg:relative lg:inset-auto lg:h-full"
220 + }`}
221 + >
222 + {/* Header */}
223 + <div className="flex items-center justify-between px-4 py-3 border-b border-white/10 flex-shrink-0">
224 + <div className="flex-1 min-w-0">
225 + <h3 className="text-sm font-medium text-white truncate">{file.name}</h3>
226 + <p className="text-xs text-gray-500">
227 + {formatFileSize(file.size)} - {formatDate(file.createdAt)}
228 + </p>
229 + </div>
230 + <div className="flex items-center gap-1">
231 + <button
232 + onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}
233 + className="p-2 text-gray-400 hover:text-white transition-colors"
234 + >
235 + <Download className="w-4 h-4" />
236 + </button>
237 + <button
238 + onClick={() => setFullscreen(!fullscreen)}
239 + className="p-2 text-gray-400 hover:text-white transition-colors hidden lg:block"
240 + >
241 + {fullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
242 + </button>
243 + <button
244 + onClick={onClose}
245 + className="p-2 text-gray-400 hover:text-white transition-colors"
246 + >
247 + <X className="w-4 h-4" />
248 + </button>
249 + </div>
250 + </div>
251 +
252 + {/* Preview Content */}
253 + <div className="flex-1 relative overflow-hidden">
254 + {renderPreview()}
255 +
256 + {/* Navigation arrows */}
257 + {files.length > 1 && (
258 + <>
259 + {currentIndex > 0 && (
260 + <button
261 + onClick={handlePrev}
262 + className="absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 rounded-full text-white hover:bg-black/70 transition-colors backdrop-blur-sm"
263 + >
264 + <ChevronLeft className="w-5 h-5" />
265 + </button>
266 + )}
267 + {currentIndex < files.length - 1 && (
268 + <button
269 + onClick={handleNext}
270 + className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 rounded-full text-white hover:bg-black/70 transition-colors backdrop-blur-sm"
271 + >
272 + <ChevronRight className="w-5 h-5" />
273 + </button>
274 + )}
275 + </>
276 + )}
277 + </div>
278 + </motion.div>
279 + </AnimatePresence>
280 + );
281 +}
added components/files/FileUpload.tsx +149 −0
@@ -0,0 +1,149 @@
1 +"use client";
2 +
3 +import { useCallback, useState } from "react";
4 +import { useDropzone } from "react-dropzone";
5 +import { motion, AnimatePresence } from "framer-motion";
6 +import { Upload, CheckCircle, AlertCircle } from "lucide-react";
7 +import { formatFileSize } from "@/lib/utils";
8 +
9 +interface UploadItem {
10 + file: File;
11 + progress: number;
12 + status: "uploading" | "done" | "error";
13 + error?: string;
14 +}
15 +
16 +interface FileUploadProps {
17 + folderId?: string | null;
18 + onUploadComplete: () => void;
19 +}
20 +
21 +export default function FileUpload({ folderId, onUploadComplete }: FileUploadProps) {
22 + const [uploads, setUploads] = useState<UploadItem[]>([]);
23 +
24 + const uploadFile = useCallback(
25 + async (file: File) => {
26 + const formData = new FormData();
27 + formData.append("file", file);
28 + if (folderId) formData.append("folderId", folderId);
29 +
30 + setUploads((prev) => [
31 + ...prev,
32 + { file, progress: 0, status: "uploading" },
33 + ]);
34 +
35 + try {
36 + const res = await fetch("/api/files", {
37 + method: "POST",
38 + body: formData,
39 + });
40 +
41 + if (!res.ok) {
42 + const data = await res.json();
43 + throw new Error(data.error || "Upload failed");
44 + }
45 +
46 + setUploads((prev) =>
47 + prev.map((u) =>
48 + u.file === file ? { ...u, progress: 100, status: "done" } : u
49 + )
50 + );
51 + onUploadComplete();
52 + } catch (err) {
53 + setUploads((prev) =>
54 + prev.map((u) =>
55 + u.file === file
56 + ? { ...u, status: "error", error: (err as Error).message }
57 + : u
58 + )
59 + );
60 + }
61 + },
62 + [folderId, onUploadComplete]
63 + );
64 +
65 + const onDrop = useCallback(
66 + (acceptedFiles: File[]) => {
67 + acceptedFiles.forEach((file) => uploadFile(file));
68 + },
69 + [uploadFile]
70 + );
71 +
72 + const { getRootProps, getInputProps, isDragActive } = useDropzone({
73 + onDrop,
74 + noClick: false,
75 + });
76 +
77 + const clearUploads = () => setUploads([]);
78 +
79 + return (
80 + <div>
81 + <div
82 + {...getRootProps()}
83 + className={`border-2 border-dashed rounded-xl p-6 sm:p-8 text-center cursor-pointer transition-all ${
84 + isDragActive
85 + ? "border-blue-500 bg-blue-50 dark:bg-blue-500/5"
86 + : "border-gray-300 dark:border-white/10 hover:border-gray-400 dark:hover:border-white/20"
87 + }`}
88 + >
89 + <input {...getInputProps()} />
90 + <Upload className="w-8 h-8 mx-auto mb-3 text-gray-400 dark:text-gray-500" />
91 + <p className="text-sm text-gray-500 dark:text-gray-400">
92 + {isDragActive
93 + ? "Déposez les fichiers ici..."
94 + : (
95 + <>
96 + <span className="hidden sm:inline">Glissez-déposez vos fichiers ou cliquez pour sélectionner</span>
97 + <span className="sm:hidden">Tapez pour sélectionner des fichiers</span>
98 + </>
99 + )}
100 + </p>
101 + <p className="text-xs text-gray-400 dark:text-gray-600 mt-1">
102 + Max {formatFileSize(parseInt(process.env.NEXT_PUBLIC_MAX_FILE_SIZE || "104857600"))}
103 + </p>
104 + </div>
105 +
106 + <AnimatePresence>
107 + {uploads.length > 0 && (
108 + <motion.div
109 + initial={{ opacity: 0, height: 0 }}
110 + animate={{ opacity: 1, height: "auto" }}
111 + exit={{ opacity: 0, height: 0 }}
112 + className="mt-3 space-y-2"
113 + >
114 + <div className="flex items-center justify-between">
115 + <span className="text-xs text-gray-500">
116 + {uploads.filter((u) => u.status === "done").length}/{uploads.length} terminé
117 + </span>
118 + <button onClick={clearUploads} className="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 min-h-[36px] px-2">
119 + Effacer
120 + </button>
121 + </div>
122 + {uploads.map((upload, i) => (
123 + <div
124 + key={i}
125 + className="flex items-center gap-3 bg-gray-100 dark:bg-white/5 rounded-lg px-3 py-3 sm:py-2"
126 + >
127 + {upload.status === "uploading" && (
128 + <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin flex-shrink-0" />
129 + )}
130 + {upload.status === "done" && (
131 + <CheckCircle className="w-4 h-4 text-green-500 dark:text-green-400 flex-shrink-0" />
132 + )}
133 + {upload.status === "error" && (
134 + <AlertCircle className="w-4 h-4 text-red-500 dark:text-red-400 flex-shrink-0" />
135 + )}
136 + <span className="text-sm text-gray-600 dark:text-gray-300 truncate flex-1">
137 + {upload.file.name}
138 + </span>
139 + <span className="text-xs text-gray-500 flex-shrink-0">
140 + {formatFileSize(upload.file.size)}
141 + </span>
142 + </div>
143 + ))}
144 + </motion.div>
145 + )}
146 + </AnimatePresence>
147 + </div>
148 + );
149 +}
added components/folders/CreateFolderModal.tsx +75 −0
@@ -0,0 +1,75 @@
1 +"use client";
2 +
3 +import { useState } from "react";
4 +import Modal from "../ui/Modal";
5 +import Button from "../ui/Button";
6 +import toast from "react-hot-toast";
7 +
8 +interface CreateFolderModalProps {
9 + isOpen: boolean;
10 + onClose: () => void;
11 + parentId: string | null;
12 + onCreated: () => void;
13 +}
14 +
15 +export default function CreateFolderModal({
16 + isOpen,
17 + onClose,
18 + parentId,
19 + onCreated,
20 +}: CreateFolderModalProps) {
21 + const [name, setName] = useState("");
22 + const [loading, setLoading] = useState(false);
23 +
24 + const handleCreate = async () => {
25 + if (!name.trim()) return;
26 + setLoading(true);
27 +
28 + try {
29 + const res = await fetch("/api/folders", {
30 + method: "POST",
31 + headers: { "Content-Type": "application/json" },
32 + body: JSON.stringify({
33 + name: name.trim(),
34 + parentId: parentId || "root",
35 + }),
36 + });
37 +
38 + if (res.ok) {
39 + toast.success("Dossier créé");
40 + setName("");
41 + onCreated();
42 + onClose();
43 + } else {
44 + const data = await res.json();
45 + toast.error(data.error || "Erreur");
46 + }
47 + } catch {
48 + toast.error("Erreur réseau");
49 + } finally {
50 + setLoading(false);
51 + }
52 + };
53 +
54 + return (
55 + <Modal isOpen={isOpen} onClose={onClose} title="Nouveau dossier">
56 + <input
57 + type="text"
58 + value={name}
59 + onChange={(e) => setName(e.target.value)}
60 + placeholder="Nom du dossier"
61 + className="w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-4 py-2.5 text-gray-900 dark:text-white text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:border-gray-400 dark:focus:border-white/20 mb-4"
62 + autoFocus
63 + onKeyDown={(e) => e.key === "Enter" && handleCreate()}
64 + />
65 + <div className="flex gap-2 justify-end">
66 + <Button variant="ghost" onClick={onClose}>
67 + Annuler
68 + </Button>
69 + <Button onClick={handleCreate} loading={loading} disabled={!name.trim()}>
70 + Créer
71 + </Button>
72 + </div>
73 + </Modal>
74 + );
75 +}
added components/folders/FolderCard.tsx +225 −0
@@ -0,0 +1,225 @@
1 +"use client";
2 +
3 +import { useRouter } from "next/navigation";
4 +import { motion } from "framer-motion";
5 +import { Folder, MoreVertical, Pencil, Trash2 } from "lucide-react";
6 +import { useState, useRef, useEffect } from "react";
7 +import toast from "react-hot-toast";
8 +import Modal from "../ui/Modal";
9 +import Button from "../ui/Button";
10 +
11 +interface FolderData {
12 + id: string;
13 + name: string;
14 + _count?: { files: number; children: number };
15 +}
16 +
17 +interface FolderCardProps {
18 + folder: FolderData;
19 + onRefresh: () => void;
20 + viewMode: "grid" | "list";
21 +}
22 +
23 +export default function FolderCard({ folder, onRefresh, viewMode }: FolderCardProps) {
24 + const router = useRouter();
25 + const [showMenu, setShowMenu] = useState(false);
26 + const [renameModal, setRenameModal] = useState(false);
27 + const [deleteModal, setDeleteModal] = useState(false);
28 + const [newName, setNewName] = useState(folder.name);
29 + const [loading, setLoading] = useState(false);
30 + const menuRef = useRef<HTMLDivElement>(null);
31 +
32 + useEffect(() => {
33 + function handleClick(e: MouseEvent) {
34 + if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
35 + setShowMenu(false);
36 + }
37 + }
38 + document.addEventListener("mousedown", handleClick);
39 + return () => document.removeEventListener("mousedown", handleClick);
40 + }, []);
41 +
42 + const handleRename = async () => {
43 + setLoading(true);
44 + try {
45 + const res = await fetch(`/api/folders/${folder.id}`, {
46 + method: "PATCH",
47 + headers: { "Content-Type": "application/json" },
48 + body: JSON.stringify({ name: newName }),
49 + });
50 + if (res.ok) {
51 + toast.success("Dossier renommé");
52 + onRefresh();
53 + }
54 + } catch {
55 + toast.error("Erreur");
56 + } finally {
57 + setLoading(false);
58 + setRenameModal(false);
59 + }
60 + };
61 +
62 + const handleDelete = async () => {
63 + setLoading(true);
64 + try {
65 + const res = await fetch(`/api/folders/${folder.id}`, {
66 + method: "DELETE",
67 + });
68 + if (res.ok) {
69 + toast.success("Dossier supprimé");
70 + onRefresh();
71 + }
72 + } catch {
73 + toast.error("Erreur");
74 + } finally {
75 + setLoading(false);
76 + setDeleteModal(false);
77 + }
78 + };
79 +
80 + const inputClasses = "w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-4 py-3 text-gray-900 dark:text-white text-sm focus:outline-none mb-4";
81 +
82 + if (viewMode === "list") {
83 + return (
84 + <>
85 + <motion.div
86 + initial={{ opacity: 0 }}
87 + animate={{ opacity: 1 }}
88 + className="flex items-center gap-3 px-3 py-3.5 hover:bg-gray-100 dark:hover:bg-white/5 rounded-lg cursor-pointer group transition-colors"
89 + onClick={() => router.push(`/dashboard/folder/${folder.id}`)}
90 + >
91 + <Folder className="w-5 h-5 text-blue-400 flex-shrink-0" />
92 + <span className="text-sm text-gray-700 dark:text-gray-200 flex-1 truncate">{folder.name}</span>
93 + {folder._count && (
94 + <span className="text-xs text-gray-500">
95 + {folder._count.files} fichiers
96 + </span>
97 + )}
98 + <div className="relative" ref={menuRef}>
99 + <button
100 + onClick={(e) => {
101 + e.stopPropagation();
102 + setShowMenu(!showMenu);
103 + }}
104 + className="min-w-[44px] min-h-[44px] flex items-center justify-center text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
105 + >
106 + <MoreVertical className="w-4 h-4" />
107 + </button>
108 + {showMenu && (
109 + <div className="absolute right-0 top-full mt-1 w-40 bg-white dark:bg-gray-900 border border-gray-200 dark:border-white/10 rounded-xl shadow-2xl z-50 py-1 max-h-[70vh] overflow-auto">
110 + <button
111 + onClick={(e) => {
112 + e.stopPropagation();
113 + setRenameModal(true);
114 + setShowMenu(false);
115 + }}
116 + className="flex items-center gap-2 w-full px-4 py-3 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5"
117 + >
118 + <Pencil className="w-4 h-4" /> Renommer
119 + </button>
120 + <button
121 + onClick={(e) => {
122 + e.stopPropagation();
123 + setDeleteModal(true);
124 + setShowMenu(false);
125 + }}
126 + className="flex items-center gap-2 w-full px-4 py-3 text-sm text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10"
127 + >
128 + <Trash2 className="w-4 h-4" /> Supprimer
129 + </button>
130 + </div>
131 + )}
132 + </div>
133 + </motion.div>
134 + <Modal isOpen={renameModal} onClose={() => setRenameModal(false)} title="Renommer le dossier">
135 + <input type="text" value={newName} onChange={(e) => setNewName(e.target.value)} className={inputClasses} autoFocus />
136 + <div className="flex gap-2 justify-end">
137 + <Button variant="ghost" onClick={() => setRenameModal(false)}>Annuler</Button>
138 + <Button onClick={handleRename} loading={loading}>Renommer</Button>
139 + </div>
140 + </Modal>
141 + <Modal isOpen={deleteModal} onClose={() => setDeleteModal(false)} title="Supprimer le dossier">
142 + <p className="text-gray-500 dark:text-gray-400 text-sm mb-4">Supprimer <strong className="text-gray-900 dark:text-white">{folder.name}</strong> et tout son contenu ?</p>
143 + <div className="flex gap-2 justify-end">
144 + <Button variant="ghost" onClick={() => setDeleteModal(false)}>Annuler</Button>
145 + <Button variant="danger" onClick={handleDelete} loading={loading}>Supprimer</Button>
146 + </div>
147 + </Modal>
148 + </>
149 + );
150 + }
151 +
152 + return (
153 + <>
154 + <motion.div
155 + initial={{ opacity: 0, scale: 0.95 }}
156 + animate={{ opacity: 1, scale: 1 }}
157 + className="group bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl p-4 hover:border-blue-300 dark:hover:border-blue-500/30 transition-colors cursor-pointer relative shadow-sm dark:shadow-none min-h-[72px]"
158 + onClick={() => router.push(`/dashboard/folder/${folder.id}`)}
159 + >
160 + <div className="flex items-center gap-3">
161 + <Folder className="w-8 h-8 text-blue-400" />
162 + <div className="flex-1 min-w-0">
163 + <p className="text-sm text-gray-700 dark:text-gray-200 truncate">{folder.name}</p>
164 + {folder._count && (
165 + <p className="text-xs text-gray-500">
166 + {folder._count.files} fichiers, {folder._count.children} dossiers
167 + </p>
168 + )}
169 + </div>
170 + </div>
171 +
172 + <div className="absolute top-2 right-2" ref={menuRef}>
173 + <button
174 + onClick={(e) => {
175 + e.stopPropagation();
176 + setShowMenu(!showMenu);
177 + }}
178 + className="p-2 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white sm:opacity-0 sm:group-hover:opacity-100 transition-opacity min-w-[40px] min-h-[40px] flex items-center justify-center"
179 + >
180 + <MoreVertical className="w-4 h-4" />
181 + </button>
182 + {showMenu && (
183 + <div className="absolute right-0 top-full mt-1 w-40 bg-white dark:bg-gray-900 border border-gray-200 dark:border-white/10 rounded-xl shadow-2xl z-50 py-1 max-h-[70vh] overflow-auto">
184 + <button
185 + onClick={(e) => {
186 + e.stopPropagation();
187 + setRenameModal(true);
188 + setShowMenu(false);
189 + }}
190 + className="flex items-center gap-2 w-full px-4 py-3 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5"
191 + >
192 + <Pencil className="w-4 h-4" /> Renommer
193 + </button>
194 + <button
195 + onClick={(e) => {
196 + e.stopPropagation();
197 + setDeleteModal(true);
198 + setShowMenu(false);
199 + }}
200 + className="flex items-center gap-2 w-full px-4 py-3 text-sm text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10"
201 + >
202 + <Trash2 className="w-4 h-4" /> Supprimer
203 + </button>
204 + </div>
205 + )}
206 + </div>
207 + </motion.div>
208 +
209 + <Modal isOpen={renameModal} onClose={() => setRenameModal(false)} title="Renommer le dossier">
210 + <input type="text" value={newName} onChange={(e) => setNewName(e.target.value)} className={inputClasses} autoFocus />
211 + <div className="flex gap-2 justify-end">
212 + <Button variant="ghost" onClick={() => setRenameModal(false)}>Annuler</Button>
213 + <Button onClick={handleRename} loading={loading}>Renommer</Button>
214 + </div>
215 + </Modal>
216 + <Modal isOpen={deleteModal} onClose={() => setDeleteModal(false)} title="Supprimer le dossier">
217 + <p className="text-gray-500 dark:text-gray-400 text-sm mb-4">Supprimer <strong className="text-gray-900 dark:text-white">{folder.name}</strong> et tout son contenu ?</p>
218 + <div className="flex gap-2 justify-end">
219 + <Button variant="ghost" onClick={() => setDeleteModal(false)}>Annuler</Button>
220 + <Button variant="danger" onClick={handleDelete} loading={loading}>Supprimer</Button>
221 + </div>
222 + </Modal>
223 + </>
224 + );
225 +}
added components/layout/AppShell.tsx +56 −0
@@ -0,0 +1,56 @@
1 +"use client";
2 +
3 +import { useState } from "react";
4 +import Sidebar from "./Sidebar";
5 +import MobileNav from "./MobileNav";
6 +import ThemeToggle from "../theme/ThemeToggle";
7 +import NotificationBell from "./NotificationBell";
8 +import AiPanel from "../ai/AiPanel";
9 +
10 +interface AppShellProps {
11 + children: React.ReactNode;
12 +}
13 +
14 +export default function AppShell({ children }: AppShellProps) {
15 + const [sidebarOpen, setSidebarOpen] = useState(false);
16 +
17 + return (
18 + <div className="min-h-screen bg-gray-50 dark:bg-[#0a0a0a] flex transition-colors">
19 + {/* Desktop Sidebar */}
20 + <div className="hidden lg:block w-64 flex-shrink-0">
21 + <Sidebar />
22 + </div>
23 +
24 + {/* Mobile Sidebar Overlay */}
25 + <MobileNav isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
26 +
27 + {/* Main Content */}
28 + <div className="flex-1 flex flex-col min-h-screen min-w-0">
29 + {/* Mobile Header - safe area + larger touch targets */}
30 + <div className="lg:hidden flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10 bg-white dark:bg-[#0a0a0a] transition-colors safe-top">
31 + <button
32 + onClick={() => setSidebarOpen(true)}
33 + className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white p-3 -ml-1 min-w-[44px] min-h-[44px] flex items-center justify-center active:bg-gray-100 dark:active:bg-white/10 rounded-xl transition-colors"
34 + >
35 + <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
36 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
37 + </svg>
38 + </button>
39 + <span className="text-gray-900 dark:text-white font-semibold text-base">SPB Cloud</span>
40 + <div className="flex items-center gap-0">
41 + <NotificationBell />
42 + <ThemeToggle />
43 + </div>
44 + </div>
45 +
46 + {/* Content - with safe area bottom for notched devices */}
47 + <main className="flex-1 overflow-auto pb-safe">
48 + {children}
49 + </main>
50 + </div>
51 +
52 + {/* AI Assistant */}
53 + <AiPanel />
54 + </div>
55 + );
56 +}
added components/layout/Breadcrumb.tsx +38 −0
@@ -0,0 +1,38 @@
1 +"use client";
2 +
3 +import Link from "next/link";
4 +import { ChevronRight, Home } from "lucide-react";
5 +
6 +interface BreadcrumbItem {
7 + id: string;
8 + name: string;
9 +}
10 +
11 +interface BreadcrumbProps {
12 + items: BreadcrumbItem[];
13 +}
14 +
15 +export default function Breadcrumb({ items }: BreadcrumbProps) {
16 + return (
17 + <nav className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400 overflow-x-auto pb-1 flex-nowrap scrollbar-none">
18 + <Link
19 + href="/dashboard"
20 + className="flex items-center gap-1 hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap py-1 flex-shrink-0"
21 + >
22 + <Home className="w-3.5 h-3.5" />
23 + <span>Fichiers</span>
24 + </Link>
25 + {items.map((item) => (
26 + <span key={item.id} className="flex items-center gap-1 flex-shrink-0">
27 + <ChevronRight className="w-3.5 h-3.5 text-gray-400 dark:text-gray-600 flex-shrink-0" />
28 + <Link
29 + href={`/dashboard/folder/${item.id}`}
30 + className="hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap py-1"
31 + >
32 + {item.name}
33 + </Link>
34 + </span>
35 + ))}
36 + </nav>
37 + );
38 +}
added components/layout/MobileNav.tsx +36 −0
@@ -0,0 +1,36 @@
1 +"use client";
2 +
3 +import { motion, AnimatePresence } from "framer-motion";
4 +import Sidebar from "./Sidebar";
5 +
6 +interface MobileNavProps {
7 + isOpen: boolean;
8 + onClose: () => void;
9 +}
10 +
11 +export default function MobileNav({ isOpen, onClose }: MobileNavProps) {
12 + return (
13 + <AnimatePresence>
14 + {isOpen && (
15 + <>
16 + <motion.div
17 + initial={{ opacity: 0 }}
18 + animate={{ opacity: 1 }}
19 + exit={{ opacity: 0 }}
20 + className="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 lg:hidden"
21 + onClick={onClose}
22 + />
23 + <motion.div
24 + initial={{ x: -300 }}
25 + animate={{ x: 0 }}
26 + exit={{ x: -300 }}
27 + transition={{ type: "spring", damping: 28, stiffness: 300 }}
28 + className="fixed left-0 top-0 bottom-0 w-[280px] max-w-[85vw] z-50 lg:hidden shadow-2xl"
29 + >
30 + <Sidebar onNavigate={onClose} />
31 + </motion.div>
32 + </>
33 + )}
34 + </AnimatePresence>
35 + );
36 +}
added components/layout/NotificationBell.tsx +145 −0
@@ -0,0 +1,145 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useRef } from "react";
4 +import { Bell, Check, CheckCheck } from "lucide-react";
5 +import { motion, AnimatePresence } from "framer-motion";
6 +import { formatDate } from "@/lib/utils";
7 +
8 +interface NotificationData {
9 + id: string;
10 + type: string;
11 + title: string;
12 + message: string;
13 + read: boolean;
14 + createdAt: string;
15 +}
16 +
17 +export default function NotificationBell() {
18 + const [notifications, setNotifications] = useState<NotificationData[]>([]);
19 + const [unreadCount, setUnreadCount] = useState(0);
20 + const [open, setOpen] = useState(false);
21 + const ref = useRef<HTMLDivElement>(null);
22 +
23 + const fetchCount = async () => {
24 + try {
25 + const res = await fetch("/api/notifications?count=true");
26 + if (res.ok) {
27 + const data = await res.json();
28 + setUnreadCount(data.count);
29 + }
30 + } catch {}
31 + };
32 +
33 + const fetchNotifications = async () => {
34 + try {
35 + const res = await fetch("/api/notifications");
36 + if (res.ok) setNotifications(await res.json());
37 + } catch {}
38 + };
39 +
40 + useEffect(() => {
41 + fetchCount();
42 + const interval = setInterval(fetchCount, 30000);
43 + return () => clearInterval(interval);
44 + }, []);
45 +
46 + useEffect(() => {
47 + if (open) fetchNotifications();
48 + }, [open]);
49 +
50 + useEffect(() => {
51 + function handleClick(e: MouseEvent) {
52 + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
53 + }
54 + document.addEventListener("mousedown", handleClick);
55 + return () => document.removeEventListener("mousedown", handleClick);
56 + }, []);
57 +
58 + const markAllRead = async () => {
59 + await fetch("/api/notifications", {
60 + method: "POST",
61 + headers: { "Content-Type": "application/json" },
62 + body: JSON.stringify({ action: "mark_all_read" }),
63 + });
64 + setUnreadCount(0);
65 + setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
66 + };
67 +
68 + const markRead = async (id: string) => {
69 + await fetch(`/api/notifications/${id}`, { method: "PATCH" });
70 + setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, read: true } : n));
71 + setUnreadCount((prev) => Math.max(0, prev - 1));
72 + };
73 +
74 + const typeColors: Record<string, string> = {
75 + upload_complete: "text-green-500",
76 + backup_complete: "text-blue-500",
77 + ai_complete: "text-purple-500",
78 + share_accessed: "text-orange-500",
79 + };
80 +
81 + return (
82 + <div className="relative" ref={ref}>
83 + <button
84 + onClick={() => setOpen(!open)}
85 + className="relative min-w-[44px] min-h-[44px] p-2.5 flex items-center justify-center text-gray-400 hover:text-gray-700 dark:hover:text-white transition-colors"
86 + >
87 + <Bell className="w-5 h-5" />
88 + {unreadCount > 0 && (
89 + <span className="absolute top-1 right-1 w-4 h-4 bg-red-500 text-white text-xs font-bold rounded-full flex items-center justify-center">
90 + {unreadCount > 9 ? "9+" : unreadCount}
91 + </span>
92 + )}
93 + </button>
94 +
95 + <AnimatePresence>
96 + {open && (
97 + <motion.div
98 + initial={{ opacity: 0, y: 5, scale: 0.95 }}
99 + animate={{ opacity: 1, y: 0, scale: 1 }}
100 + exit={{ opacity: 0, y: 5, scale: 0.95 }}
101 + className="fixed inset-x-4 top-16 sm:absolute sm:inset-x-auto sm:top-full sm:right-0 sm:mt-2 sm:w-80 bg-white dark:bg-gray-900 border border-gray-200 dark:border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden"
102 + >
103 + <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-white/10">
104 + <h3 className="text-sm font-medium text-gray-900 dark:text-white">Notifications</h3>
105 + {unreadCount > 0 && (
106 + <button onClick={markAllRead} className="text-xs text-blue-500 hover:text-blue-600 flex items-center gap-1 min-h-[36px]">
107 + <CheckCheck className="w-3 h-3" /> Tout lire
108 + </button>
109 + )}
110 + </div>
111 + <div className="max-h-80 overflow-y-auto">
112 + {notifications.length === 0 ? (
113 + <p className="text-center text-gray-500 text-sm py-8">Aucune notification</p>
114 + ) : (
115 + notifications.map((n) => (
116 + <div
117 + key={n.id}
118 + className={`px-4 py-3.5 border-b border-gray-100 dark:border-white/5 hover:bg-gray-50 dark:hover:bg-white/5 transition-colors cursor-pointer ${!n.read ? "bg-blue-50/50 dark:bg-blue-500/5" : ""}`}
119 + onClick={() => !n.read && markRead(n.id)}
120 + >
121 + <div className="flex items-start gap-2">
122 + <div className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${n.read ? "bg-transparent" : "bg-blue-500"}`} />
123 + <div className="flex-1 min-w-0">
124 + <p className={`text-sm font-medium ${typeColors[n.type] || "text-gray-900 dark:text-white"}`}>
125 + {n.title}
126 + </p>
127 + <p className="text-xs text-gray-500 mt-0.5">{n.message}</p>
128 + <p className="text-xs text-gray-400 mt-1">{formatDate(n.createdAt)}</p>
129 + </div>
130 + {!n.read && (
131 + <button className="p-1.5 text-gray-400 hover:text-green-500 min-w-[32px] min-h-[32px] flex items-center justify-center" title="Marquer comme lu">
132 + <Check className="w-3.5 h-3.5" />
133 + </button>
134 + )}
135 + </div>
136 + </div>
137 + ))
138 + )}
139 + </div>
140 + </motion.div>
141 + )}
142 + </AnimatePresence>
143 + </div>
144 + );
145 +}
added components/layout/Sidebar.tsx +188 −0
@@ -0,0 +1,188 @@
1 +"use client";
2 +
3 +import Link from "next/link";
4 +import { usePathname, useRouter } from "next/navigation";
5 +import { useState, useEffect } from "react";
6 +import {
7 + Cloud,
8 + FolderOpen,
9 + LayoutDashboard,
10 + Share2,
11 + Activity,
12 + LogOut,
13 + Star,
14 + Tag,
15 + ChevronDown,
16 + ChevronRight,
17 + Users,
18 + HardDrive,
19 + Key,
20 + Settings,
21 +} from "lucide-react";
22 +import ThemeToggle from "../theme/ThemeToggle";
23 +import NotificationBell from "./NotificationBell";
24 +
25 +const navItems = [
26 + { href: "/dashboard", label: "Fichiers", icon: FolderOpen },
27 + { href: "/dashboard/favorites", label: "Favoris", icon: Star },
28 + { href: "/admin", label: "Dashboard", icon: LayoutDashboard },
29 + { href: "/admin/shares", label: "Liens partagés", icon: Share2 },
30 + { href: "/admin/activity", label: "Activité", icon: Activity },
31 +];
32 +
33 +const adminItems = [
34 + { href: "/admin/users", label: "Utilisateurs", icon: Users },
35 + { href: "/admin/backup", label: "Backups", icon: HardDrive },
36 + { href: "/admin/api-keys", label: "Clés API", icon: Key },
37 + { href: "/admin/settings", label: "Paramètres", icon: Settings },
38 +];
39 +
40 +interface TagData {
41 + id: string;
42 + name: string;
43 + color: string;
44 + _count?: { files: number };
45 +}
46 +
47 +interface SidebarProps {
48 + onNavigate?: () => void;
49 +}
50 +
51 +export default function Sidebar({ onNavigate }: SidebarProps) {
52 + const pathname = usePathname();
53 + const router = useRouter();
54 + const [tags, setTags] = useState<TagData[]>([]);
55 + const [showTags, setShowTags] = useState(false);
56 + const [showAdmin, setShowAdmin] = useState(false);
57 +
58 + useEffect(() => {
59 + fetch("/api/tags")
60 + .then((res) => res.json())
61 + .then(setTags)
62 + .catch(() => {});
63 + }, []);
64 +
65 + const handleLogout = async () => {
66 + await fetch("/api/auth/logout", { method: "POST" });
67 + router.push("/login");
68 + onNavigate?.();
69 + };
70 +
71 + const handleLinkClick = () => {
72 + onNavigate?.();
73 + };
74 +
75 + const isAdminActive = adminItems.some((i) => pathname === i.href);
76 +
77 + return (
78 + <div className="h-screen flex flex-col bg-white dark:bg-[#0a0a0a] border-r border-gray-200 dark:border-white/10 transition-colors">
79 + {/* Logo */}
80 + <div className="px-4 py-4 flex items-center gap-2">
81 + <div className="w-9 h-9 rounded-xl bg-gray-100 dark:bg-white/10 flex items-center justify-center flex-shrink-0">
82 + <Cloud className="w-5 h-5 text-gray-700 dark:text-white" />
83 + </div>
84 + <span className="text-gray-900 dark:text-white font-semibold flex-1 text-base">SPB Cloud</span>
85 + <NotificationBell />
86 + <ThemeToggle />
87 + </div>
88 +
89 + {/* Navigation - touch optimized */}
90 + <nav className="flex-1 px-2 py-1 space-y-0.5 overflow-auto overscroll-contain">
91 + {navItems.map((item) => {
92 + const isActive =
93 + pathname === item.href ||
94 + (item.href === "/dashboard" && pathname.startsWith("/dashboard") && !pathname.includes("favorites"));
95 + return (
96 + <Link
97 + key={item.href}
98 + href={item.href}
99 + onClick={handleLinkClick}
100 + className={`flex items-center gap-3 px-3 py-3 rounded-xl text-sm transition-colors min-h-[44px] ${
101 + isActive
102 + ? "bg-gray-100 dark:bg-white/10 text-gray-900 dark:text-white font-medium"
103 + : "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-white/5 active:bg-gray-100 dark:active:bg-white/10"
104 + }`}
105 + >
106 + <item.icon className={`w-5 h-5 flex-shrink-0 ${item.href === "/dashboard/favorites" && pathname === item.href ? "text-yellow-400" : ""}`} />
107 + {item.label}
108 + </Link>
109 + );
110 + })}
111 +
112 + {/* Admin Section - toggle does NOT close sidebar */}
113 + <div className="pt-3">
114 + <button
115 + onClick={() => setShowAdmin(!showAdmin)}
116 + className={`flex items-center gap-2 px-3 py-2.5 w-full text-xs transition-colors min-h-[40px] rounded-lg active:bg-gray-50 dark:active:bg-white/5 ${
117 + isAdminActive ? "text-gray-700 dark:text-gray-300" : "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
118 + }`}
119 + >
120 + {showAdmin ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
121 + <Settings className="w-3.5 h-3.5" />
122 + <span className="uppercase tracking-wider font-medium">Administration</span>
123 + </button>
124 + {(showAdmin || isAdminActive) && (
125 + <div className="space-y-0.5 ml-1">
126 + {adminItems.map((item) => (
127 + <Link
128 + key={item.href}
129 + href={item.href}
130 + onClick={handleLinkClick}
131 + className={`flex items-center gap-2.5 px-3 py-2.5 text-sm rounded-xl transition-colors min-h-[40px] ${
132 + pathname === item.href
133 + ? "bg-gray-100 dark:bg-white/10 text-gray-900 dark:text-white font-medium"
134 + : "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-white/5 active:bg-gray-100 dark:active:bg-white/10"
135 + }`}
136 + >
137 + <item.icon className="w-4 h-4 flex-shrink-0" />
138 + {item.label}
139 + </Link>
140 + ))}
141 + </div>
142 + )}
143 + </div>
144 +
145 + {/* Tags Section - toggle does NOT close sidebar */}
146 + {tags.length > 0 && (
147 + <div className="pt-3">
148 + <button
149 + onClick={() => setShowTags(!showTags)}
150 + className="flex items-center gap-2 px-3 py-2.5 w-full text-xs text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors min-h-[40px] rounded-lg active:bg-gray-50 dark:active:bg-white/5"
151 + >
152 + {showTags ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
153 + <Tag className="w-3.5 h-3.5" />
154 + <span className="uppercase tracking-wider font-medium">Tags</span>
155 + </button>
156 + {showTags && (
157 + <div className="space-y-0.5 ml-1">
158 + {tags.map((tag) => (
159 + <Link
160 + key={tag.id}
161 + href={`/dashboard?tagId=${tag.id}`}
162 + onClick={handleLinkClick}
163 + className="flex items-center gap-2.5 px-3 py-2.5 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-white/5 active:bg-gray-100 dark:active:bg-white/10 rounded-xl transition-colors min-h-[40px]"
164 + >
165 + <span className="w-3 h-3 rounded-full flex-shrink-0" style={{ backgroundColor: tag.color }} />
166 + <span className="truncate">{tag.name}</span>
167 + {tag._count && <span className="text-xs text-gray-400 dark:text-gray-600 ml-auto tabular-nums">{tag._count.files}</span>}
168 + </Link>
169 + ))}
170 + </div>
171 + )}
172 + </div>
173 + )}
174 + </nav>
175 +
176 + {/* Footer */}
177 + <div className="px-2 py-3 border-t border-gray-200 dark:border-white/10">
178 + <button
179 + onClick={handleLogout}
180 + className="flex items-center gap-3 px-3 py-3 rounded-xl text-sm text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/5 active:bg-red-100 dark:active:bg-red-500/10 transition-colors w-full min-h-[44px]"
181 + >
182 + <LogOut className="w-5 h-5" />
183 + Déconnexion
184 + </button>
185 + </div>
186 + </div>
187 + );
188 +}
added components/preview/PreviewEngine.tsx +166 −0
@@ -0,0 +1,166 @@
1 +"use client";
2 +
3 +import { useState, useCallback } from "react";
4 +import { motion, AnimatePresence } from "framer-motion";
5 +import { Info, ChevronUp } from "lucide-react";
6 +import PreviewToolbar from "./PreviewToolbar";
7 +import PreviewNavigation from "./PreviewNavigation";
8 +import MetadataEditor from "./editors/MetadataEditor";
9 +import VersionHistory from "./VersionHistory";
10 +import { getPlugin } from "./plugins";
11 +import type { FileData } from "@/types/files";
12 +import toast from "react-hot-toast";
13 +
14 +interface PreviewEngineProps {
15 + file: FileData | null;
16 + files?: FileData[];
17 + onClose: () => void;
18 + onNavigate?: (file: FileData) => void;
19 +}
20 +
21 +export default function PreviewEngine({
22 + file,
23 + files = [],
24 + onClose,
25 + onNavigate,
26 +}: PreviewEngineProps) {
27 + const [fullscreen, setFullscreen] = useState(false);
28 + const [isEditing, setIsEditing] = useState(false);
29 + const [showInfo, setShowInfo] = useState(false);
30 + const [showMobileInfo, setShowMobileInfo] = useState(false);
31 +
32 + const handleSave = useCallback(
33 + async (data: Blob | string, asNewVersion?: boolean) => {
34 + if (!file) return;
35 + try {
36 + if (typeof data === "string") {
37 + const res = await fetch(`/api/files/${file.id}/content`, {
38 + method: "PUT",
39 + headers: { "Content-Type": "text/plain" },
40 + body: data,
41 + });
42 + if (!res.ok) throw new Error("Save failed");
43 + toast.success("Fichier sauvegardé");
44 + } else {
45 + const formData = new FormData();
46 + formData.append("file", data, file.name);
47 + if (asNewVersion) formData.append("asNewVersion", "true");
48 +
49 + const res = await fetch(`/api/files/${file.id}/save-edit`, {
50 + method: "POST",
51 + body: formData,
52 + });
53 + if (!res.ok) throw new Error("Save failed");
54 + toast.success(asNewVersion ? "Nouvelle version créée" : "Fichier sauvegardé");
55 + }
56 + } catch {
57 + toast.error("Erreur lors de la sauvegarde");
58 + }
59 + },
60 + [file]
61 + );
62 +
63 + const handleToggleFavorite = useCallback(async () => {
64 + if (!file) return;
65 + try {
66 + await fetch(`/api/files/${file.id}/favorite`, { method: "POST" });
67 + toast.success("Favori mis à jour");
68 + } catch {
69 + toast.error("Erreur");
70 + }
71 + }, [file]);
72 +
73 + if (!file) return null;
74 +
75 + const plugin = getPlugin(file.mimeType);
76 + const PluginComponent = plugin.component;
77 +
78 + return (
79 + <AnimatePresence>
80 + <motion.div
81 + initial={{ opacity: 0 }}
82 + animate={{ opacity: 1 }}
83 + exit={{ opacity: 0 }}
84 + className={`fixed inset-0 z-50 flex flex-col bg-white dark:bg-[#0a0a0a] transition-colors ${
85 + fullscreen ? "" : "lg:relative lg:inset-auto lg:h-full"
86 + }`}
87 + >
88 + <PreviewToolbar
89 + file={file}
90 + fullscreen={fullscreen}
91 + isEditing={isEditing}
92 + supportsEdit={!!plugin.supportsEdit}
93 + onClose={onClose}
94 + onToggleFullscreen={() => setFullscreen(!fullscreen)}
95 + onToggleEdit={() => setIsEditing(!isEditing)}
96 + onToggleFavorite={handleToggleFavorite}
97 + isFavorite={file.metadata?.isFavorite}
98 + />
99 +
100 + <div className="flex-1 flex overflow-hidden">
101 + {/* Main preview area */}
102 + <div className="flex-1 relative overflow-hidden">
103 + <PluginComponent
104 + file={file}
105 + isEditing={isEditing}
106 + onSave={handleSave}
107 + fullscreen={fullscreen}
108 + />
109 +
110 + {files.length > 1 && onNavigate && (
111 + <PreviewNavigation
112 + files={files}
113 + currentFile={file}
114 + onNavigate={(f) => {
115 + setIsEditing(false);
116 + onNavigate(f);
117 + }}
118 + />
119 + )}
120 + </div>
121 +
122 + {/* Info panel toggle - only visible on desktop in fullscreen */}
123 + {fullscreen && (
124 + <div className="hidden lg:flex flex-col border-l border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-[#0a0a0a]">
125 + <button
126 + onClick={() => setShowInfo(!showInfo)}
127 + className={`min-w-[44px] min-h-[44px] flex items-center justify-center p-2.5 transition-colors ${showInfo ? "text-gray-900 dark:text-white bg-gray-200 dark:bg-white/10" : "text-gray-400 dark:text-gray-500 hover:text-gray-900 dark:hover:text-white"}`}
128 + title="Infos & tags"
129 + >
130 + <Info className="w-4 h-4" />
131 + </button>
132 + </div>
133 + )}
134 + </div>
135 +
136 + {/* Mobile info toggle button */}
137 + <div className="lg:hidden border-t border-gray-200 dark:border-white/10">
138 + <button
139 + onClick={() => setShowMobileInfo(!showMobileInfo)}
140 + className="w-full flex items-center justify-center gap-2 py-3 text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
141 + >
142 + <Info className="w-4 h-4" />
143 + <span>{showMobileInfo ? "Masquer les infos" : "Voir les infos"}</span>
144 + <ChevronUp className={`w-4 h-4 transition-transform ${showMobileInfo ? "rotate-180" : ""}`} />
145 + </button>
146 + </div>
147 +
148 + {/* Mobile info panel (bottom slide-up) */}
149 + {showMobileInfo && (
150 + <div className="lg:hidden overflow-auto max-h-64 border-t border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-[#0a0a0a]">
151 + <MetadataEditor file={file} />
152 + <VersionHistory fileId={file.id} />
153 + </div>
154 + )}
155 +
156 + {/* Desktop info sidebar (metadata + versions) */}
157 + {((fullscreen && showInfo) || !fullscreen) && (
158 + <div className={`hidden lg:block ${fullscreen ? "w-72 border-l border-gray-200 dark:border-white/10 overflow-auto bg-gray-50 dark:bg-[#0a0a0a]" : "border-t border-gray-200 dark:border-white/10 overflow-auto max-h-64 lg:max-h-none"}`}>
159 + <MetadataEditor file={file} />
160 + <VersionHistory fileId={file.id} />
161 + </div>
162 + )}
163 + </motion.div>
164 + </AnimatePresence>
165 + );
166 +}
added components/preview/PreviewNavigation.tsx +63 −0
@@ -0,0 +1,63 @@
1 +"use client";
2 +
3 +import { useEffect, useCallback } from "react";
4 +import { ChevronLeft, ChevronRight } from "lucide-react";
5 +import type { FileData } from "@/types/files";
6 +
7 +interface PreviewNavigationProps {
8 + files: FileData[];
9 + currentFile: FileData;
10 + onNavigate: (file: FileData) => void;
11 +}
12 +
13 +export default function PreviewNavigation({
14 + files,
15 + currentFile,
16 + onNavigate,
17 +}: PreviewNavigationProps) {
18 + const currentIndex = files.findIndex((f) => f.id === currentFile.id);
19 +
20 + const goPrev = useCallback(() => {
21 + if (currentIndex > 0) onNavigate(files[currentIndex - 1]);
22 + }, [currentIndex, files, onNavigate]);
23 +
24 + const goNext = useCallback(() => {
25 + if (currentIndex < files.length - 1) onNavigate(files[currentIndex + 1]);
26 + }, [currentIndex, files, onNavigate]);
27 +
28 + useEffect(() => {
29 + const handleKey = (e: KeyboardEvent) => {
30 + // Don't capture if typing in an input
31 + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
32 + if (e.key === "ArrowLeft" && e.altKey) { e.preventDefault(); goPrev(); }
33 + if (e.key === "ArrowRight" && e.altKey) { e.preventDefault(); goNext(); }
34 + };
35 + window.addEventListener("keydown", handleKey);
36 + return () => window.removeEventListener("keydown", handleKey);
37 + }, [goPrev, goNext]);
38 +
39 + if (files.length <= 1) return null;
40 +
41 + return (
42 + <>
43 + {currentIndex > 0 && (
44 + <button
45 + onClick={goPrev}
46 + className="absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 rounded-full text-white hover:bg-black/70 transition-colors backdrop-blur-sm z-10"
47 + title="Précédent (Alt+←)"
48 + >
49 + <ChevronLeft className="w-5 h-5" />
50 + </button>
51 + )}
52 + {currentIndex < files.length - 1 && (
53 + <button
54 + onClick={goNext}
55 + className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 rounded-full text-white hover:bg-black/70 transition-colors backdrop-blur-sm z-10"
56 + title="Suivant (Alt+→)"
57 + >
58 + <ChevronRight className="w-5 h-5" />
59 + </button>
60 + )}
61 + </>
62 + );
63 +}
added components/preview/PreviewToolbar.tsx +85 −0
@@ -0,0 +1,85 @@
1 +"use client";
2 +
3 +import { X, Download, Maximize2, Minimize2, Edit3, Eye, Star } from "lucide-react";
4 +import { formatFileSize, formatDate } from "@/lib/utils";
5 +import type { FileData } from "@/types/files";
6 +
7 +interface PreviewToolbarProps {
8 + file: FileData;
9 + fullscreen: boolean;
10 + isEditing: boolean;
11 + supportsEdit: boolean;
12 + onClose: () => void;
13 + onToggleFullscreen: () => void;
14 + onToggleEdit: () => void;
15 + onToggleFavorite?: () => void;
16 + isFavorite?: boolean;
17 +}
18 +
19 +export default function PreviewToolbar({
20 + file,
21 + fullscreen,
22 + isEditing,
23 + supportsEdit,
24 + onClose,
25 + onToggleFullscreen,
26 + onToggleEdit,
27 + onToggleFavorite,
28 + isFavorite,
29 +}: PreviewToolbarProps) {
30 + return (
31 + <div className="flex items-center justify-between px-3 sm:px-4 py-2.5 sm:py-3 border-b border-gray-200 dark:border-white/10 flex-shrink-0 bg-white dark:bg-[#0a0a0a] transition-colors">
32 + <div className="flex-1 min-w-0 mr-2">
33 + <h3 className="text-sm font-medium text-gray-900 dark:text-white truncate max-w-[200px] sm:max-w-none">{file.name}</h3>
34 + <p className="text-xs text-gray-500">
35 + {formatFileSize(file.size)} - {formatDate(file.createdAt)}
36 + </p>
37 + </div>
38 + <div className="flex items-center gap-1.5">
39 + {onToggleFavorite && (
40 + <button
41 + onClick={onToggleFavorite}
42 + className="p-2.5 transition-colors rounded-lg"
43 + title={isFavorite ? "Retirer des favoris" : "Ajouter aux favoris"}
44 + >
45 + <Star
46 + className={`w-4 h-4 ${isFavorite ? "text-yellow-400 fill-yellow-400" : "text-gray-400 hover:text-yellow-400"}`}
47 + />
48 + </button>
49 + )}
50 + {supportsEdit && (
51 + <button
52 + onClick={onToggleEdit}
53 + className={`p-2.5 transition-colors rounded-lg ${
54 + isEditing ? "text-blue-400 bg-blue-400/10" : "text-gray-400 hover:text-gray-900 dark:hover:text-white"
55 + }`}
56 + title={isEditing ? "Mode aperçu" : "Mode édition"}
57 + >
58 + {isEditing ? <Eye className="w-4 h-4" /> : <Edit3 className="w-4 h-4" />}
59 + </button>
60 + )}
61 + <button
62 + onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}
63 + className="p-2.5 text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors rounded-lg"
64 + title="Télécharger"
65 + >
66 + <Download className="w-4 h-4" />
67 + </button>
68 + <button
69 + onClick={onToggleFullscreen}
70 + className="p-2.5 text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors hidden lg:flex items-center justify-center rounded-lg"
71 + title={fullscreen ? "Réduire" : "Plein écran"}
72 + >
73 + {fullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
74 + </button>
75 + <button
76 + onClick={onClose}
77 + className="p-2.5 text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors rounded-lg"
78 + title="Fermer"
79 + >
80 + <X className="w-4 h-4" />
81 + </button>
82 + </div>
83 + </div>
84 + );
85 +}
added components/preview/VersionHistory.tsx +124 −0
@@ -0,0 +1,124 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useCallback } from "react";
4 +import { Clock, Download, RotateCcw, GitBranch } from "lucide-react";
5 +import { formatFileSize, formatDate } from "@/lib/utils";
6 +import type { FileVersionData } from "@/types/files";
7 +import toast from "react-hot-toast";
8 +
9 +interface VersionHistoryProps {
10 + fileId: string;
11 + onRestore?: () => void;
12 +}
13 +
14 +export default function VersionHistory({ fileId, onRestore }: VersionHistoryProps) {
15 + const [versions, setVersions] = useState<FileVersionData[]>([]);
16 + const [loading, setLoading] = useState(true);
17 +
18 + const loadVersions = useCallback(async () => {
19 + try {
20 + const res = await fetch(`/api/files/${fileId}/versions`);
21 + if (res.ok) setVersions(await res.json());
22 + } catch {}
23 + setLoading(false);
24 + }, [fileId]);
25 +
26 + useEffect(() => {
27 + loadVersions();
28 + }, [loadVersions]);
29 +
30 + const createSnapshot = async () => {
31 + try {
32 + const res = await fetch(`/api/files/${fileId}/versions`, {
33 + method: "POST",
34 + headers: { "Content-Type": "application/json" },
35 + body: JSON.stringify({ changeNote: "Manual snapshot" }),
36 + });
37 + if (res.ok) {
38 + toast.success("Snapshot créé");
39 + loadVersions();
40 + }
41 + } catch {
42 + toast.error("Erreur");
43 + }
44 + };
45 +
46 + const restoreVersion = async (versionId: string, versionNum: number) => {
47 + if (!confirm(`Restaurer la version ${versionNum} ? La version actuelle sera sauvegardée.`)) return;
48 + try {
49 + const res = await fetch(`/api/files/${fileId}/versions/${versionId}/restore`, {
50 + method: "POST",
51 + });
52 + if (res.ok) {
53 + toast.success(`Version ${versionNum} restaurée`);
54 + loadVersions();
55 + onRestore?.();
56 + }
57 + } catch {
58 + toast.error("Erreur de restauration");
59 + }
60 + };
61 +
62 + if (loading) {
63 + return (
64 + <div className="p-4 flex justify-center">
65 + <div className="w-5 h-5 border-2 border-white/20 border-t-white rounded-full animate-spin" />
66 + </div>
67 + );
68 + }
69 +
70 + return (
71 + <div className="p-4 border-t border-white/10">
72 + <div className="flex items-center justify-between mb-3">
73 + <h4 className="text-xs font-medium text-gray-400 flex items-center gap-1.5">
74 + <GitBranch className="w-3.5 h-3.5" />
75 + Versions ({versions.length})
76 + </h4>
77 + <button
78 + onClick={createSnapshot}
79 + className="text-[10px] px-2 py-1 bg-white/10 hover:bg-white/20 text-gray-300 rounded transition-colors"
80 + >
81 + Snapshot
82 + </button>
83 + </div>
84 +
85 + {versions.length === 0 ? (
86 + <p className="text-xs text-gray-600">Aucune version précédente</p>
87 + ) : (
88 + <div className="space-y-1.5 max-h-60 overflow-auto">
89 + {versions.map((v) => (
90 + <div
91 + key={v.id}
92 + className="flex items-center gap-2 p-2 bg-white/5 rounded-lg group hover:bg-white/10 transition-colors"
93 + >
94 + <Clock className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
95 + <div className="flex-1 min-w-0">
96 + <p className="text-xs text-gray-300">v{v.versionNum}</p>
97 + <p className="text-[10px] text-gray-500 truncate">
98 + {v.changeNote || "—"} · {formatFileSize(v.size)}
99 + </p>
100 + <p className="text-[10px] text-gray-600">{formatDate(v.createdAt)}</p>
101 + </div>
102 + <div className="hidden group-hover:flex items-center gap-1">
103 + <button
104 + onClick={() => window.open(`/api/files/${fileId}/versions/${v.id}`, "_blank")}
105 + className="p-1 text-gray-500 hover:text-white transition-colors"
106 + title="Télécharger cette version"
107 + >
108 + <Download className="w-3 h-3" />
109 + </button>
110 + <button
111 + onClick={() => restoreVersion(v.id, v.versionNum)}
112 + className="p-1 text-gray-500 hover:text-orange-400 transition-colors"
113 + title="Restaurer cette version"
114 + >
115 + <RotateCcw className="w-3 h-3" />
116 + </button>
117 + </div>
118 + </div>
119 + ))}
120 + </div>
121 + )}
122 + </div>
123 + );
124 +}
added components/preview/editors/MetadataEditor.tsx +118 −0
@@ -0,0 +1,118 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useCallback } from "react";
4 +import { Star, FileText, Tag } from "lucide-react";
5 +import TagSelector from "@/components/tags/TagSelector";
6 +import type { FileData } from "@/types/files";
7 +import toast from "react-hot-toast";
8 +
9 +interface MetadataEditorProps {
10 + file: FileData;
11 + onUpdate?: () => void;
12 +}
13 +
14 +export default function MetadataEditor({ file, onUpdate }: MetadataEditorProps) {
15 + const [description, setDescription] = useState(file.metadata?.description || "");
16 + const [isFavorite, setIsFavorite] = useState(file.metadata?.isFavorite || false);
17 + const [tags, setTags] = useState(file.tags || []);
18 + const [, setSaving] = useState(false);
19 +
20 + const loadMetadata = useCallback(async () => {
21 + try {
22 + const [metaRes, tagsRes] = await Promise.all([
23 + fetch(`/api/files/${file.id}/metadata`),
24 + fetch(`/api/files/${file.id}/tags`),
25 + ]);
26 + if (metaRes.ok) {
27 + const meta = await metaRes.json();
28 + setDescription(meta.description || "");
29 + setIsFavorite(meta.isFavorite);
30 + }
31 + if (tagsRes.ok) {
32 + setTags(await tagsRes.json());
33 + }
34 + } catch {}
35 + }, [file.id]);
36 +
37 + useEffect(() => {
38 + loadMetadata();
39 + }, [loadMetadata]);
40 +
41 + const saveDescription = useCallback(async () => {
42 + setSaving(true);
43 + try {
44 + await fetch(`/api/files/${file.id}/metadata`, {
45 + method: "PATCH",
46 + headers: { "Content-Type": "application/json" },
47 + body: JSON.stringify({ description }),
48 + });
49 + toast.success("Description sauvegardée");
50 + onUpdate?.();
51 + } catch {
52 + toast.error("Erreur");
53 + }
54 + setSaving(false);
55 + }, [file.id, description, onUpdate]);
56 +
57 + const toggleFavorite = useCallback(async () => {
58 + try {
59 + const res = await fetch(`/api/files/${file.id}/favorite`, { method: "POST" });
60 + if (res.ok) {
61 + const meta = await res.json();
62 + setIsFavorite(meta.isFavorite);
63 + onUpdate?.();
64 + }
65 + } catch {}
66 + }, [file.id, onUpdate]);
67 +
68 + return (
69 + <div className="p-4 space-y-4 border-t border-white/10">
70 + {/* Favorite */}
71 + <div className="flex items-center justify-between">
72 + <span className="text-xs text-gray-400 flex items-center gap-1.5">
73 + <Star className="w-3.5 h-3.5" />
74 + Favori
75 + </span>
76 + <button
77 + onClick={toggleFavorite}
78 + className="p-1 transition-colors"
79 + >
80 + <Star
81 + className={`w-5 h-5 ${
82 + isFavorite ? "text-yellow-400 fill-yellow-400" : "text-gray-600 hover:text-yellow-400"
83 + }`}
84 + />
85 + </button>
86 + </div>
87 +
88 + {/* Description */}
89 + <div>
90 + <label className="text-xs text-gray-400 flex items-center gap-1.5 mb-1.5">
91 + <FileText className="w-3.5 h-3.5" />
92 + Description
93 + </label>
94 + <textarea
95 + value={description}
96 + onChange={(e) => setDescription(e.target.value)}
97 + onBlur={saveDescription}
98 + placeholder="Ajouter une description..."
99 + rows={3}
100 + className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-xs text-gray-300 placeholder:text-gray-600 resize-none focus:outline-none focus:border-white/20"
101 + />
102 + </div>
103 +
104 + {/* Tags */}
105 + <div>
106 + <label className="text-xs text-gray-400 flex items-center gap-1.5 mb-1.5">
107 + <Tag className="w-3.5 h-3.5" />
108 + Tags
109 + </label>
110 + <TagSelector
111 + fileId={file.id}
112 + currentTags={tags}
113 + onTagsChange={loadMetadata}
114 + />
115 + </div>
116 + </div>
117 + );
118 +}
added components/preview/plugins/ArchivePreview.tsx +265 −0
@@ -0,0 +1,265 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useMemo } from "react";
4 +import { File, Folder, Download, Eye, ChevronRight, ChevronDown, Archive } from "lucide-react";
5 +import type { PreviewPluginProps } from "./types";
6 +import { formatFileSize } from "@/lib/utils";
7 +
8 +interface ArchiveEntry {
9 + name: string;
10 + path: string;
11 + size: number;
12 + compressedSize: number;
13 + isDirectory: boolean;
14 + date: Date | null;
15 +}
16 +
17 +interface TreeNode {
18 + name: string;
19 + path: string;
20 + isDirectory: boolean;
21 + size: number;
22 + date: Date | null;
23 + children: TreeNode[];
24 +}
25 +
26 +export default function ArchivePreview({ file }: PreviewPluginProps) {
27 + const [entries, setEntries] = useState<ArchiveEntry[]>([]);
28 + const [loading, setLoading] = useState(true);
29 + const [error, setError] = useState<string | null>(null);
30 + const [previewContent, setPreviewContent] = useState<string | null>(null);
31 + const [previewName, setPreviewName] = useState("");
32 + // eslint-disable-next-line @typescript-eslint/no-explicit-any
33 + const [zipInstance, setZipInstance] = useState<any>(null);
34 +
35 + useEffect(() => {
36 + const loadArchive = async () => {
37 + try {
38 + const JSZip = (await import("jszip")).default;
39 + const response = await fetch(`/api/files/${file.id}/preview`);
40 + const buffer = await response.arrayBuffer();
41 + const zip = await JSZip.loadAsync(buffer);
42 + setZipInstance(zip);
43 +
44 + const items: ArchiveEntry[] = [];
45 + zip.forEach((relativePath, zipEntry) => {
46 + items.push({
47 + name: zipEntry.name.split("/").filter(Boolean).pop() || zipEntry.name,
48 + path: relativePath,
49 + // eslint-disable-next-line @typescript-eslint/no-explicit-any
50 + size: (zipEntry as any)._data?.uncompressedSize || 0,
51 + // eslint-disable-next-line @typescript-eslint/no-explicit-any
52 + compressedSize: (zipEntry as any)._data?.compressedSize || 0,
53 + isDirectory: zipEntry.dir,
54 + date: zipEntry.date,
55 + });
56 + });
57 +
58 + setEntries(items);
59 + setLoading(false);
60 + } catch {
61 + setError("Impossible de lire l'archive");
62 + setLoading(false);
63 + }
64 + };
65 +
66 + loadArchive();
67 + }, [file.id]);
68 +
69 + const tree = useMemo(() => {
70 + const root: TreeNode = { name: "", path: "", isDirectory: true, size: 0, date: null, children: [] };
71 +
72 + for (const entry of entries) {
73 + const parts = entry.path.split("/").filter(Boolean);
74 + let current = root;
75 +
76 + for (let i = 0; i < parts.length; i++) {
77 + const isLast = i === parts.length - 1;
78 + let child = current.children.find((c) => c.name === parts[i]);
79 +
80 + if (!child) {
81 + child = {
82 + name: parts[i],
83 + path: parts.slice(0, i + 1).join("/"),
84 + isDirectory: isLast ? entry.isDirectory : true,
85 + size: isLast ? entry.size : 0,
86 + date: isLast ? entry.date : null,
87 + children: [],
88 + };
89 + current.children.push(child);
90 + }
91 +
92 + current = child;
93 + }
94 + }
95 +
96 + // Sort: directories first, then by name
97 + const sortTree = (node: TreeNode) => {
98 + node.children.sort((a, b) => {
99 + if (a.isDirectory && !b.isDirectory) return -1;
100 + if (!a.isDirectory && b.isDirectory) return 1;
101 + return a.name.localeCompare(b.name);
102 + });
103 + node.children.forEach(sortTree);
104 + };
105 + sortTree(root);
106 +
107 + return root;
108 + }, [entries]);
109 +
110 + const handlePreviewFile = async (path: string, name: string) => {
111 + if (!zipInstance) return;
112 + try {
113 + const content = await zipInstance.file(path)?.async("string");
114 + setPreviewContent(content || "");
115 + setPreviewName(name);
116 + } catch {
117 + setPreviewContent("Impossible de lire ce fichier");
118 + setPreviewName(name);
119 + }
120 + };
121 +
122 + const handleDownloadFile = async (path: string, name: string) => {
123 + if (!zipInstance) return;
124 + try {
125 + const blob = await zipInstance.file(path)?.async("blob");
126 + if (!blob) return;
127 + const url = URL.createObjectURL(blob);
128 + const a = document.createElement("a");
129 + a.href = url;
130 + a.download = name;
131 + a.click();
132 + URL.revokeObjectURL(url);
133 + } catch {
134 + // ignore
135 + }
136 + };
137 +
138 + if (loading) {
139 + return (
140 + <div className="flex items-center justify-center h-full">
141 + <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />
142 + </div>
143 + );
144 + }
145 +
146 + if (error) {
147 + return (
148 + <div className="flex flex-col items-center justify-center h-full text-gray-400">
149 + <Archive className="w-12 h-12 mb-4 text-gray-600" />
150 + <p>{error}</p>
151 + </div>
152 + );
153 + }
154 +
155 + const fileCount = entries.filter((e) => !e.isDirectory).length;
156 + const totalSize = entries.reduce((sum, e) => sum + e.size, 0);
157 +
158 + return (
159 + <div className="flex flex-col h-full">
160 + {/* Stats */}
161 + <div className="flex items-center gap-3 px-3 py-2 border-b border-white/10 bg-white/5 text-xs text-gray-400">
162 + <span>{fileCount} fichiers</span>
163 + <span>{formatFileSize(totalSize)} total</span>
164 + </div>
165 +
166 + <div className="flex flex-1 overflow-hidden">
167 + {/* Tree */}
168 + <div className="flex-1 overflow-auto p-3">
169 + {tree.children.map((node) => (
170 + <TreeNodeView
171 + key={node.path}
172 + node={node}
173 + onPreview={handlePreviewFile}
174 + onDownload={handleDownloadFile}
175 + />
176 + ))}
177 + </div>
178 +
179 + {/* Preview panel */}
180 + {previewContent !== null && (
181 + <div className="w-1/2 border-l border-white/10 flex flex-col">
182 + <div className="flex items-center justify-between px-3 py-2 border-b border-white/10 bg-white/5">
183 + <span className="text-xs text-gray-400 truncate">{previewName}</span>
184 + <button
185 + onClick={() => setPreviewContent(null)}
186 + className="text-xs text-gray-500 hover:text-white"
187 + >
188 + Fermer
189 + </button>
190 + </div>
191 + <pre className="flex-1 overflow-auto p-3 text-xs text-gray-300 font-mono whitespace-pre-wrap">
192 + {previewContent}
193 + </pre>
194 + </div>
195 + )}
196 + </div>
197 + </div>
198 + );
199 +}
200 +
201 +function TreeNodeView({
202 + node,
203 + onPreview,
204 + onDownload,
205 +}: {
206 + node: TreeNode;
207 + onPreview: (path: string, name: string) => void;
208 + onDownload: (path: string, name: string) => void;
209 +}) {
210 + const [expanded, setExpanded] = useState(true);
211 +
212 + const isTextFile = (name: string) => {
213 + const ext = name.split(".").pop()?.toLowerCase() || "";
214 + return ["txt", "md", "json", "xml", "csv", "js", "ts", "jsx", "tsx", "html", "css", "py", "rb", "go", "rs", "java", "c", "cpp", "h", "yml", "yaml", "toml", "sh", "env", "log", "sql", "conf", "cfg"].includes(ext);
215 + };
216 +
217 + if (node.isDirectory) {
218 + return (
219 + <div>
220 + <button
221 + onClick={() => setExpanded(!expanded)}
222 + className="flex items-center gap-1.5 w-full px-1 py-0.5 text-sm text-gray-300 hover:text-white hover:bg-white/5 rounded transition-colors"
223 + >
224 + {expanded ? <ChevronDown className="w-3 h-3 text-gray-500" /> : <ChevronRight className="w-3 h-3 text-gray-500" />}
225 + <Folder className="w-4 h-4 text-yellow-500/70" />
226 + <span>{node.name}</span>
227 + </button>
228 + {expanded && (
229 + <div className="ml-4">
230 + {node.children.map((child) => (
231 + <TreeNodeView key={child.path} node={child} onPreview={onPreview} onDownload={onDownload} />
232 + ))}
233 + </div>
234 + )}
235 + </div>
236 + );
237 + }
238 +
239 + return (
240 + <div className="flex items-center gap-1.5 group px-1 py-0.5 text-sm text-gray-400 hover:text-white hover:bg-white/5 rounded transition-colors">
241 + <div className="w-3" />
242 + <File className="w-4 h-4 text-gray-500" />
243 + <span className="flex-1 truncate">{node.name}</span>
244 + <span className="text-xs text-gray-600 mr-2">{formatFileSize(node.size)}</span>
245 + <div className="hidden group-hover:flex items-center gap-1">
246 + {isTextFile(node.name) && (
247 + <button
248 + onClick={() => onPreview(node.path, node.name)}
249 + className="p-1 text-gray-500 hover:text-white transition-colors"
250 + title="Aperçu"
251 + >
252 + <Eye className="w-3 h-3" />
253 + </button>
254 + )}
255 + <button
256 + onClick={() => onDownload(node.path, node.name)}
257 + className="p-1 text-gray-500 hover:text-white transition-colors"
258 + title="Télécharger"
259 + >
260 + <Download className="w-3 h-3" />
261 + </button>
262 + </div>
263 + </div>
264 + );
265 +}
added components/preview/plugins/AudioPreview.tsx +183 −0
@@ -0,0 +1,183 @@
1 +"use client";
2 +
3 +import { useEffect, useRef, useState, useCallback } from "react";
4 +import { Play, Pause, Volume2, VolumeX, Gauge } from "lucide-react";
5 +import type { PreviewPluginProps } from "./types";
6 +
7 +export default function AudioPreview({ file }: PreviewPluginProps) {
8 + const containerRef = useRef<HTMLDivElement>(null);
9 + // eslint-disable-next-line @typescript-eslint/no-explicit-any
10 + const wavesurferRef = useRef<any>(null);
11 + const [isPlaying, setIsPlaying] = useState(false);
12 + const [currentTime, setCurrentTime] = useState(0);
13 + const [duration, setDuration] = useState(0);
14 + const [volume, setVolume] = useState(0.8);
15 + const [isMuted, setIsMuted] = useState(false);
16 + const [playbackRate, setPlaybackRate] = useState(1);
17 + const [loading, setLoading] = useState(true);
18 + const [error, setError] = useState(false);
19 +
20 + useEffect(() => {
21 + // eslint-disable-next-line @typescript-eslint/no-explicit-any
22 + let ws: any = null;
23 +
24 + const initWaveSurfer = async () => {
25 + try {
26 + const WaveSurfer = (await import("wavesurfer.js")).default;
27 +
28 + if (!containerRef.current) return;
29 +
30 + ws = WaveSurfer.create({
31 + container: containerRef.current,
32 + waveColor: "#4b5563",
33 + progressColor: "#9ca3af",
34 + cursorColor: "#ffffff",
35 + barWidth: 2,
36 + barGap: 1,
37 + barRadius: 2,
38 + height: 120,
39 + normalize: true,
40 + url: `/api/files/${file.id}/stream`,
41 + });
42 +
43 + ws.on("ready", () => {
44 + setDuration(ws.getDuration());
45 + setLoading(false);
46 + ws.setVolume(volume);
47 + });
48 +
49 + ws.on("audioprocess", () => {
50 + setCurrentTime(ws.getCurrentTime());
51 + });
52 +
53 + ws.on("seeking", () => {
54 + setCurrentTime(ws.getCurrentTime());
55 + });
56 +
57 + ws.on("finish", () => {
58 + setIsPlaying(false);
59 + });
60 +
61 + ws.on("error", () => {
62 + setError(true);
63 + setLoading(false);
64 + });
65 +
66 + wavesurferRef.current = ws;
67 + } catch {
68 + setError(true);
69 + setLoading(false);
70 + }
71 + };
72 +
73 + initWaveSurfer();
74 +
75 + return () => {
76 + if (ws) ws.destroy();
77 + };
78 + }, [file.id]);
79 +
80 + const togglePlay = useCallback(() => {
81 + const ws = wavesurferRef.current;
82 + if (!ws) return;
83 + ws.playPause();
84 + setIsPlaying(!isPlaying);
85 + }, [isPlaying]);
86 +
87 + const toggleMute = useCallback(() => {
88 + const ws = wavesurferRef.current;
89 + if (!ws) return;
90 + const newMuted = !isMuted;
91 + ws.setVolume(newMuted ? 0 : volume);
92 + setIsMuted(newMuted);
93 + }, [isMuted, volume]);
94 +
95 + const changeVolume = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
96 + const ws = wavesurferRef.current;
97 + if (!ws) return;
98 + const vol = parseFloat(e.target.value);
99 + ws.setVolume(vol);
100 + setVolume(vol);
101 + setIsMuted(vol === 0);
102 + }, []);
103 +
104 + const cycleSpeed = useCallback(() => {
105 + const ws = wavesurferRef.current;
106 + if (!ws) return;
107 + const speeds = [0.5, 1, 1.25, 1.5, 2];
108 + const idx = speeds.indexOf(playbackRate);
109 + const next = speeds[(idx + 1) % speeds.length];
110 + setPlaybackRate(next);
111 + ws.setPlaybackRate(next);
112 + }, [playbackRate]);
113 +
114 + const formatTime = (t: number) => {
115 + const m = Math.floor(t / 60);
116 + const s = Math.floor(t % 60);
117 + return `${m}:${s.toString().padStart(2, "0")}`;
118 + };
119 +
120 + if (error) {
121 + return (
122 + <div className="flex flex-col items-center justify-center h-full p-8">
123 + <p className="text-gray-400 mb-4">Lecteur audio</p>
124 + <audio src={`/api/files/${file.id}/stream`} controls className="w-full max-w-md" />
125 + </div>
126 + );
127 + }
128 +
129 + return (
130 + <div className="flex flex-col items-center justify-center h-full p-8 gap-6">
131 + {/* File info */}
132 + <div className="text-center">
133 + <div className="w-20 h-20 mx-auto mb-4 rounded-full bg-white/5 flex items-center justify-center">
134 + <Volume2 className="w-10 h-10 text-gray-400" />
135 + </div>
136 + <p className="text-sm text-gray-400">{file.name}</p>
137 + </div>
138 +
139 + {/* Waveform */}
140 + <div className="w-full max-w-lg">
141 + {loading && (
142 + <div className="flex items-center justify-center h-[120px]">
143 + <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />
144 + </div>
145 + )}
146 + <div ref={containerRef} className={loading ? "hidden" : ""} />
147 + </div>
148 +
149 + {/* Controls */}
150 + <div className="flex items-center gap-4">
151 + <button onClick={togglePlay} className="p-3 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors">
152 + {isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5 ml-0.5" />}
153 + </button>
154 +
155 + <span className="text-xs text-gray-500 min-w-[5rem] text-center">
156 + {formatTime(currentTime)} / {formatTime(duration)}
157 + </span>
158 +
159 + <button onClick={cycleSpeed} className="text-xs text-gray-400 hover:text-white flex items-center gap-1">
160 + <Gauge className="w-3 h-3" />
161 + {playbackRate}x
162 + </button>
163 +
164 + <div className="flex items-center gap-1">
165 + <button onClick={toggleMute} className="text-gray-400 hover:text-white">
166 + {isMuted || volume === 0 ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
167 + </button>
168 + <input
169 + type="range"
170 + min={0}
171 + max={1}
172 + step={0.05}
173 + value={isMuted ? 0 : volume}
174 + onChange={changeVolume}
175 + className="w-20 h-1 appearance-none bg-white/20 rounded-full cursor-pointer
176 + [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-2.5 [&::-webkit-slider-thumb]:h-2.5
177 + [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"
178 + />
179 + </div>
180 + </div>
181 + </div>
182 + );
183 +}
added components/preview/plugins/CodePreview.tsx +246 −0
@@ -0,0 +1,246 @@
1 +"use client";
2 +
3 +import { useEffect, useRef, useState, useCallback } from "react";
4 +import { Copy, Check, WrapText } from "lucide-react";
5 +import type { PreviewPluginProps } from "./types";
6 +import { getLanguageFromMime, getLanguageFromFilename } from "@/lib/utils";
7 +
8 +export default function CodePreview({ file, isEditing, onSave }: PreviewPluginProps) {
9 + const editorRef = useRef<HTMLDivElement>(null);
10 + // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 + const viewRef = useRef<any>(null);
12 + const [content, setContent] = useState<string>("");
13 + const [loading, setLoading] = useState(true);
14 + const [copied, setCopied] = useState(false);
15 + const [wordWrap, setWordWrap] = useState(false);
16 + const [modified, setModified] = useState(false);
17 + const saveTimeout = useRef<ReturnType<typeof setTimeout>>();
18 +
19 + // Load content
20 + useEffect(() => {
21 + setLoading(true);
22 + fetch(`/api/files/${file.id}/preview`)
23 + .then((res) => res.text())
24 + .then((text) => {
25 + setContent(text);
26 + setLoading(false);
27 + })
28 + .catch(() => setLoading(false));
29 + }, [file.id]);
30 +
31 + // Initialize CodeMirror
32 + useEffect(() => {
33 + if (loading || !editorRef.current) return;
34 +
35 + let destroyed = false;
36 +
37 + const initEditor = async () => {
38 + const { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, highlightActiveLine } = await import("@codemirror/view");
39 + const { EditorState } = await import("@codemirror/state");
40 + const { defaultKeymap, history, historyKeymap, indentWithTab } = await import("@codemirror/commands");
41 + const { syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldGutter, foldKeymap, indentOnInput } = await import("@codemirror/language");
42 + const { searchKeymap, highlightSelectionMatches } = await import("@codemirror/search");
43 + const { oneDark } = await import("@codemirror/theme-one-dark");
44 +
45 + if (destroyed || !editorRef.current) return;
46 +
47 + // Get language extension
48 + const lang = getLanguageFromMime(file.mimeType) || getLanguageFromFilename(file.name);
49 + // eslint-disable-next-line @typescript-eslint/no-explicit-any
50 + let langExtension: any = null;
51 +
52 + try {
53 + switch (lang) {
54 + case "javascript": case "typescript": {
55 + const { javascript } = await import("@codemirror/lang-javascript");
56 + langExtension = javascript({ typescript: lang === "typescript", jsx: true });
57 + break;
58 + }
59 + case "html": {
60 + const { html } = await import("@codemirror/lang-html");
61 + langExtension = html();
62 + break;
63 + }
64 + case "css": {
65 + const { css } = await import("@codemirror/lang-css");
66 + langExtension = css();
67 + break;
68 + }
69 + case "json": {
70 + const { json } = await import("@codemirror/lang-json");
71 + langExtension = json();
72 + break;
73 + }
74 + case "markdown": {
75 + const { markdown } = await import("@codemirror/lang-markdown");
76 + langExtension = markdown();
77 + break;
78 + }
79 + case "python": {
80 + const { python } = await import("@codemirror/lang-python");
81 + langExtension = python();
82 + break;
83 + }
84 + case "xml": {
85 + const { xml } = await import("@codemirror/lang-xml");
86 + langExtension = xml();
87 + break;
88 + }
89 + case "sql": {
90 + const { sql } = await import("@codemirror/lang-sql");
91 + langExtension = sql();
92 + break;
93 + }
94 + case "java": {
95 + const { java } = await import("@codemirror/lang-java");
96 + langExtension = java();
97 + break;
98 + }
99 + case "c": case "cpp": {
100 + const { cpp } = await import("@codemirror/lang-cpp");
101 + langExtension = cpp();
102 + break;
103 + }
104 + case "rust": {
105 + const { rust } = await import("@codemirror/lang-rust");
106 + langExtension = rust();
107 + break;
108 + }
109 + case "go": {
110 + const { go } = await import("@codemirror/lang-go");
111 + langExtension = go();
112 + break;
113 + }
114 + case "php": {
115 + const { php } = await import("@codemirror/lang-php");
116 + langExtension = php();
117 + break;
118 + }
119 + }
120 + } catch {
121 + // Language not available, continue without
122 + }
123 +
124 + const extensions = [
125 + lineNumbers(),
126 + highlightActiveLineGutter(),
127 + highlightSpecialChars(),
128 + drawSelection(),
129 + highlightActiveLine(),
130 + bracketMatching(),
131 + foldGutter(),
132 + indentOnInput(),
133 + syntaxHighlighting(defaultHighlightStyle),
134 + highlightSelectionMatches(),
135 + history(),
136 + oneDark,
137 + keymap.of([
138 + ...defaultKeymap,
139 + ...historyKeymap,
140 + ...foldKeymap,
141 + ...searchKeymap,
142 + indentWithTab,
143 + ]),
144 + EditorView.editable.of(isEditing),
145 + EditorState.readOnly.of(!isEditing),
146 + ];
147 +
148 + if (langExtension) extensions.push(langExtension);
149 +
150 + if (wordWrap) {
151 + extensions.push(EditorView.lineWrapping);
152 + }
153 +
154 + if (isEditing && onSave) {
155 + extensions.push(
156 + EditorView.updateListener.of((update) => {
157 + if (update.docChanged) {
158 + setModified(true);
159 + if (saveTimeout.current) clearTimeout(saveTimeout.current);
160 + saveTimeout.current = setTimeout(() => {
161 + const text = update.state.doc.toString();
162 + onSave(text, false).then(() => setModified(false));
163 + }, 2000);
164 + }
165 + }),
166 + keymap.of([{
167 + key: "Mod-s",
168 + run: (view) => {
169 + if (saveTimeout.current) clearTimeout(saveTimeout.current);
170 + const text = view.state.doc.toString();
171 + onSave(text, false).then(() => setModified(false));
172 + return true;
173 + },
174 + }]),
175 + );
176 + }
177 +
178 + // Clean up previous editor
179 + if (viewRef.current) viewRef.current.destroy();
180 +
181 + editorRef.current.innerHTML = "";
182 + const view = new EditorView({
183 + state: EditorState.create({ doc: content, extensions }),
184 + parent: editorRef.current,
185 + });
186 +
187 + viewRef.current = view;
188 + };
189 +
190 + initEditor();
191 +
192 + return () => {
193 + destroyed = true;
194 + if (viewRef.current) {
195 + viewRef.current.destroy();
196 + viewRef.current = null;
197 + }
198 + };
199 + }, [loading, content, isEditing, wordWrap, file.mimeType, file.name]);
200 +
201 + const copyContent = useCallback(() => {
202 + navigator.clipboard.writeText(content);
203 + setCopied(true);
204 + setTimeout(() => setCopied(false), 2000);
205 + }, [content]);
206 +
207 + if (loading) {
208 + return (
209 + <div className="flex items-center justify-center h-full">
210 + <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />
211 + </div>
212 + );
213 + }
214 +
215 + return (
216 + <div className="flex flex-col h-full">
217 + {/* Toolbar */}
218 + <div className="flex items-center gap-1 px-3 py-2 border-b border-white/10 bg-white/5">
219 + <span className="text-xs text-gray-500 mr-2">
220 + {getLanguageFromMime(file.mimeType) || getLanguageFromFilename(file.name)}
221 + </span>
222 + {isEditing && modified && (
223 + <span className="text-xs text-yellow-400 mr-2">modifié</span>
224 + )}
225 + <div className="flex-1" />
226 + <button
227 + onClick={() => setWordWrap(!wordWrap)}
228 + className={`p-1.5 transition-colors rounded hover:bg-white/10 ${wordWrap ? "text-white" : "text-gray-400"}`}
229 + title="Retour à la ligne"
230 + >
231 + <WrapText className="w-4 h-4" />
232 + </button>
233 + <button
234 + onClick={copyContent}
235 + className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
236 + title="Copier"
237 + >
238 + {copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
239 + </button>
240 + </div>
241 +
242 + {/* Editor */}
243 + <div ref={editorRef} className="flex-1 overflow-auto [&_.cm-editor]:h-full [&_.cm-scroller]:!overflow-auto" />
244 + </div>
245 + );
246 +}
added components/preview/plugins/CsvJsonPreview.tsx +355 −0
@@ -0,0 +1,355 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useMemo, useCallback } from "react";
4 +import { ArrowUpDown, ChevronDown, ChevronRight, Filter } from "lucide-react";
5 +import type { PreviewPluginProps } from "./types";
6 +
7 +interface SortConfig {
8 + column: number;
9 + direction: "asc" | "desc";
10 +}
11 +
12 +export default function CsvJsonPreview({ file }: PreviewPluginProps) {
13 + const [content, setContent] = useState<string>("");
14 + const [loading, setLoading] = useState(true);
15 +
16 + const isJson = file.mimeType.includes("json");
17 +
18 + useEffect(() => {
19 + setLoading(true);
20 + fetch(`/api/files/${file.id}/preview`)
21 + .then((res) => res.text())
22 + .then((text) => {
23 + setContent(text);
24 + setLoading(false);
25 + })
26 + .catch(() => setLoading(false));
27 + }, [file.id]);
28 +
29 + if (loading) {
30 + return (
31 + <div className="flex items-center justify-center h-full">
32 + <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />
33 + </div>
34 + );
35 + }
36 +
37 + if (isJson) {
38 + return <JsonViewer content={content} />;
39 + }
40 +
41 + return <CsvViewer content={content} />;
42 +}
43 +
44 +// --- CSV Viewer ---
45 +
46 +function CsvViewer({ content }: { content: string }) {
47 + const [sortConfig, setSortConfig] = useState<SortConfig | null>(null);
48 + const [filterColumn, setFilterColumn] = useState<number | null>(null);
49 + const [filterText, setFilterText] = useState("");
50 + const [page, setPage] = useState(0);
51 + const pageSize = 100;
52 +
53 + const { headers, rows } = useMemo(() => {
54 + try {
55 + // eslint-disable-next-line @typescript-eslint/no-require-imports
56 + const Papa = require("papaparse");
57 + const result = Papa.parse(content, { header: false, skipEmptyLines: true });
58 + const data = result.data as string[][];
59 + return { headers: data[0] || [], rows: data.slice(1) };
60 + } catch {
61 + const lines = content.split("\n").filter(Boolean);
62 + return {
63 + headers: lines[0]?.split(",") || [],
64 + rows: lines.slice(1).map((line) => line.split(",")),
65 + };
66 + }
67 + }, [content]);
68 +
69 + const filteredRows = useMemo(() => {
70 + let result = [...rows];
71 +
72 + if (filterColumn !== null && filterText) {
73 + result = result.filter((row) =>
74 + (row[filterColumn] || "").toLowerCase().includes(filterText.toLowerCase())
75 + );
76 + }
77 +
78 + if (sortConfig) {
79 + result.sort((a, b) => {
80 + const aVal = a[sortConfig.column] || "";
81 + const bVal = b[sortConfig.column] || "";
82 + const numA = parseFloat(aVal);
83 + const numB = parseFloat(bVal);
84 + if (!isNaN(numA) && !isNaN(numB)) {
85 + return sortConfig.direction === "asc" ? numA - numB : numB - numA;
86 + }
87 + return sortConfig.direction === "asc"
88 + ? aVal.localeCompare(bVal)
89 + : bVal.localeCompare(aVal);
90 + });
91 + }
92 +
93 + return result;
94 + }, [rows, sortConfig, filterColumn, filterText]);
95 +
96 + const pageRows = filteredRows.slice(page * pageSize, (page + 1) * pageSize);
97 + const totalPages = Math.ceil(filteredRows.length / pageSize);
98 +
99 + const toggleSort = useCallback((col: number) => {
100 + setSortConfig((prev) => {
101 + if (prev?.column === col) {
102 + return prev.direction === "asc" ? { column: col, direction: "desc" } : null;
103 + }
104 + return { column: col, direction: "asc" };
105 + });
106 + }, []);
107 +
108 + return (
109 + <div className="flex flex-col h-full">
110 + {/* Stats */}
111 + <div className="flex items-center gap-3 px-3 py-2 border-b border-white/10 bg-white/5 text-xs text-gray-400">
112 + <span>{rows.length} lignes</span>
113 + <span>{headers.length} colonnes</span>
114 + {filterText && <span>{filteredRows.length} résultats</span>}
115 + </div>
116 +
117 + {/* Table */}
118 + <div className="flex-1 overflow-auto">
119 + <table className="w-full text-sm border-collapse">
120 + <thead className="sticky top-0 z-10">
121 + <tr>
122 + <th className="border border-white/10 px-2 py-1.5 text-left text-gray-500 bg-[#1a1a1a] text-xs w-10">
123 + #
124 + </th>
125 + {headers.map((header, i) => (
126 + <th
127 + key={i}
128 + className="border border-white/10 px-3 py-1.5 text-left text-gray-300 bg-[#1a1a1a] text-xs"
129 + >
130 + <div className="flex items-center gap-1">
131 + <button
132 + onClick={() => toggleSort(i)}
133 + className="flex items-center gap-1 hover:text-white transition-colors"
134 + >
135 + {header}
136 + <ArrowUpDown className={`w-3 h-3 ${sortConfig?.column === i ? "text-white" : "text-gray-600"}`} />
137 + </button>
138 + <button
139 + onClick={() => {
140 + setFilterColumn(filterColumn === i ? null : i);
141 + setFilterText("");
142 + }}
143 + className={`p-0.5 rounded transition-colors ${filterColumn === i ? "text-white" : "text-gray-600 hover:text-gray-400"}`}
144 + >
145 + <Filter className="w-3 h-3" />
146 + </button>
147 + </div>
148 + {filterColumn === i && (
149 + <input
150 + type="text"
151 + value={filterText}
152 + onChange={(e) => { setFilterText(e.target.value); setPage(0); }}
153 + placeholder="Filtrer..."
154 + className="mt-1 w-full bg-white/10 border border-white/10 rounded px-2 py-0.5 text-xs text-white placeholder:text-gray-500"
155 + autoFocus
156 + />
157 + )}
158 + </th>
159 + ))}
160 + </tr>
161 + </thead>
162 + <tbody>
163 + {pageRows.map((row, i) => (
164 + <tr key={i} className="hover:bg-white/5">
165 + <td className="border border-white/10 px-2 py-1 text-gray-600 text-xs">
166 + {page * pageSize + i + 1}
167 + </td>
168 + {row.map((cell, j) => (
169 + <td key={j} className="border border-white/10 px-3 py-1 text-gray-400 text-xs">
170 + {cell}
171 + </td>
172 + ))}
173 + </tr>
174 + ))}
175 + </tbody>
176 + </table>
177 + </div>
178 +
179 + {/* Pagination */}
180 + {totalPages > 1 && (
181 + <div className="flex items-center justify-center gap-2 px-3 py-2 border-t border-white/10 bg-white/5">
182 + <button
183 + onClick={() => setPage((p) => Math.max(0, p - 1))}
184 + disabled={page === 0}
185 + className="px-2 py-1 text-xs text-gray-400 hover:text-white disabled:opacity-30"
186 + >
187 + Précédent
188 + </button>
189 + <span className="text-xs text-gray-500">
190 + {page + 1} / {totalPages}
191 + </span>
192 + <button
193 + onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
194 + disabled={page >= totalPages - 1}
195 + className="px-2 py-1 text-xs text-gray-400 hover:text-white disabled:opacity-30"
196 + >
197 + Suivant
198 + </button>
199 + </div>
200 + )}
201 + </div>
202 + );
203 +}
204 +
205 +// --- JSON Viewer ---
206 +
207 +function JsonViewer({ content }: { content: string }) {
208 + const parsed = useMemo(() => {
209 + try {
210 + return JSON.parse(content);
211 + } catch {
212 + return null;
213 + }
214 + }, [content]);
215 +
216 + if (parsed === null) {
217 + return (
218 + <div className="overflow-auto h-full p-4">
219 + <pre className="text-sm text-gray-300 whitespace-pre-wrap font-mono">{content}</pre>
220 + </div>
221 + );
222 + }
223 +
224 + return (
225 + <div className="overflow-auto h-full p-4 font-mono text-sm">
226 + <JsonNode value={parsed} depth={0} />
227 + </div>
228 + );
229 +}
230 +
231 +// eslint-disable-next-line @typescript-eslint/no-explicit-any
232 +function JsonNode({ value, depth, keyName }: { value: any; depth: number; keyName?: string }) {
233 + const [collapsed, setCollapsed] = useState(depth > 2);
234 +
235 + if (value === null) {
236 + return (
237 + <span>
238 + {keyName && <span className="text-purple-400">&quot;{keyName}&quot;</span>}
239 + {keyName && <span className="text-gray-500">: </span>}
240 + <span className="text-gray-500">null</span>
241 + </span>
242 + );
243 + }
244 +
245 + if (typeof value === "boolean") {
246 + return (
247 + <span>
248 + {keyName && <span className="text-purple-400">&quot;{keyName}&quot;</span>}
249 + {keyName && <span className="text-gray-500">: </span>}
250 + <span className="text-yellow-400">{value.toString()}</span>
251 + </span>
252 + );
253 + }
254 +
255 + if (typeof value === "number") {
256 + return (
257 + <span>
258 + {keyName && <span className="text-purple-400">&quot;{keyName}&quot;</span>}
259 + {keyName && <span className="text-gray-500">: </span>}
260 + <span className="text-blue-400">{value}</span>
261 + </span>
262 + );
263 + }
264 +
265 + if (typeof value === "string") {
266 + return (
267 + <span>
268 + {keyName && <span className="text-purple-400">&quot;{keyName}&quot;</span>}
269 + {keyName && <span className="text-gray-500">: </span>}
270 + <span className="text-green-400">&quot;{value}&quot;</span>
271 + </span>
272 + );
273 + }
274 +
275 + if (Array.isArray(value)) {
276 + if (value.length === 0) {
277 + return (
278 + <span>
279 + {keyName && <span className="text-purple-400">&quot;{keyName}&quot;</span>}
280 + {keyName && <span className="text-gray-500">: </span>}
281 + <span className="text-gray-400">[]</span>
282 + </span>
283 + );
284 + }
285 +
286 + return (
287 + <div>
288 + <span
289 + className="cursor-pointer hover:text-white text-gray-400 inline-flex items-center gap-1"
290 + onClick={() => setCollapsed(!collapsed)}
291 + >
292 + {collapsed ? <ChevronRight className="w-3 h-3 inline" /> : <ChevronDown className="w-3 h-3 inline" />}
293 + {keyName && <span className="text-purple-400">&quot;{keyName}&quot;</span>}
294 + {keyName && <span className="text-gray-500">: </span>}
295 + <span>[</span>
296 + {collapsed && <span className="text-gray-600">{value.length} items</span>}
297 + {collapsed && <span>]</span>}
298 + </span>
299 + {!collapsed && (
300 + <div style={{ marginLeft: depth < 10 ? 16 : 0 }}>
301 + {value.map((item, i) => (
302 + <div key={i}>
303 + <JsonNode value={item} depth={depth + 1} />
304 + {i < value.length - 1 && <span className="text-gray-600">,</span>}
305 + </div>
306 + ))}
307 + <div className="text-gray-400">]</div>
308 + </div>
309 + )}
310 + </div>
311 + );
312 + }
313 +
314 + if (typeof value === "object") {
315 + const keys = Object.keys(value);
316 + if (keys.length === 0) {
317 + return (
318 + <span>
319 + {keyName && <span className="text-purple-400">&quot;{keyName}&quot;</span>}
320 + {keyName && <span className="text-gray-500">: </span>}
321 + <span className="text-gray-400">{"{}"}</span>
322 + </span>
323 + );
324 + }
325 +
326 + return (
327 + <div>
328 + <span
329 + className="cursor-pointer hover:text-white text-gray-400 inline-flex items-center gap-1"
330 + onClick={() => setCollapsed(!collapsed)}
331 + >
332 + {collapsed ? <ChevronRight className="w-3 h-3 inline" /> : <ChevronDown className="w-3 h-3 inline" />}
333 + {keyName && <span className="text-purple-400">&quot;{keyName}&quot;</span>}
334 + {keyName && <span className="text-gray-500">: </span>}
335 + <span>{"{"}</span>
336 + {collapsed && <span className="text-gray-600">{keys.length} keys</span>}
337 + {collapsed && <span>{"}"}</span>}
338 + </span>
339 + {!collapsed && (
340 + <div style={{ marginLeft: depth < 10 ? 16 : 0 }}>
341 + {keys.map((key, i) => (
342 + <div key={key}>
343 + <JsonNode value={value[key]} depth={depth + 1} keyName={key} />
344 + {i < keys.length - 1 && <span className="text-gray-600">,</span>}
345 + </div>
346 + ))}
347 + <div className="text-gray-400">{"}"}</div>
348 + </div>
349 + )}
350 + </div>
351 + );
352 + }
353 +
354 + return <span className="text-gray-400">{String(value)}</span>;
355 +}
added components/preview/plugins/FallbackPreview.tsx +23 −0
@@ -0,0 +1,23 @@
1 +"use client";
2 +
3 +import { Download, FileQuestion } from "lucide-react";
4 +import type { PreviewPluginProps } from "./types";
5 +import { formatFileSize } from "@/lib/utils";
6 +
7 +export default function FallbackPreview({ file }: PreviewPluginProps) {
8 + return (
9 + <div className="flex flex-col items-center justify-center h-full text-gray-400 p-8">
10 + <FileQuestion className="w-16 h-16 mb-4 text-gray-600" />
11 + <p className="text-lg mb-1">Aperçu non disponible</p>
12 + <p className="text-sm text-gray-500 mb-1">{file.name}</p>
13 + <p className="text-xs text-gray-600 mb-6">{file.mimeType} - {formatFileSize(file.size)}</p>
14 + <button
15 + onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}
16 + className="flex items-center gap-2 px-4 py-2 bg-white/10 rounded-lg text-sm text-white hover:bg-white/20 transition-colors"
17 + >
18 + <Download className="w-4 h-4" />
19 + Télécharger
20 + </button>
21 + </div>
22 + );
23 +}
added components/preview/plugins/ImagePreview.tsx +154 −0
@@ -0,0 +1,154 @@
1 +"use client";
2 +
3 +import { useState, useRef, useCallback, useEffect } from "react";
4 +import { ZoomIn, ZoomOut, RotateCw, Maximize, Minimize } from "lucide-react";
5 +import type { PreviewPluginProps } from "./types";
6 +
7 +export default function ImagePreview({ file }: PreviewPluginProps) {
8 + const [scale, setScale] = useState(1);
9 + const [rotation, setRotation] = useState(0);
10 + const [position, setPosition] = useState({ x: 0, y: 0 });
11 + const [isDragging, setIsDragging] = useState(false);
12 + const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
13 + const [isFitted, setIsFitted] = useState(true);
14 + const containerRef = useRef<HTMLDivElement>(null);
15 +
16 + const resetView = useCallback(() => {
17 + setScale(1);
18 + setRotation(0);
19 + setPosition({ x: 0, y: 0 });
20 + setIsFitted(true);
21 + }, []);
22 +
23 + const handleWheel = useCallback((e: React.WheelEvent) => {
24 + e.preventDefault();
25 + const delta = e.deltaY > 0 ? 0.9 : 1.1;
26 + setScale((s) => Math.min(Math.max(s * delta, 0.1), 10));
27 + setIsFitted(false);
28 + }, []);
29 +
30 + const handleMouseDown = useCallback((e: React.MouseEvent) => {
31 + if (scale > 1) {
32 + setIsDragging(true);
33 + setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });
34 + }
35 + }, [scale, position]);
36 +
37 + const handleMouseMove = useCallback((e: React.MouseEvent) => {
38 + if (isDragging) {
39 + setPosition({
40 + x: e.clientX - dragStart.x,
41 + y: e.clientY - dragStart.y,
42 + });
43 + }
44 + }, [isDragging, dragStart]);
45 +
46 + const handleMouseUp = useCallback(() => {
47 + setIsDragging(false);
48 + }, []);
49 +
50 + // Touch handling for pinch-to-zoom
51 + const lastTouchDist = useRef<number | null>(null);
52 +
53 + const handleTouchMove = useCallback((e: React.TouchEvent) => {
54 + if (e.touches.length === 2) {
55 + e.preventDefault();
56 + const dist = Math.hypot(
57 + e.touches[0].clientX - e.touches[1].clientX,
58 + e.touches[0].clientY - e.touches[1].clientY
59 + );
60 + if (lastTouchDist.current !== null) {
61 + const delta = dist / lastTouchDist.current;
62 + setScale((s) => Math.min(Math.max(s * delta, 0.1), 10));
63 + setIsFitted(false);
64 + }
65 + lastTouchDist.current = dist;
66 + } else if (e.touches.length === 1 && scale > 1) {
67 + const touch = e.touches[0];
68 + setPosition((pos) => ({
69 + x: pos.x + (touch.clientX - (dragStart.x || touch.clientX)),
70 + y: pos.y + (touch.clientY - (dragStart.y || touch.clientY)),
71 + }));
72 + setDragStart({ x: touch.clientX, y: touch.clientY });
73 + }
74 + }, [scale, dragStart]);
75 +
76 + const handleTouchEnd = useCallback(() => {
77 + lastTouchDist.current = null;
78 + }, []);
79 +
80 + const handleTouchStart = useCallback((e: React.TouchEvent) => {
81 + if (e.touches.length === 1 && scale > 1) {
82 + setDragStart({ x: e.touches[0].clientX, y: e.touches[0].clientY });
83 + }
84 + }, [scale]);
85 +
86 + useEffect(() => {
87 + resetView();
88 + }, [file.id, resetView]);
89 +
90 + return (
91 + <div className="flex flex-col h-full">
92 + {/* Controls */}
93 + <div className="flex items-center gap-1 px-3 py-2 border-b border-white/10 bg-white/5">
94 + <button
95 + onClick={() => { setScale((s) => Math.min(s * 1.2, 10)); setIsFitted(false); }}
96 + className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
97 + title="Zoom in"
98 + >
99 + <ZoomIn className="w-4 h-4" />
100 + </button>
101 + <button
102 + onClick={() => { setScale((s) => Math.max(s * 0.8, 0.1)); setIsFitted(false); }}
103 + className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
104 + title="Zoom out"
105 + >
106 + <ZoomOut className="w-4 h-4" />
107 + </button>
108 + <span className="text-xs text-gray-500 min-w-[3rem] text-center">
109 + {Math.round(scale * 100)}%
110 + </span>
111 + <button
112 + onClick={() => setRotation((r) => (r + 90) % 360)}
113 + className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
114 + title="Rotate"
115 + >
116 + <RotateCw className="w-4 h-4" />
117 + </button>
118 + <button
119 + onClick={resetView}
120 + className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
121 + title={isFitted ? "Taille réelle" : "Ajuster"}
122 + >
123 + {isFitted ? <Maximize className="w-4 h-4" /> : <Minimize className="w-4 h-4" />}
124 + </button>
125 + </div>
126 +
127 + {/* Image */}
128 + <div
129 + ref={containerRef}
130 + className="flex-1 overflow-hidden flex items-center justify-center cursor-grab active:cursor-grabbing"
131 + onWheel={handleWheel}
132 + onMouseDown={handleMouseDown}
133 + onMouseMove={handleMouseMove}
134 + onMouseUp={handleMouseUp}
135 + onMouseLeave={handleMouseUp}
136 + onTouchStart={handleTouchStart}
137 + onTouchMove={handleTouchMove}
138 + onTouchEnd={handleTouchEnd}
139 + >
140 + <img
141 + src={`/api/files/${file.id}/preview`}
142 + alt={file.name}
143 + className="max-w-full max-h-full select-none"
144 + style={{
145 + transform: `translate(${position.x}px, ${position.y}px) scale(${scale}) rotate(${rotation}deg)`,
146 + transition: isDragging ? "none" : "transform 0.2s ease",
147 + objectFit: "contain",
148 + }}
149 + draggable={false}
150 + />
151 + </div>
152 + </div>
153 + );
154 +}
added components/preview/plugins/MarkdownPreview.tsx +53 −0
@@ -0,0 +1,53 @@
1 +"use client";
2 +
3 +import { useState, useEffect } from "react";
4 +import type { PreviewPluginProps } from "./types";
5 +import dynamic from "next/dynamic";
6 +
7 +const ReactMarkdown = dynamic(() => import("react-markdown"), { ssr: false });
8 +
9 +export default function MarkdownPreview({ file }: PreviewPluginProps) {
10 + const [content, setContent] = useState<string>("");
11 + const [loading, setLoading] = useState(true);
12 +
13 + useEffect(() => {
14 + setLoading(true);
15 + fetch(`/api/files/${file.id}/preview`)
16 + .then((res) => res.text())
17 + .then((text) => {
18 + setContent(text);
19 + setLoading(false);
20 + })
21 + .catch(() => setLoading(false));
22 + }, [file.id]);
23 +
24 + if (loading) {
25 + return (
26 + <div className="flex items-center justify-center h-full">
27 + <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />
28 + </div>
29 + );
30 + }
31 +
32 + return (
33 + <div className="overflow-auto h-full p-6">
34 + <article className="prose prose-invert prose-sm max-w-none
35 + prose-headings:text-gray-200 prose-headings:border-b prose-headings:border-white/10 prose-headings:pb-2
36 + prose-p:text-gray-300
37 + prose-a:text-blue-400
38 + prose-strong:text-gray-200
39 + prose-code:text-green-400 prose-code:bg-white/10 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-xs
40 + prose-pre:bg-[#1a1a1a] prose-pre:border prose-pre:border-white/10
41 + prose-blockquote:border-gray-600
42 + prose-table:border-collapse
43 + prose-th:border prose-th:border-white/10 prose-th:bg-white/5 prose-th:px-3 prose-th:py-2
44 + prose-td:border prose-td:border-white/10 prose-td:px-3 prose-td:py-2
45 + prose-li:text-gray-300
46 + prose-hr:border-white/10
47 + prose-img:rounded-lg
48 + ">
49 + <ReactMarkdown>{content}</ReactMarkdown>
50 + </article>
51 + </div>
52 + );
53 +}
added components/preview/plugins/PdfPreview.tsx +200 −0
@@ -0,0 +1,200 @@
1 +"use client";
2 +
3 +import { useEffect, useRef, useState, useCallback } from "react";
4 +import { ChevronLeft, ChevronRight, ZoomIn, ZoomOut } from "lucide-react";
5 +import type { PreviewPluginProps } from "./types";
6 +
7 +export default function PdfPreview({ file }: PreviewPluginProps) {
8 + const canvasRef = useRef<HTMLCanvasElement>(null);
9 + const containerRef = useRef<HTMLDivElement>(null);
10 + // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 + const pdfDocRef = useRef<any>(null);
12 + const renderTaskRef = useRef<{ cancel: () => void } | null>(null);
13 + const [pageNum, setPageNum] = useState(1);
14 + const [totalPages, setTotalPages] = useState(0);
15 + const [scale, setScale] = useState(1.2);
16 + const [loading, setLoading] = useState(true);
17 + const [error, setError] = useState(false);
18 +
19 + const renderPage = useCallback(async (num: number, currentScale: number) => {
20 + const pdf = pdfDocRef.current;
21 + if (!pdf || !canvasRef.current) return;
22 +
23 + try {
24 + // Cancel any in-progress render
25 + if (renderTaskRef.current) {
26 + renderTaskRef.current.cancel();
27 + renderTaskRef.current = null;
28 + }
29 +
30 + const page = await pdf.getPage(num);
31 + const viewport = page.getViewport({ scale: currentScale });
32 + const canvas = canvasRef.current;
33 + const context = canvas.getContext("2d");
34 + if (!context) return;
35 +
36 + canvas.height = viewport.height;
37 + canvas.width = viewport.width;
38 +
39 + const renderTask = page.render({ canvasContext: context, viewport });
40 + renderTaskRef.current = renderTask;
41 + await renderTask.promise;
42 + renderTaskRef.current = null;
43 + } catch (e: unknown) {
44 + // Ignore cancelled render errors
45 + if (e && typeof e === "object" && "name" in e && (e as { name: string }).name === "RenderingCancelledException") return;
46 + }
47 + }, []);
48 +
49 + useEffect(() => {
50 + let cancelled = false;
51 +
52 + const loadPdf = async () => {
53 + try {
54 + const pdfjsLib = await import("pdfjs-dist");
55 +
56 + // Use local worker file copied to /public
57 + pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";
58 +
59 + const loadingTask = pdfjsLib.getDocument({
60 + url: `/api/files/${file.id}/preview`,
61 + cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.624/cmaps/",
62 + cMapPacked: true,
63 + });
64 +
65 + const pdf = await loadingTask.promise;
66 + if (cancelled) return;
67 +
68 + pdfDocRef.current = pdf;
69 + setTotalPages(pdf.numPages);
70 + setLoading(false);
71 +
72 + // Render first page after state is set
73 + await renderPage(1, 1.2);
74 + } catch (err) {
75 + console.error("PDF load error:", err);
76 + if (!cancelled) {
77 + setError(true);
78 + setLoading(false);
79 + }
80 + }
81 + };
82 +
83 + loadPdf();
84 + return () => {
85 + cancelled = true;
86 + if (renderTaskRef.current) {
87 + renderTaskRef.current.cancel();
88 + }
89 + };
90 + }, [file.id, renderPage]);
91 +
92 + // Re-render when page or scale changes (but not on initial load)
93 + useEffect(() => {
94 + if (!loading && pdfDocRef.current) {
95 + renderPage(pageNum, scale);
96 + }
97 + }, [pageNum, scale, renderPage, loading]);
98 +
99 + const goToPage = useCallback((num: number) => {
100 + setPageNum(Math.max(1, Math.min(num, totalPages)));
101 + }, [totalPages]);
102 +
103 + useEffect(() => {
104 + const handleKey = (e: KeyboardEvent) => {
105 + if (e.target instanceof HTMLInputElement) return;
106 + if (e.key === "ArrowLeft" || e.key === "ArrowUp") { e.preventDefault(); goToPage(pageNum - 1); }
107 + if (e.key === "ArrowRight" || e.key === "ArrowDown") { e.preventDefault(); goToPage(pageNum + 1); }
108 + };
109 + window.addEventListener("keydown", handleKey);
110 + return () => window.removeEventListener("keydown", handleKey);
111 + }, [pageNum, goToPage]);
112 +
113 + if (error) {
114 + // Fallback to browser's built-in PDF viewer via iframe
115 + return (
116 + <div className="w-full h-full">
117 + <iframe
118 + src={`/api/files/${file.id}/preview`}
119 + className="w-full h-full border-0"
120 + title={file.name}
121 + />
122 + </div>
123 + );
124 + }
125 +
126 + return (
127 + <div className="flex flex-col h-full">
128 + {/* Toolbar */}
129 + <div className="flex items-center gap-2 px-3 py-2 border-b border-white/10 bg-white/5 flex-shrink-0">
130 + <button
131 + onClick={() => goToPage(pageNum - 1)}
132 + disabled={pageNum <= 1}
133 + className="p-1.5 text-gray-400 hover:text-white disabled:opacity-30 transition-colors rounded hover:bg-white/10"
134 + >
135 + <ChevronLeft className="w-4 h-4" />
136 + </button>
137 + <div className="flex items-center gap-1 text-xs text-gray-400">
138 + <input
139 + type="number"
140 + value={pageNum}
141 + onChange={(e) => goToPage(parseInt(e.target.value) || 1)}
142 + className="w-10 bg-white/10 border border-white/10 rounded px-1.5 py-0.5 text-center text-white text-xs"
143 + min={1}
144 + max={totalPages}
145 + />
146 + <span>/ {totalPages}</span>
147 + </div>
148 + <button
149 + onClick={() => goToPage(pageNum + 1)}
150 + disabled={pageNum >= totalPages}
151 + className="p-1.5 text-gray-400 hover:text-white disabled:opacity-30 transition-colors rounded hover:bg-white/10"
152 + >
153 + <ChevronRight className="w-4 h-4" />
154 + </button>
155 +
156 + <div className="w-px h-4 bg-white/10 mx-1" />
157 +
158 + <button
159 + onClick={() => setScale((s) => Math.min(s + 0.2, 3))}
160 + className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
161 + title="Zoom in"
162 + >
163 + <ZoomIn className="w-4 h-4" />
164 + </button>
165 + <span className="text-xs text-gray-500 min-w-[3rem] text-center">
166 + {Math.round(scale * 100)}%
167 + </span>
168 + <button
169 + onClick={() => setScale((s) => Math.max(s - 0.2, 0.4))}
170 + className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"
171 + title="Zoom out"
172 + >
173 + <ZoomOut className="w-4 h-4" />
174 + </button>
175 +
176 + <button
177 + onClick={() => setScale(1.2)}
178 + className="text-[10px] text-gray-500 hover:text-white px-1.5 py-0.5 rounded hover:bg-white/10 transition-colors"
179 + >
180 + Reset
181 + </button>
182 + </div>
183 +
184 + {/* Canvas */}
185 + <div ref={containerRef} className="flex-1 overflow-auto flex justify-center p-4 bg-gray-900/50">
186 + {loading ? (
187 + <div className="flex items-center justify-center h-full">
188 + <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />
189 + </div>
190 + ) : (
191 + <canvas
192 + ref={canvasRef}
193 + className="shadow-2xl max-w-full"
194 + style={{ background: "white" }}
195 + />
196 + )}
197 + </div>
198 + </div>
199 + );
200 +}
added components/preview/plugins/VideoPreview.tsx +204 −0
@@ -0,0 +1,204 @@
1 +"use client";
2 +
3 +import { useState, useRef, useCallback, useEffect } from "react";
4 +import {
5 + Play, Pause, Volume2, VolumeX, Maximize,
6 + SkipBack, SkipForward, Gauge
7 +} from "lucide-react";
8 +import type { PreviewPluginProps } from "./types";
9 +
10 +export default function VideoPreview({ file }: PreviewPluginProps) {
11 + const videoRef = useRef<HTMLVideoElement>(null);
12 + const [isPlaying, setIsPlaying] = useState(false);
13 + const [currentTime, setCurrentTime] = useState(0);
14 + const [duration, setDuration] = useState(0);
15 + const [volume, setVolume] = useState(1);
16 + const [isMuted, setIsMuted] = useState(false);
17 + const [playbackRate, setPlaybackRate] = useState(1);
18 + const [showControls, setShowControls] = useState(true);
19 + const hideTimeout = useRef<ReturnType<typeof setTimeout>>();
20 +
21 + const togglePlay = useCallback(() => {
22 + const video = videoRef.current;
23 + if (!video) return;
24 + if (video.paused) {
25 + video.play();
26 + setIsPlaying(true);
27 + } else {
28 + video.pause();
29 + setIsPlaying(false);
30 + }
31 + }, []);
32 +
33 + const handleSeek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
34 + const video = videoRef.current;
35 + if (!video) return;
36 + const time = parseFloat(e.target.value);
37 + video.currentTime = time;
38 + setCurrentTime(time);
39 + }, []);
40 +
41 + const skip = useCallback((seconds: number) => {
42 + const video = videoRef.current;
43 + if (!video) return;
44 + video.currentTime = Math.max(0, Math.min(video.currentTime + seconds, duration));
45 + }, [duration]);
46 +
47 + const toggleMute = useCallback(() => {
48 + const video = videoRef.current;
49 + if (!video) return;
50 + video.muted = !video.muted;
51 + setIsMuted(video.muted);
52 + }, []);
53 +
54 + const changeVolume = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
55 + const video = videoRef.current;
56 + if (!video) return;
57 + const vol = parseFloat(e.target.value);
58 + video.volume = vol;
59 + setVolume(vol);
60 + setIsMuted(vol === 0);
61 + }, []);
62 +
63 + const cycleSpeed = useCallback(() => {
64 + const speeds = [0.5, 1, 1.25, 1.5, 2];
65 + const idx = speeds.indexOf(playbackRate);
66 + const next = speeds[(idx + 1) % speeds.length];
67 + setPlaybackRate(next);
68 + if (videoRef.current) videoRef.current.playbackRate = next;
69 + }, [playbackRate]);
70 +
71 + const toggleFullscreen = useCallback(() => {
72 + const video = videoRef.current;
73 + if (!video) return;
74 + if (document.fullscreenElement) {
75 + document.exitFullscreen();
76 + } else {
77 + video.requestFullscreen();
78 + }
79 + }, []);
80 +
81 + const handleMouseMove = useCallback(() => {
82 + setShowControls(true);
83 + if (hideTimeout.current) clearTimeout(hideTimeout.current);
84 + hideTimeout.current = setTimeout(() => {
85 + if (isPlaying) setShowControls(false);
86 + }, 3000);
87 + }, [isPlaying]);
88 +
89 + useEffect(() => {
90 + const handleKey = (e: KeyboardEvent) => {
91 + switch (e.key) {
92 + case " ": e.preventDefault(); togglePlay(); break;
93 + case "ArrowLeft": skip(-10); break;
94 + case "ArrowRight": skip(10); break;
95 + case "f": toggleFullscreen(); break;
96 + case "m": toggleMute(); break;
97 + }
98 + };
99 + window.addEventListener("keydown", handleKey);
100 + return () => window.removeEventListener("keydown", handleKey);
101 + }, [togglePlay, skip, toggleFullscreen, toggleMute]);
102 +
103 + const formatTime = (t: number) => {
104 + const m = Math.floor(t / 60);
105 + const s = Math.floor(t % 60);
106 + return `${m}:${s.toString().padStart(2, "0")}`;
107 + };
108 +
109 + return (
110 + <div
111 + className="flex flex-col h-full bg-black relative"
112 + onMouseMove={handleMouseMove}
113 + >
114 + <div className="flex-1 flex items-center justify-center relative">
115 + <video
116 + ref={videoRef}
117 + src={`/api/files/${file.id}/stream`}
118 + className="max-w-full max-h-full"
119 + onClick={togglePlay}
120 + onTimeUpdate={() => setCurrentTime(videoRef.current?.currentTime || 0)}
121 + onLoadedMetadata={() => setDuration(videoRef.current?.duration || 0)}
122 + onEnded={() => setIsPlaying(false)}
123 + preload="metadata"
124 + />
125 +
126 + {/* Play overlay */}
127 + {!isPlaying && (
128 + <button
129 + onClick={togglePlay}
130 + className="absolute inset-0 flex items-center justify-center bg-black/30"
131 + >
132 + <div className="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
133 + <Play className="w-8 h-8 text-white ml-1" />
134 + </div>
135 + </button>
136 + )}
137 + </div>
138 +
139 + {/* Controls */}
140 + <div
141 + className={`px-4 py-3 bg-gradient-to-t from-black/90 to-transparent transition-opacity ${
142 + showControls ? "opacity-100" : "opacity-0"
143 + }`}
144 + >
145 + {/* Seek bar */}
146 + <input
147 + type="range"
148 + min={0}
149 + max={duration || 0}
150 + step={0.1}
151 + value={currentTime}
152 + onChange={handleSeek}
153 + className="w-full h-1 mb-2 appearance-none bg-white/20 rounded-full cursor-pointer
154 + [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
155 + [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"
156 + />
157 +
158 + <div className="flex items-center gap-3">
159 + <button onClick={() => skip(-10)} className="text-white/70 hover:text-white">
160 + <SkipBack className="w-4 h-4" />
161 + </button>
162 + <button onClick={togglePlay} className="text-white">
163 + {isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
164 + </button>
165 + <button onClick={() => skip(10)} className="text-white/70 hover:text-white">
166 + <SkipForward className="w-4 h-4" />
167 + </button>
168 +
169 + <span className="text-xs text-white/60 min-w-[5rem]">
170 + {formatTime(currentTime)} / {formatTime(duration)}
171 + </span>
172 +
173 + <div className="flex-1" />
174 +
175 + <button onClick={cycleSpeed} className="text-xs text-white/70 hover:text-white flex items-center gap-1">
176 + <Gauge className="w-3 h-3" />
177 + {playbackRate}x
178 + </button>
179 +
180 + <div className="flex items-center gap-1">
181 + <button onClick={toggleMute} className="text-white/70 hover:text-white">
182 + {isMuted || volume === 0 ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
183 + </button>
184 + <input
185 + type="range"
186 + min={0}
187 + max={1}
188 + step={0.05}
189 + value={isMuted ? 0 : volume}
190 + onChange={changeVolume}
191 + className="w-16 h-1 appearance-none bg-white/20 rounded-full cursor-pointer
192 + [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-2.5 [&::-webkit-slider-thumb]:h-2.5
193 + [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"
194 + />
195 + </div>
196 +
197 + <button onClick={toggleFullscreen} className="text-white/70 hover:text-white">
198 + <Maximize className="w-4 h-4" />
199 + </button>
200 + </div>
201 + </div>
202 + </div>
203 + );
204 +}
added components/preview/plugins/index.ts +99 −0
@@ -0,0 +1,99 @@
1 +/* eslint-disable @typescript-eslint/no-explicit-any */
2 +import dynamic from "next/dynamic";
3 +import type { PreviewPlugin } from "./types";
4 +import { isArchiveMime, isCodeMime, isTextMime } from "@/lib/utils";
5 +
6 +const ImagePreview = dynamic(() => import("./ImagePreview"), { ssr: false });
7 +const VideoPreview = dynamic(() => import("./VideoPreview"), { ssr: false });
8 +const AudioPreview = dynamic(() => import("./AudioPreview"), { ssr: false });
9 +const PdfPreview = dynamic(() => import("./PdfPreview"), { ssr: false });
10 +const CodePreview = dynamic(() => import("./CodePreview"), { ssr: false });
11 +const CsvJsonPreview = dynamic(() => import("./CsvJsonPreview"), { ssr: false });
12 +const MarkdownPreview = dynamic(() => import("./MarkdownPreview"), { ssr: false });
13 +const ArchivePreview = dynamic(() => import("./ArchivePreview"), { ssr: false });
14 +const FallbackPreview = dynamic(() => import("./FallbackPreview"), { ssr: false });
15 +
16 +const plugins: PreviewPlugin[] = [
17 + {
18 + id: "image",
19 + name: "Image",
20 + canHandle: (mime) => mime.startsWith("image/"),
21 + priority: 10,
22 + component: ImagePreview as any,
23 + },
24 + {
25 + id: "video",
26 + name: "Video",
27 + canHandle: (mime) => mime.startsWith("video/"),
28 + priority: 10,
29 + component: VideoPreview as any,
30 + },
31 + {
32 + id: "audio",
33 + name: "Audio",
34 + canHandle: (mime) => mime.startsWith("audio/"),
35 + priority: 10,
36 + component: AudioPreview as any,
37 + },
38 + {
39 + id: "pdf",
40 + name: "PDF",
41 + canHandle: (mime) => mime === "application/pdf",
42 + priority: 10,
43 + component: PdfPreview as any,
44 + },
45 + {
46 + id: "csv-json",
47 + name: "CSV/JSON",
48 + canHandle: (mime) => mime === "text/csv" || mime.includes("json"),
49 + priority: 15,
50 + component: CsvJsonPreview as any,
51 + },
52 + {
53 + id: "markdown",
54 + name: "Markdown",
55 + canHandle: (mime) => mime.includes("markdown") || mime === "text/x-markdown",
56 + priority: 15,
57 + component: MarkdownPreview as any,
58 + },
59 + {
60 + id: "code",
61 + name: "Code",
62 + canHandle: (mime) => isCodeMime(mime),
63 + priority: 10,
64 + component: CodePreview as any,
65 + supportsEdit: true,
66 + },
67 + {
68 + id: "text",
69 + name: "Texte",
70 + canHandle: (mime) => isTextMime(mime),
71 + priority: 5,
72 + component: CodePreview as any,
73 + supportsEdit: true,
74 + },
75 + {
76 + id: "archive",
77 + name: "Archive",
78 + canHandle: (mime) => isArchiveMime(mime),
79 + priority: 10,
80 + component: ArchivePreview as any,
81 + },
82 + {
83 + id: "fallback",
84 + name: "Télécharger",
85 + canHandle: () => true,
86 + priority: 0,
87 + component: FallbackPreview as any,
88 + },
89 +];
90 +
91 +export function getPlugin(mimeType: string): PreviewPlugin {
92 + return plugins
93 + .filter((p) => p.canHandle(mimeType))
94 + .sort((a, b) => b.priority - a.priority)[0];
95 +}
96 +
97 +export function getAllPlugins(): PreviewPlugin[] {
98 + return plugins;
99 +}
added components/preview/plugins/types.ts +17 −0
@@ -0,0 +1,17 @@
1 +import { FileData } from "@/types/files";
2 +
3 +export interface PreviewPluginProps {
4 + file: FileData;
5 + isEditing: boolean;
6 + onSave?: (data: Blob | string, asNewVersion?: boolean) => Promise<void>;
7 + fullscreen: boolean;
8 +}
9 +
10 +export interface PreviewPlugin {
11 + id: string;
12 + name: string;
13 + canHandle: (mimeType: string) => boolean;
14 + priority: number;
15 + component: React.ComponentType<PreviewPluginProps>;
16 + supportsEdit?: boolean;
17 +}
added components/tags/TagBadge.tsx +38 −0
@@ -0,0 +1,38 @@
1 +"use client";
2 +
3 +import { X } from "lucide-react";
4 +
5 +interface TagBadgeProps {
6 + name: string;
7 + color: string;
8 + onRemove?: () => void;
9 + size?: "sm" | "md";
10 +}
11 +
12 +export default function TagBadge({ name, color, onRemove, size = "sm" }: TagBadgeProps) {
13 + return (
14 + <span
15 + className={`inline-flex items-center gap-1 rounded-full border border-white/10 ${
16 + size === "sm" ? "px-2 py-0.5 text-[10px]" : "px-2.5 py-1 text-xs"
17 + }`}
18 + style={{ backgroundColor: `${color}20`, color }}
19 + >
20 + <span
21 + className={`rounded-full ${size === "sm" ? "w-1.5 h-1.5" : "w-2 h-2"}`}
22 + style={{ backgroundColor: color }}
23 + />
24 + {name}
25 + {onRemove && (
26 + <button
27 + onClick={(e) => {
28 + e.stopPropagation();
29 + onRemove();
30 + }}
31 + className="hover:opacity-70 transition-opacity"
32 + >
33 + <X className={size === "sm" ? "w-2.5 h-2.5" : "w-3 h-3"} />
34 + </button>
35 + )}
36 + </span>
37 + );
38 +}
added components/tags/TagSelector.tsx +155 −0
@@ -0,0 +1,155 @@
1 +"use client";
2 +
3 +import { useState, useEffect, useRef } from "react";
4 +import { Plus } from "lucide-react";
5 +import TagBadge from "./TagBadge";
6 +
7 +interface TagData {
8 + id: string;
9 + name: string;
10 + color: string;
11 +}
12 +
13 +interface TagSelectorProps {
14 + fileId: string;
15 + currentTags: { id: string; tag: TagData }[];
16 + onTagsChange: () => void;
17 +}
18 +
19 +const TAG_COLORS = [
20 + "#ef4444", "#f97316", "#eab308", "#22c55e",
21 + "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899",
22 +];
23 +
24 +export default function TagSelector({ fileId, currentTags, onTagsChange }: TagSelectorProps) {
25 + const [allTags, setAllTags] = useState<TagData[]>([]);
26 + const [isOpen, setIsOpen] = useState(false);
27 + const [newTagName, setNewTagName] = useState("");
28 + const [newTagColor, setNewTagColor] = useState(TAG_COLORS[0]);
29 + const ref = useRef<HTMLDivElement>(null);
30 +
31 + useEffect(() => {
32 + fetch("/api/tags")
33 + .then((res) => res.json())
34 + .then(setAllTags)
35 + .catch(() => {});
36 + }, []);
37 +
38 + useEffect(() => {
39 + const handleClick = (e: MouseEvent) => {
40 + if (ref.current && !ref.current.contains(e.target as Node)) {
41 + setIsOpen(false);
42 + }
43 + };
44 + document.addEventListener("mousedown", handleClick);
45 + return () => document.removeEventListener("mousedown", handleClick);
46 + }, []);
47 +
48 + const currentTagIds = currentTags.map((t) => t.tag.id);
49 + const availableTags = allTags.filter((t) => !currentTagIds.includes(t.id));
50 +
51 + const addTag = async (tagId: string) => {
52 + await fetch(`/api/files/${fileId}/tags`, {
53 + method: "POST",
54 + headers: { "Content-Type": "application/json" },
55 + body: JSON.stringify({ tagId }),
56 + });
57 + onTagsChange();
58 + };
59 +
60 + const removeTag = async (tagId: string) => {
61 + await fetch(`/api/files/${fileId}/tags/${tagId}`, { method: "DELETE" });
62 + onTagsChange();
63 + };
64 +
65 + const createAndAddTag = async () => {
66 + if (!newTagName.trim()) return;
67 + const res = await fetch("/api/tags", {
68 + method: "POST",
69 + headers: { "Content-Type": "application/json" },
70 + body: JSON.stringify({ name: newTagName.trim(), color: newTagColor }),
71 + });
72 + const tag = await res.json();
73 + setAllTags((prev) => [...prev, tag]);
74 + await addTag(tag.id);
75 + setNewTagName("");
76 + setNewTagColor(TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)]);
77 + };
78 +
79 + return (
80 + <div ref={ref} className="relative">
81 + <div className="flex flex-wrap gap-1.5 items-center">
82 + {currentTags.map((ft) => (
83 + <TagBadge
84 + key={ft.id}
85 + name={ft.tag.name}
86 + color={ft.tag.color}
87 + onRemove={() => removeTag(ft.tag.id)}
88 + size="md"
89 + />
90 + ))}
91 + <button
92 + onClick={() => setIsOpen(!isOpen)}
93 + className="inline-flex items-center gap-1 px-2 py-1 text-xs text-gray-500 hover:text-white border border-dashed border-white/20 hover:border-white/40 rounded-full transition-colors"
94 + >
95 + <Plus className="w-3 h-3" />
96 + Tag
97 + </button>
98 + </div>
99 +
100 + {isOpen && (
101 + <div className="absolute top-full left-0 mt-2 w-56 bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl z-50 p-2">
102 + {availableTags.length > 0 && (
103 + <div className="mb-2">
104 + <p className="text-[10px] text-gray-500 uppercase tracking-wider px-2 mb-1">Tags existants</p>
105 + {availableTags.map((tag) => (
106 + <button
107 + key={tag.id}
108 + onClick={() => addTag(tag.id)}
109 + className="flex items-center gap-2 w-full px-2 py-1.5 text-xs text-gray-300 hover:bg-white/10 rounded transition-colors"
110 + >
111 + <span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: tag.color }} />
112 + {tag.name}
113 + </button>
114 + ))}
115 + </div>
116 + )}
117 +
118 + <div className="border-t border-white/10 pt-2">
119 + <p className="text-[10px] text-gray-500 uppercase tracking-wider px-2 mb-1">Nouveau tag</p>
120 + <div className="flex items-center gap-1 px-1">
121 + <input
122 + type="text"
123 + value={newTagName}
124 + onChange={(e) => setNewTagName(e.target.value)}
125 + onKeyDown={(e) => e.key === "Enter" && createAndAddTag()}
126 + placeholder="Nom..."
127 + className="flex-1 bg-white/5 border border-white/10 rounded px-2 py-1 text-xs text-white placeholder:text-gray-600"
128 + autoFocus
129 + />
130 + <div className="flex gap-0.5">
131 + {TAG_COLORS.slice(0, 4).map((c) => (
132 + <button
133 + key={c}
134 + onClick={() => setNewTagColor(c)}
135 + className={`w-4 h-4 rounded-full border-2 ${
136 + newTagColor === c ? "border-white" : "border-transparent"
137 + }`}
138 + style={{ backgroundColor: c }}
139 + />
140 + ))}
141 + </div>
142 + <button
143 + onClick={createAndAddTag}
144 + disabled={!newTagName.trim()}
145 + className="p-1 text-gray-400 hover:text-white disabled:opacity-30"
146 + >
147 + <Plus className="w-3.5 h-3.5" />
148 + </button>
149 + </div>
150 + </div>
151 + </div>
152 + )}
153 + </div>
154 + );
155 +}
added components/theme/DynamicToaster.tsx +28 −0
@@ -0,0 +1,28 @@
1 +"use client";
2 +
3 +import { Toaster } from "react-hot-toast";
4 +import { useTheme } from "./ThemeProvider";
5 +
6 +export function DynamicToaster() {
7 + const { theme } = useTheme();
8 +
9 + return (
10 + <Toaster
11 + position="bottom-right"
12 + toastOptions={{
13 + style: {
14 + background: theme === "dark" ? "#1f2937" : "#ffffff",
15 + color: theme === "dark" ? "#e5e7eb" : "#111827",
16 + border: theme === "dark"
17 + ? "1px solid rgba(255,255,255,0.1)"
18 + : "1px solid rgba(0,0,0,0.1)",
19 + borderRadius: "12px",
20 + fontSize: "14px",
21 + boxShadow: theme === "dark"
22 + ? "0 4px 12px rgba(0,0,0,0.5)"
23 + : "0 4px 12px rgba(0,0,0,0.1)",
24 + },
25 + }}
26 + />
27 + );
28 +}
added components/theme/ThemeProvider.tsx +60 −0
@@ -0,0 +1,60 @@
1 +"use client";
2 +
3 +import { createContext, useContext, useEffect, useState } from "react";
4 +
5 +type Theme = "dark" | "light";
6 +
7 +interface ThemeContextType {
8 + theme: Theme;
9 + toggleTheme: () => void;
10 +}
11 +
12 +const ThemeContext = createContext<ThemeContextType>({
13 + theme: "dark",
14 + toggleTheme: () => {},
15 +});
16 +
17 +export function useTheme() {
18 + return useContext(ThemeContext);
19 +}
20 +
21 +export default function ThemeProvider({ children }: { children: React.ReactNode }) {
22 + const [theme, setTheme] = useState<Theme>("dark");
23 + const [mounted, setMounted] = useState(false);
24 +
25 + useEffect(() => {
26 + const stored = localStorage.getItem("spb-theme") as Theme | null;
27 + if (stored === "light" || stored === "dark") {
28 + setTheme(stored);
29 + }
30 + setMounted(true);
31 + }, []);
32 +
33 + useEffect(() => {
34 + if (!mounted) return;
35 + const root = document.documentElement;
36 + if (theme === "dark") {
37 + root.classList.add("dark");
38 + root.classList.remove("light");
39 + } else {
40 + root.classList.remove("dark");
41 + root.classList.add("light");
42 + }
43 + localStorage.setItem("spb-theme", theme);
44 + }, [theme, mounted]);
45 +
46 + const toggleTheme = () => {
47 + setTheme((prev) => (prev === "dark" ? "light" : "dark"));
48 + };
49 +
50 + // Prevent flash of wrong theme
51 + if (!mounted) {
52 + return <>{children}</>;
53 + }
54 +
55 + return (
56 + <ThemeContext.Provider value={{ theme, toggleTheme }}>
57 + {children}
58 + </ThemeContext.Provider>
59 + );
60 +}
added components/theme/ThemeToggle.tsx +38 −0
@@ -0,0 +1,38 @@
1 +"use client";
2 +
3 +import { motion } from "framer-motion";
4 +import { Sun, Moon } from "lucide-react";
5 +import { useTheme } from "./ThemeProvider";
6 +
7 +interface ThemeToggleProps {
8 + className?: string;
9 +}
10 +
11 +export default function ThemeToggle({ className = "" }: ThemeToggleProps) {
12 + const { theme, toggleTheme } = useTheme();
13 +
14 + return (
15 + <motion.button
16 + whileTap={{ scale: 0.9 }}
17 + onClick={toggleTheme}
18 + className={`relative flex items-center justify-center min-w-[44px] min-h-[44px] w-10 h-10 rounded-xl transition-colors
19 + dark:bg-white/10 dark:hover:bg-white/20 dark:active:bg-white/25 dark:text-yellow-300
20 + bg-gray-100 hover:bg-gray-200 active:bg-gray-300 text-gray-600
21 + ${className}`}
22 + title={theme === "dark" ? "Mode clair" : "Mode sombre"}
23 + >
24 + <motion.div
25 + key={theme}
26 + initial={{ rotate: -90, opacity: 0 }}
27 + animate={{ rotate: 0, opacity: 1 }}
28 + transition={{ duration: 0.2 }}
29 + >
30 + {theme === "dark" ? (
31 + <Sun className="w-5 h-5" />
32 + ) : (
33 + <Moon className="w-5 h-5" />
34 + )}
35 + </motion.div>
36 + </motion.button>
37 + );
38 +}
added components/ui/Button.tsx +47 −0
@@ -0,0 +1,47 @@
1 +"use client";
2 +
3 +import { motion } from "framer-motion";
4 +import { Loader2 } from "lucide-react";
5 +import { ButtonHTMLAttributes } from "react";
6 +
7 +interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
8 + variant?: "primary" | "secondary" | "danger" | "ghost";
9 + size?: "sm" | "md" | "lg";
10 + loading?: boolean;
11 +}
12 +
13 +const variants = {
14 + primary: "bg-gray-900 text-white hover:bg-gray-800 active:bg-gray-700 dark:bg-white dark:text-black dark:hover:bg-gray-200 dark:active:bg-gray-300",
15 + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 active:bg-gray-300 border border-gray-200 dark:bg-white/10 dark:text-white dark:hover:bg-white/20 dark:active:bg-white/25 dark:border-white/10",
16 + danger: "bg-red-50 text-red-600 hover:bg-red-100 active:bg-red-200 border border-red-200 dark:bg-red-500/10 dark:text-red-400 dark:hover:bg-red-500/20 dark:active:bg-red-500/30 dark:border-red-500/20",
17 + ghost: "text-gray-500 hover:text-gray-900 hover:bg-gray-100 active:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/5 dark:active:bg-white/10",
18 +};
19 +
20 +// Mobile-optimized: minimum 44px touch targets
21 +const sizes = {
22 + sm: "px-3 py-2 text-xs min-h-[36px] sm:min-h-[32px]",
23 + md: "px-4 py-2.5 text-sm min-h-[44px] sm:min-h-[40px]",
24 + lg: "px-6 py-3.5 text-base min-h-[48px]",
25 +};
26 +
27 +export default function Button({
28 + variant = "primary",
29 + size = "md",
30 + loading,
31 + children,
32 + className = "",
33 + disabled,
34 + ...props
35 +}: ButtonProps) {
36 + return (
37 + <motion.button
38 + whileTap={{ scale: 0.97 }}
39 + className={`inline-flex items-center justify-center gap-2 rounded-xl font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed select-none ${variants[variant]} ${sizes[size]} ${className}`}
40 + disabled={disabled || loading}
41 + {...(props as Record<string, unknown>)}
42 + >
43 + {loading && <Loader2 className="w-4 h-4 animate-spin" />}
44 + {children}
45 + </motion.button>
46 + );
47 +}
added components/ui/Modal.tsx +79 −0
@@ -0,0 +1,79 @@
1 +"use client";
2 +
3 +import { motion, AnimatePresence } from "framer-motion";
4 +import { X } from "lucide-react";
5 +import { useEffect } from "react";
6 +
7 +interface ModalProps {
8 + isOpen: boolean;
9 + onClose: () => void;
10 + title?: string;
11 + children: React.ReactNode;
12 + maxWidth?: string;
13 +}
14 +
15 +export default function Modal({
16 + isOpen,
17 + onClose,
18 + title,
19 + children,
20 + maxWidth = "max-w-md",
21 +}: ModalProps) {
22 + useEffect(() => {
23 + if (isOpen) {
24 + document.body.style.overflow = "hidden";
25 + } else {
26 + document.body.style.overflow = "";
27 + }
28 + return () => {
29 + document.body.style.overflow = "";
30 + };
31 + }, [isOpen]);
32 +
33 + return (
34 + <AnimatePresence>
35 + {isOpen && (
36 + <div
37 + className="fixed inset-0 z-50 flex items-end sm:items-center justify-center sm:p-4"
38 + onMouseDown={(e) => e.stopPropagation()}
39 + onClick={(e) => e.stopPropagation()}
40 + onPointerDown={(e) => e.stopPropagation()}
41 + >
42 + <motion.div
43 + initial={{ opacity: 0 }}
44 + animate={{ opacity: 1 }}
45 + exit={{ opacity: 0 }}
46 + className="absolute inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm"
47 + onClick={onClose}
48 + />
49 + {/* Bottom sheet on mobile, centered card on desktop */}
50 + <motion.div
51 + initial={{ opacity: 0, y: 100 }}
52 + animate={{ opacity: 1, y: 0 }}
53 + exit={{ opacity: 0, y: 100 }}
54 + transition={{ type: "spring", damping: 25, stiffness: 300 }}
55 + className={`relative w-full ${maxWidth} bg-white dark:bg-gray-900 border border-gray-200 dark:border-white/10 rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden transition-colors max-h-[90vh] flex flex-col`}
56 + >
57 + {/* Drag handle on mobile */}
58 + <div className="sm:hidden flex justify-center pt-2 pb-1">
59 + <div className="w-10 h-1 rounded-full bg-gray-300 dark:bg-gray-600" />
60 + </div>
61 +
62 + {title && (
63 + <div className="flex items-center justify-between px-4 sm:px-6 py-3 sm:py-4 border-b border-gray-200 dark:border-white/10">
64 + <h2 className="text-base sm:text-lg font-semibold text-gray-900 dark:text-white">{title}</h2>
65 + <button
66 + onClick={onClose}
67 + className="text-gray-400 hover:text-gray-700 dark:hover:text-white transition-colors p-2 -mr-1 min-w-[44px] min-h-[44px] flex items-center justify-center rounded-xl active:bg-gray-100 dark:active:bg-white/10"
68 + >
69 + <X className="w-5 h-5" />
70 + </button>
71 + </div>
72 + )}
73 + <div className="p-4 sm:p-6 overflow-auto flex-1">{children}</div>
74 + </motion.div>
75 + </div>
76 + )}
77 + </AnimatePresence>
78 + );
79 +}
added components/ui/SearchBar.tsx +50 −0
@@ -0,0 +1,50 @@
1 +"use client";
2 +
3 +import { Search, X } from "lucide-react";
4 +import { useState, useCallback } from "react";
5 +
6 +interface SearchBarProps {
7 + onSearch: (query: string) => void;
8 + placeholder?: string;
9 +}
10 +
11 +export default function SearchBar({
12 + onSearch,
13 + placeholder = "Rechercher...",
14 +}: SearchBarProps) {
15 + const [query, setQuery] = useState("");
16 +
17 + const handleChange = useCallback(
18 + (value: string) => {
19 + setQuery(value);
20 + onSearch(value);
21 + },
22 + [onSearch]
23 + );
24 +
25 + const handleClear = () => {
26 + setQuery("");
27 + onSearch("");
28 + };
29 +
30 + return (
31 + <div className="relative">
32 + <Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500 pointer-events-none" />
33 + <input
34 + type="text"
35 + value={query}
36 + onChange={(e) => handleChange(e.target.value)}
37 + placeholder={placeholder}
38 + className="w-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl pl-10 pr-10 py-3 sm:py-2.5 text-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500/50 dark:focus:border-white/20 transition-all"
39 + />
40 + {query && (
41 + <button
42 + onClick={handleClear}
43 + className="absolute right-1.5 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 p-2 min-w-[36px] min-h-[36px] flex items-center justify-center rounded-lg active:bg-gray-200 dark:active:bg-white/10"
44 + >
45 + <X className="w-4 h-4" />
46 + </button>
47 + )}
48 + </div>
49 + );
50 +}
added lib/activity.ts +15 −0
@@ -0,0 +1,15 @@
1 +import { prisma } from "./prisma";
2 +
3 +export async function logActivity(
4 + action: string,
5 + fileId?: string | null,
6 + details?: string
7 +) {
8 + await prisma.activityLog.create({
9 + data: {
10 + action,
11 + fileId: fileId || null,
12 + details: details || null,
13 + },
14 + });
15 +}
added lib/ai.ts +167 −0
@@ -0,0 +1,167 @@
1 +import Anthropic from "@anthropic-ai/sdk";
2 +import { prisma } from "./prisma";
3 +import { readFileSync, existsSync } from "fs";
4 +import { getFilePath } from "./storage";
5 +
6 +const anthropic = new Anthropic({
7 + apiKey: process.env.ANTHROPIC_API_KEY,
8 +});
9 +
10 +export async function aiSearch(query: string, userId?: string): Promise<{
11 + results: Array<{ fileId: string; name: string; relevance: string; snippet: string }>;
12 + answer: string;
13 +}> {
14 + const where: Record<string, unknown> = {};
15 + if (userId) where.userId = userId;
16 +
17 + const files = await prisma.file.findMany({
18 + where,
19 + select: { id: true, name: true, mimeType: true, size: true, aiTags: true, aiDescription: true, ocrText: true, createdAt: true, folder: { select: { name: true } } },
20 + take: 200,
21 + orderBy: { createdAt: "desc" },
22 + });
23 +
24 + const fileList = files.map((f) => {
25 + const parts = [`- ${f.name} (${f.mimeType}, ID:${f.id})`];
26 + if (f.folder) parts.push(` Dossier: ${f.folder.name}`);
27 + if (f.aiDescription) parts.push(` Description: ${f.aiDescription}`);
28 + if (f.aiTags) parts.push(` Tags: ${f.aiTags}`);
29 + if (f.ocrText) parts.push(` OCR: ${f.ocrText.substring(0, 200)}`);
30 + return parts.join("\n");
31 + }).join("\n");
32 +
33 + const response = await anthropic.messages.create({
34 + model: "claude-sonnet-4-5-20250929",
35 + max_tokens: 1024,
36 + messages: [{ role: "user", content: `Tu es un assistant de recherche de fichiers. Voici la liste des fichiers:\n\n${fileList}\n\nRecherche: "${query}"\n\nRéponds en JSON:\n{"results": [{"fileId": "...", "name": "...", "relevance": "high|medium|low", "snippet": "courte explication"}], "answer": "résumé"}` }],
37 + });
38 +
39 + try {
40 + const text = response.content[0].type === "text" ? response.content[0].text : "";
41 + const jsonMatch = text.match(/\{[\s\S]*\}/);
42 + if (jsonMatch) return JSON.parse(jsonMatch[0]);
43 + } catch {}
44 + return { results: [], answer: "Aucun résultat trouvé." };
45 +}
46 +
47 +export async function aiAutoTag(fileId: string): Promise<string[]> {
48 + const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, size: true, ocrText: true, storagePath: true } });
49 + if (!file) return [];
50 +
51 + let contentHint = `Fichier: ${file.name}\nType: ${file.mimeType}\nTaille: ${Number(file.size)} bytes`;
52 + if (file.ocrText) contentHint += `\nContenu OCR: ${file.ocrText.substring(0, 500)}`;
53 +
54 + if (file.mimeType.startsWith("text/") || file.mimeType.includes("json") || file.mimeType.includes("xml")) {
55 + try {
56 + const filePath = getFilePath(file.storagePath);
57 + if (existsSync(filePath)) contentHint += `\nContenu: ${readFileSync(filePath, "utf-8").substring(0, 1000)}`;
58 + } catch {}
59 + }
60 +
61 + const response = await anthropic.messages.create({
62 + model: "claude-sonnet-4-5-20250929",
63 + max_tokens: 256,
64 + messages: [{ role: "user", content: `Analyse ce fichier et suggère 3-5 tags pertinents en français.\n\n${contentHint}\n\nRéponds UNIQUEMENT avec un JSON array de strings. Ex: ["finance", "rapport", "2024"]` }],
65 + });
66 +
67 + try {
68 + const text = response.content[0].type === "text" ? response.content[0].text : "";
69 + const match = text.match(/\[[\s\S]*\]/);
70 + if (match) {
71 + const tags = JSON.parse(match[0]) as string[];
72 + await prisma.file.update({ where: { id: fileId }, data: { aiTags: JSON.stringify(tags) } });
73 + return tags;
74 + }
75 + } catch {}
76 + return [];
77 +}
78 +
79 +export async function aiDescribe(fileId: string): Promise<string> {
80 + const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, storagePath: true, ocrText: true } });
81 + if (!file) return "";
82 +
83 + if (file.mimeType.startsWith("image/")) {
84 + try {
85 + const filePath = getFilePath(file.storagePath);
86 + if (existsSync(filePath)) {
87 + const base64 = readFileSync(filePath).toString("base64");
88 + const mediaType = file.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp";
89 + const response = await anthropic.messages.create({
90 + model: "claude-sonnet-4-5-20250929",
91 + max_tokens: 256,
92 + messages: [{ role: "user", content: [{ type: "image", source: { type: "base64", media_type: mediaType, data: base64 } }, { type: "text", text: "Décris cette image en une phrase concise en français." }] }],
93 + });
94 + const desc = response.content[0].type === "text" ? response.content[0].text : "";
95 + await prisma.file.update({ where: { id: fileId }, data: { aiDescription: desc } });
96 + return desc;
97 + }
98 + } catch {}
99 + }
100 +
101 + let hint = `Fichier: ${file.name}\nType: ${file.mimeType}`;
102 + if (file.mimeType.startsWith("text/") || file.mimeType.includes("json")) {
103 + try {
104 + const filePath = getFilePath(file.storagePath);
105 + if (existsSync(filePath)) hint += `\nContenu: ${readFileSync(filePath, "utf-8").substring(0, 2000)}`;
106 + } catch {}
107 + }
108 + if (file.ocrText) hint += `\nOCR: ${file.ocrText.substring(0, 500)}`;
109 +
110 + const response = await anthropic.messages.create({
111 + model: "claude-sonnet-4-5-20250929",
112 + max_tokens: 128,
113 + messages: [{ role: "user", content: `Décris ce fichier en une phrase concise en français:\n\n${hint}` }],
114 + });
115 + const desc = response.content[0].type === "text" ? response.content[0].text : "";
116 + await prisma.file.update({ where: { id: fileId }, data: { aiDescription: desc } });
117 + return desc;
118 +}
119 +
120 +export async function aiOcr(fileId: string): Promise<string> {
121 + const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, storagePath: true } });
122 + if (!file) return "";
123 +
124 + if (file.mimeType.startsWith("image/")) {
125 + try {
126 + const filePath = getFilePath(file.storagePath);
127 + if (!existsSync(filePath)) return "";
128 + const base64 = readFileSync(filePath).toString("base64");
129 + const mediaType = file.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp";
130 + const response = await anthropic.messages.create({
131 + model: "claude-sonnet-4-5-20250929",
132 + max_tokens: 2048,
133 + messages: [{ role: "user", content: [{ type: "image", source: { type: "base64", media_type: mediaType, data: base64 } }, { type: "text", text: "Extrais tout le texte visible dans cette image. Retourne uniquement le texte extrait, sans commentaire." }] }],
134 + });
135 + const text = response.content[0].type === "text" ? response.content[0].text : "";
136 + await prisma.file.update({ where: { id: fileId }, data: { ocrText: text } });
137 + return text;
138 + } catch {}
139 + }
140 +
141 + if (file.mimeType === "application/pdf") {
142 + try {
143 + const filePath = getFilePath(file.storagePath);
144 + if (!existsSync(filePath)) return "";
145 + const base64 = readFileSync(filePath).toString("base64");
146 + const response = await anthropic.messages.create({
147 + model: "claude-sonnet-4-5-20250929",
148 + max_tokens: 4096,
149 + messages: [{ role: "user", content: [{ type: "document", source: { type: "base64", media_type: "application/pdf", data: base64 } }, { type: "text", text: "Extrais tout le texte de ce document PDF. Retourne uniquement le texte extrait." }] as Anthropic.ContentBlockParam[] }],
150 + });
151 + const text = response.content[0].type === "text" ? response.content[0].text : "";
152 + await prisma.file.update({ where: { id: fileId }, data: { ocrText: text } });
153 + return text;
154 + } catch {}
155 + }
156 + return "";
157 +}
158 +
159 +export async function aiChat(message: string, context?: string): Promise<string> {
160 + const response = await anthropic.messages.create({
161 + model: "claude-sonnet-4-5-20250929",
162 + max_tokens: 1024,
163 + system: "Tu es l'assistant IA de SPB Cloud, un cloud personnel privé. Aide l'utilisateur avec ses fichiers et réponds en français.",
164 + messages: [{ role: "user", content: context ? `Contexte:\n${context}\n\nQuestion: ${message}` : message }],
165 + });
166 + return response.content[0].type === "text" ? response.content[0].text : "";
167 +}
added lib/auth.ts +38 −0
@@ -0,0 +1,38 @@
1 +import bcrypt from "bcryptjs";
2 +import { prisma } from "./prisma";
3 +
4 +export async function verifyPassword(password: string, email?: string): Promise<{ valid: boolean; user: { id: string; role: string; twoFactorEnabled: boolean } | null }> {
5 + let user;
6 + if (email) {
7 + user = await prisma.user.findUnique({ where: { email } });
8 + if (!user) return { valid: false, user: null };
9 + const valid = await bcrypt.compare(password, user.passwordHash);
10 + return { valid, user: valid ? { id: user.id, role: user.role, twoFactorEnabled: user.twoFactorEnabled } : null };
11 + }
12 +
13 + // Legacy: try all users (for simple password-only login)
14 + const users = await prisma.user.findMany();
15 + for (const u of users) {
16 + const match = await bcrypt.compare(password, u.passwordHash);
17 + if (match) {
18 + return { valid: true, user: { id: u.id, role: u.role, twoFactorEnabled: u.twoFactorEnabled } };
19 + }
20 + }
21 + return { valid: false, user: null };
22 +}
23 +
24 +export async function hashPassword(password: string): Promise<string> {
25 + return bcrypt.hash(password, 12);
26 +}
27 +
28 +export async function getUserById(userId: string) {
29 + return prisma.user.findUnique({
30 + where: { id: userId },
31 + select: { id: true, email: true, name: true, role: true, twoFactorEnabled: true, createdAt: true },
32 + });
33 +}
34 +
35 +export async function requireAdmin(userId: string): Promise<boolean> {
36 + const user = await prisma.user.findUnique({ where: { id: userId } });
37 + return user?.role === "admin";
38 +}
added lib/backup.ts +60 −0
@@ -0,0 +1,60 @@
1 +import { existsSync, mkdirSync, createWriteStream, readdirSync, statSync } from "fs";
2 +import { rm } from "fs/promises";
3 +import path from "path";
4 +import archiver from "archiver";
5 +import { prisma } from "./prisma";
6 +
7 +const BACKUP_DIR = process.env.BACKUP_DIR || "./backups";
8 +const UPLOAD_DIR = process.env.UPLOAD_DIR || "./uploads";
9 +
10 +if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true });
11 +
12 +export async function createBackup(type: "full" | "incremental" = "full"): Promise<{ id: string; path: string; size: number }> {
13 + const log = await prisma.backupLog.create({ data: { status: "running", type } });
14 +
15 + try {
16 + const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
17 + const backupPath = path.join(BACKUP_DIR, `backup-${type}-${timestamp}.zip`);
18 + const output = createWriteStream(backupPath);
19 + const archive = archiver("zip", { zlib: { level: 9 } });
20 +
21 + const completed = new Promise<number>((resolve, reject) => {
22 + output.on("close", () => resolve(archive.pointer()));
23 + archive.on("error", reject);
24 + });
25 +
26 + archive.pipe(output);
27 + if (existsSync(UPLOAD_DIR)) archive.directory(UPLOAD_DIR, "uploads");
28 + const dbPath = path.resolve("./prisma/dev.db");
29 + if (existsSync(dbPath)) archive.file(dbPath, { name: "database/dev.db" });
30 + await archive.finalize();
31 + const size = await completed;
32 +
33 + await prisma.backupLog.update({ where: { id: log.id }, data: { status: "completed", path: backupPath, size: BigInt(size), completedAt: new Date() } });
34 + return { id: log.id, path: backupPath, size };
35 + } catch (error) {
36 + await prisma.backupLog.update({ where: { id: log.id }, data: { status: "failed", error: (error as Error).message, completedAt: new Date() } });
37 + throw error;
38 + }
39 +}
40 +
41 +export async function listBackups() {
42 + const logs = await prisma.backupLog.findMany({ orderBy: { startedAt: "desc" }, take: 50 });
43 + return logs.map((l) => ({ id: l.id, status: l.status, type: l.type, size: l.size ? Number(l.size) : 0, path: l.path, startedAt: l.startedAt, completedAt: l.completedAt }));
44 +}
45 +
46 +export async function deleteBackup(id: string): Promise<boolean> {
47 + const log = await prisma.backupLog.findUnique({ where: { id } });
48 + if (!log) return false;
49 + if (log.path && existsSync(log.path)) await rm(log.path);
50 + await prisma.backupLog.delete({ where: { id } });
51 + return true;
52 +}
53 +
54 +export function getBackupStats() {
55 + if (!existsSync(BACKUP_DIR)) return { count: 0, totalSize: 0 };
56 + const files = readdirSync(BACKUP_DIR);
57 + let totalSize = 0;
58 + for (const file of files) totalSize += statSync(path.join(BACKUP_DIR, file)).size;
59 + return { count: files.length, totalSize };
60 +}
added lib/config.ts +14 −0
@@ -0,0 +1,14 @@
1 +export const features = {
2 + codeEditor: true,
3 + pdfAnnotations: true,
4 + imageEditor: true,
5 + versioning: true,
6 + tags: true,
7 + waveform: true,
8 + archiveBrowser: true,
9 +};
10 +
11 +export const MAX_VERSIONS_PER_FILE = parseInt(
12 + process.env.MAX_VERSIONS_PER_FILE || "10",
13 + 10
14 +);
added lib/encryption.ts +62 −0
@@ -0,0 +1,62 @@
1 +import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
2 +import { readFileSync, writeFileSync, existsSync } from "fs";
3 +import { getFilePath } from "./storage";
4 +import { prisma } from "./prisma";
5 +
6 +const ALGORITHM = "aes-256-gcm";
7 +
8 +function getKey(): Buffer {
9 + const masterKey = process.env.ENCRYPTION_KEY || "default-encryption-key-change-me!";
10 + return scryptSync(masterKey, "spb-cloud-salt", 32);
11 +}
12 +
13 +export async function encryptFile(fileId: string): Promise<boolean> {
14 + const file = await prisma.file.findUnique({ where: { id: fileId } });
15 + if (!file || file.isEncrypted) return false;
16 +
17 + const filePath = getFilePath(file.storagePath);
18 + if (!existsSync(filePath)) return false;
19 +
20 + const key = getKey();
21 + const iv = randomBytes(16);
22 + const cipher = createCipheriv(ALGORITHM, key, iv);
23 +
24 + const plaintext = readFileSync(filePath);
25 + const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
26 + const authTag = cipher.getAuthTag();
27 + const combined = Buffer.concat([authTag, encrypted]);
28 + writeFileSync(filePath, combined);
29 +
30 + await prisma.file.update({ where: { id: fileId }, data: { isEncrypted: true, encryptionIV: iv.toString("hex") } });
31 + return true;
32 +}
33 +
34 +export async function decryptFile(fileId: string): Promise<Buffer | null> {
35 + const file = await prisma.file.findUnique({ where: { id: fileId } });
36 + if (!file || !file.isEncrypted || !file.encryptionIV) return null;
37 +
38 + const filePath = getFilePath(file.storagePath);
39 + if (!existsSync(filePath)) return null;
40 +
41 + const key = getKey();
42 + const iv = Buffer.from(file.encryptionIV, "hex");
43 + const combined = readFileSync(filePath);
44 + const authTag = combined.subarray(0, 16);
45 + const encrypted = combined.subarray(16);
46 + const decipher = createDecipheriv(ALGORITHM, key, iv);
47 + decipher.setAuthTag(authTag);
48 + return Buffer.concat([decipher.update(encrypted), decipher.final()]);
49 +}
50 +
51 +export async function removeEncryption(fileId: string): Promise<boolean> {
52 + const decrypted = await decryptFile(fileId);
53 + if (!decrypted) return false;
54 +
55 + const file = await prisma.file.findUnique({ where: { id: fileId } });
56 + if (!file) return false;
57 +
58 + const filePath = getFilePath(file.storagePath);
59 + writeFileSync(filePath, decrypted);
60 + await prisma.file.update({ where: { id: fileId }, data: { isEncrypted: false, encryptionIV: null } });
61 + return true;
62 +}
added lib/notifications.ts +35 −0
@@ -0,0 +1,35 @@
1 +import { prisma } from "./prisma";
2 +
3 +export async function createNotification(userId: string, type: string, title: string, message: string, data?: Record<string, unknown>) {
4 + return prisma.notification.create({ data: { userId, type, title, message, data: data ? JSON.stringify(data) : null } });
5 +}
6 +
7 +export async function getNotifications(userId: string, unreadOnly = false) {
8 + const where: Record<string, unknown> = { userId };
9 + if (unreadOnly) where.read = false;
10 + return prisma.notification.findMany({ where, orderBy: { createdAt: "desc" }, take: 50 });
11 +}
12 +
13 +export async function markAsRead(notificationId: string) {
14 + return prisma.notification.update({ where: { id: notificationId }, data: { read: true } });
15 +}
16 +
17 +export async function markAllAsRead(userId: string) {
18 + return prisma.notification.updateMany({ where: { userId, read: false }, data: { read: true } });
19 +}
20 +
21 +export async function getUnreadCount(userId: string): Promise<number> {
22 + return prisma.notification.count({ where: { userId, read: false } });
23 +}
24 +
25 +export async function notifyUploadComplete(userId: string, fileName: string, fileId: string) {
26 + return createNotification(userId, "upload_complete", "Upload terminé", `${fileName} uploadé avec succès`, { fileId });
27 +}
28 +
29 +export async function notifyBackupComplete(userId: string, backupId: string) {
30 + return createNotification(userId, "backup_complete", "Backup terminé", "Le backup a été créé avec succès", { backupId });
31 +}
32 +
33 +export async function notifyAiComplete(userId: string, fileName: string, action: string) {
34 + return createNotification(userId, "ai_complete", "Analyse IA", `${action} terminée pour ${fileName}`);
35 +}
added lib/prisma.ts +9 −0
@@ -0,0 +1,9 @@
1 +import { PrismaClient } from "@prisma/client";
2 +
3 +const globalForPrisma = globalThis as unknown as {
4 + prisma: PrismaClient | undefined;
5 +};
6 +
7 +export const prisma = globalForPrisma.prisma ?? new PrismaClient();
8 +
9 +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
added lib/rate-limit.ts +29 −0
@@ -0,0 +1,29 @@
1 +import { RateLimiterMemory } from "rate-limiter-flexible";
2 +
3 +const loginLimiter = new RateLimiterMemory({
4 + points: 5,
5 + duration: 60, // 5 attempts per minute
6 +});
7 +
8 +const apiLimiter = new RateLimiterMemory({
9 + points: 100,
10 + duration: 60, // 100 requests per minute
11 +});
12 +
13 +export async function checkLoginRateLimit(ip: string): Promise<boolean> {
14 + try {
15 + await loginLimiter.consume(ip);
16 + return true;
17 + } catch {
18 + return false;
19 + }
20 +}
21 +
22 +export async function checkApiRateLimit(ip: string): Promise<boolean> {
23 + try {
24 + await apiLimiter.consume(ip);
25 + return true;
26 + } catch {
27 + return false;
28 + }
29 +}
added lib/session.ts +25 −0
@@ -0,0 +1,25 @@
1 +import { SessionOptions } from "iron-session";
2 +
3 +export interface SessionData {
4 + isLoggedIn: boolean;
5 + userId?: string;
6 + userRole?: string;
7 + csrfToken?: string;
8 + pendingTwoFactor?: boolean;
9 + pendingUserId?: string;
10 +}
11 +
12 +export const sessionOptions: SessionOptions = {
13 + password: process.env.SESSION_SECRET || "spb-cloud-secret-key-change-in-production-min-32-chars",
14 + cookieName: "spb-cloud-session",
15 + cookieOptions: {
16 + secure: process.env.NODE_ENV === "production",
17 + httpOnly: true,
18 + sameSite: "lax" as const,
19 + maxAge: 60 * 60 * 24,
20 + },
21 +};
22 +
23 +export const defaultSession: SessionData = {
24 + isLoggedIn: false,
25 +};
added lib/storage.ts +193 −0
@@ -0,0 +1,193 @@
1 +import { createReadStream, createWriteStream, existsSync, mkdirSync } from "fs";
2 +import { unlink, copyFile, readdir, rm, stat } from "fs/promises";
3 +import path from "path";
4 +import { v4 as uuidv4 } from "uuid";
5 +import { ReadStream } from "fs";
6 +
7 +const UPLOAD_DIR = process.env.UPLOAD_DIR || "./uploads";
8 +const FILES_DIR = path.join(UPLOAD_DIR, "files");
9 +const CHUNKS_DIR = path.join(UPLOAD_DIR, "chunks");
10 +const VERSIONS_DIR = path.join(UPLOAD_DIR, "versions");
11 +
12 +// Ensure directories exist
13 +[FILES_DIR, CHUNKS_DIR, VERSIONS_DIR].forEach((dir) => {
14 + if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
15 +});
16 +
17 +function getExtension(filename: string): string {
18 + const ext = path.extname(filename);
19 + return ext || "";
20 +}
21 +
22 +export async function saveFile(
23 + buffer: Buffer,
24 + originalName: string
25 +): Promise<{ storagePath: string; size: number }> {
26 + const ext = getExtension(originalName);
27 + const storagePath = `${uuidv4()}${ext}`;
28 + const fullPath = path.join(FILES_DIR, storagePath);
29 +
30 + await new Promise<void>((resolve, reject) => {
31 + const stream = createWriteStream(fullPath);
32 + stream.write(buffer);
33 + stream.end();
34 + stream.on("finish", resolve);
35 + stream.on("error", reject);
36 + });
37 +
38 + return { storagePath, size: buffer.length };
39 +}
40 +
41 +export async function saveChunk(
42 + buffer: Buffer,
43 + uploadId: string,
44 + chunkIndex: number
45 +): Promise<void> {
46 + const chunkDir = path.join(CHUNKS_DIR, uploadId);
47 + if (!existsSync(chunkDir)) mkdirSync(chunkDir, { recursive: true });
48 +
49 + const chunkPath = path.join(chunkDir, `chunk-${chunkIndex}`);
50 + await new Promise<void>((resolve, reject) => {
51 + const stream = createWriteStream(chunkPath);
52 + stream.write(buffer);
53 + stream.end();
54 + stream.on("finish", resolve);
55 + stream.on("error", reject);
56 + });
57 +}
58 +
59 +export async function assembleChunks(
60 + uploadId: string,
61 + totalChunks: number,
62 + originalName: string
63 +): Promise<{ storagePath: string; size: number }> {
64 + const ext = getExtension(originalName);
65 + const storagePath = `${uuidv4()}${ext}`;
66 + const fullPath = path.join(FILES_DIR, storagePath);
67 + const chunkDir = path.join(CHUNKS_DIR, uploadId);
68 +
69 + const writeStream = createWriteStream(fullPath);
70 + let totalSize = 0;
71 +
72 + for (let i = 0; i < totalChunks; i++) {
73 + const chunkPath = path.join(chunkDir, `chunk-${i}`);
74 + const chunkBuffer = await new Promise<Buffer>((resolve, reject) => {
75 + const chunks: Buffer[] = [];
76 + const readStream = createReadStream(chunkPath);
77 + readStream.on("data", (chunk) => chunks.push(chunk as Buffer));
78 + readStream.on("end", () => resolve(Buffer.concat(chunks)));
79 + readStream.on("error", reject);
80 + });
81 + writeStream.write(chunkBuffer);
82 + totalSize += chunkBuffer.length;
83 + }
84 +
85 + await new Promise<void>((resolve) => writeStream.end(resolve));
86 +
87 + // Clean up chunks
88 + await rm(chunkDir, { recursive: true, force: true });
89 +
90 + return { storagePath, size: totalSize };
91 +}
92 +
93 +export async function deleteFile(storagePath: string): Promise<void> {
94 + const fullPath = path.join(FILES_DIR, storagePath);
95 + if (existsSync(fullPath)) {
96 + await unlink(fullPath);
97 + }
98 +}
99 +
100 +export async function duplicateFile(
101 + storagePath: string,
102 + originalName: string
103 +): Promise<{ storagePath: string; size: number }> {
104 + const ext = getExtension(originalName);
105 + const newStoragePath = `${uuidv4()}${ext}`;
106 + const srcPath = path.join(FILES_DIR, storagePath);
107 + const destPath = path.join(FILES_DIR, newStoragePath);
108 +
109 + await copyFile(srcPath, destPath);
110 + const stats = await stat(destPath);
111 +
112 + return { storagePath: newStoragePath, size: Number(stats.size) };
113 +}
114 +
115 +export function getFilePath(storagePath: string): string {
116 + return path.resolve(FILES_DIR, storagePath);
117 +}
118 +
119 +export function getFileStream(storagePath: string): ReadStream {
120 + const fullPath = path.join(FILES_DIR, storagePath);
121 + return createReadStream(fullPath);
122 +}
123 +
124 +export async function getStorageUsage(): Promise<{
125 + totalSize: number;
126 + fileCount: number;
127 +}> {
128 + let totalSize = 0;
129 + let fileCount = 0;
130 +
131 + if (existsSync(FILES_DIR)) {
132 + const files = await readdir(FILES_DIR);
133 + for (const file of files) {
134 + const filePath = path.join(FILES_DIR, file);
135 + const stats = await stat(filePath);
136 + totalSize += stats.size;
137 + fileCount++;
138 + }
139 + }
140 +
141 + return { totalSize, fileCount };
142 +}
143 +
144 +// --- Version management ---
145 +
146 +export async function saveVersion(
147 + currentStoragePath: string,
148 + originalName: string
149 +): Promise<{ storagePath: string; size: number }> {
150 + const ext = getExtension(originalName);
151 + const versionStoragePath = `${uuidv4()}${ext}`;
152 + const srcPath = path.join(FILES_DIR, currentStoragePath);
153 + const destPath = path.join(VERSIONS_DIR, versionStoragePath);
154 +
155 + await copyFile(srcPath, destPath);
156 + const stats = await stat(destPath);
157 +
158 + return { storagePath: versionStoragePath, size: Number(stats.size) };
159 +}
160 +
161 +export function getVersionPath(storagePath: string): string {
162 + return path.resolve(VERSIONS_DIR, storagePath);
163 +}
164 +
165 +export async function deleteVersion(storagePath: string): Promise<void> {
166 + const fullPath = path.join(VERSIONS_DIR, storagePath);
167 + if (existsSync(fullPath)) {
168 + await unlink(fullPath);
169 + }
170 +}
171 +
172 +export async function restoreVersion(
173 + versionStoragePath: string,
174 + currentStoragePath: string
175 +): Promise<void> {
176 + const srcPath = path.join(VERSIONS_DIR, versionStoragePath);
177 + const destPath = path.join(FILES_DIR, currentStoragePath);
178 + await copyFile(srcPath, destPath);
179 +}
180 +
181 +export async function saveFileFromBuffer(
182 + buffer: Buffer,
183 + storagePath: string
184 +): Promise<void> {
185 + const fullPath = path.join(FILES_DIR, storagePath);
186 + await new Promise<void>((resolve, reject) => {
187 + const stream = createWriteStream(fullPath);
188 + stream.write(buffer);
189 + stream.end();
190 + stream.on("finish", resolve);
191 + stream.on("error", reject);
192 + });
193 +}
added lib/utils.ts +161 −0
@@ -0,0 +1,161 @@
1 +export function formatFileSize(bytes: number): string {
2 + if (bytes === 0) return "0 B";
3 + const k = 1024;
4 + const sizes = ["B", "KB", "MB", "GB", "TB"];
5 + const i = Math.floor(Math.log(bytes) / Math.log(k));
6 + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
7 +}
8 +
9 +export function formatDate(date: string | Date): string {
10 + return new Intl.DateTimeFormat("fr-CA", {
11 + year: "numeric",
12 + month: "short",
13 + day: "numeric",
14 + hour: "2-digit",
15 + minute: "2-digit",
16 + }).format(new Date(date));
17 +}
18 +
19 +export function getFileIcon(mimeType: string): string {
20 + if (mimeType.startsWith("image/")) return "image";
21 + if (mimeType.startsWith("video/")) return "video";
22 + if (mimeType.startsWith("audio/")) return "audio";
23 + if (mimeType === "application/pdf") return "pdf";
24 + if (mimeType.includes("spreadsheet") || mimeType === "text/csv") return "spreadsheet";
25 + if (mimeType.includes("document") || mimeType.includes("word")) return "document";
26 + if (mimeType.includes("presentation")) return "presentation";
27 + if (isArchiveMime(mimeType)) return "archive";
28 + if (mimeType.startsWith("text/") || mimeType.includes("json") || mimeType.includes("xml") || mimeType.includes("javascript") || mimeType.includes("typescript")) return "code";
29 + return "file";
30 +}
31 +
32 +export function isArchiveMime(mimeType: string): boolean {
33 + return (
34 + mimeType === "application/zip" ||
35 + mimeType === "application/x-zip-compressed" ||
36 + mimeType === "application/x-rar-compressed" ||
37 + mimeType === "application/x-7z-compressed" ||
38 + mimeType === "application/x-tar" ||
39 + mimeType === "application/gzip" ||
40 + mimeType.includes("zip") ||
41 + mimeType.includes("archive") ||
42 + mimeType.includes("compressed")
43 + );
44 +}
45 +
46 +export function isPreviewable(mimeType: string): boolean {
47 + return (
48 + mimeType.startsWith("image/") ||
49 + mimeType.startsWith("video/") ||
50 + mimeType.startsWith("audio/") ||
51 + mimeType === "application/pdf" ||
52 + mimeType.startsWith("text/") ||
53 + mimeType === "text/csv" ||
54 + mimeType.includes("json") ||
55 + mimeType.includes("xml") ||
56 + mimeType.includes("javascript") ||
57 + mimeType.includes("typescript") ||
58 + mimeType.includes("markdown") ||
59 + isArchiveMime(mimeType)
60 + );
61 +}
62 +
63 +export function isTextMime(mimeType: string): boolean {
64 + return (
65 + mimeType.startsWith("text/") ||
66 + mimeType.includes("json") ||
67 + mimeType.includes("xml") ||
68 + mimeType.includes("javascript") ||
69 + mimeType.includes("typescript") ||
70 + mimeType.includes("markdown")
71 + );
72 +}
73 +
74 +export function isCodeMime(mimeType: string): boolean {
75 + return (
76 + mimeType.includes("javascript") ||
77 + mimeType.includes("typescript") ||
78 + mimeType.includes("json") ||
79 + mimeType.includes("xml") ||
80 + mimeType.includes("css") ||
81 + mimeType.includes("html") ||
82 + mimeType.includes("python") ||
83 + mimeType.includes("java") ||
84 + mimeType.includes("x-c") ||
85 + mimeType.includes("rust") ||
86 + mimeType.includes("go") ||
87 + mimeType.includes("yaml") ||
88 + mimeType.includes("toml") ||
89 + mimeType.includes("sql") ||
90 + mimeType.includes("shell") ||
91 + mimeType.includes("bash")
92 + );
93 +}
94 +
95 +export function getLanguageFromMime(mimeType: string): string {
96 + const map: Record<string, string> = {
97 + "application/javascript": "javascript",
98 + "text/javascript": "javascript",
99 + "application/typescript": "typescript",
100 + "text/typescript": "typescript",
101 + "application/json": "json",
102 + "text/html": "html",
103 + "text/css": "css",
104 + "text/xml": "xml",
105 + "application/xml": "xml",
106 + "text/markdown": "markdown",
107 + "text/x-python": "python",
108 + "text/x-java": "java",
109 + "text/x-c": "c",
110 + "text/x-cpp": "cpp",
111 + "text/x-rust": "rust",
112 + "text/x-go": "go",
113 + "text/yaml": "yaml",
114 + "application/x-yaml": "yaml",
115 + "text/x-toml": "toml",
116 + "text/x-sql": "sql",
117 + "text/x-shellscript": "shell",
118 + "application/x-sh": "shell",
119 + };
120 + return map[mimeType] || "text";
121 +}
122 +
123 +export function getLanguageFromFilename(filename: string): string {
124 + const ext = filename.split(".").pop()?.toLowerCase() || "";
125 + const map: Record<string, string> = {
126 + js: "javascript",
127 + jsx: "javascript",
128 + ts: "typescript",
129 + tsx: "typescript",
130 + json: "json",
131 + html: "html",
132 + htm: "html",
133 + css: "css",
134 + scss: "css",
135 + xml: "xml",
136 + md: "markdown",
137 + py: "python",
138 + java: "java",
139 + c: "c",
140 + h: "c",
141 + cpp: "cpp",
142 + hpp: "cpp",
143 + rs: "rust",
144 + go: "go",
145 + yaml: "yaml",
146 + yml: "yaml",
147 + toml: "toml",
148 + sql: "sql",
149 + sh: "shell",
150 + bash: "shell",
151 + zsh: "shell",
152 + rb: "ruby",
153 + php: "php",
154 + swift: "swift",
155 + kt: "kotlin",
156 + r: "r",
157 + lua: "lua",
158 + dockerfile: "dockerfile",
159 + };
160 + return map[ext] || "text";
161 +}
added middleware.ts +42 −0
@@ -0,0 +1,42 @@
1 +import { NextRequest, NextResponse } from "next/server";
2 +import { getIronSession } from "iron-session";
3 +import { SessionData, sessionOptions } from "./lib/session";
4 +
5 +const publicPaths = [
6 + "/login",
7 + "/api/auth/login",
8 + "/api/auth/2fa/verify",
9 + "/shared",
10 + "/api/shared",
11 + "/api/v1/",
12 + "/_next",
13 + "/favicon.ico",
14 +];
15 +
16 +export async function middleware(request: NextRequest) {
17 + const { pathname } = request.nextUrl;
18 +
19 + // Allow public paths
20 + if (publicPaths.some((path) => pathname.startsWith(path))) {
21 + return NextResponse.next();
22 + }
23 +
24 + // Check session
25 + const response = NextResponse.next();
26 + const session = await getIronSession<SessionData>(request, response, sessionOptions);
27 +
28 + if (!session.isLoggedIn) {
29 + if (pathname.startsWith("/api/")) {
30 + return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
31 + }
32 + return NextResponse.redirect(new URL("/login", request.url));
33 + }
34 +
35 + return response;
36 +}
37 +
38 +export const config = {
39 + matcher: [
40 + "/((?!_next/static|_next/image|favicon.ico).*)",
41 + ],
42 +};
added next-env.d.ts +5 −0
@@ -0,0 +1,5 @@
1 +/// <reference types="next" />
2 +/// <reference types="next/image-types/global" />
3 +
4 +// NOTE: This file should not be edited
5 +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
added next.config.mjs +32 −0
@@ -0,0 +1,32 @@
1 +/** @type {import('next').NextConfig} */
2 +const nextConfig = {
3 + experimental: {
4 + serverComponentsExternalPackages: ["bcryptjs"],
5 + },
6 + async headers() {
7 + return [
8 + {
9 + source: "/(.*)",
10 + headers: [
11 + { key: "X-Content-Type-Options", value: "nosniff" },
12 + { key: "X-Frame-Options", value: "SAMEORIGIN" },
13 + { key: "X-XSS-Protection", value: "1; mode=block" },
14 + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
15 + ],
16 + },
17 + {
18 + source: "/pdf.worker.min.mjs",
19 + headers: [
20 + { key: "Content-Type", value: "application/javascript" },
21 + ],
22 + },
23 + ];
24 + },
25 + webpack: (config) => {
26 + // Avoid bundling pdfjs-dist worker
27 + config.resolve.alias.canvas = false;
28 + return config;
29 + },
30 +};
31 +
32 +export default nextConfig;
added package-lock.json +9544 −0
@@ -0,0 +1,10484 @@
1 +{
2 + "name": "spb-private-cloud",
3 + "version": "1.0.0",
4 + "lockfileVersion": 3,
5 + "requires": true,
6 + "packages": {
7 + "": {
8 + "name": "spb-private-cloud",
9 + "version": "1.0.0",
10 + "hasInstallScript": true,
11 + "dependencies": {
12 + "@anthropic-ai/sdk": "^0.75.0",
13 + "@codemirror/autocomplete": "^6.20.0",
14 + "@codemirror/commands": "^6.10.2",
15 + "@codemirror/lang-cpp": "^6.0.3",
16 + "@codemirror/lang-css": "^6.3.1",
17 + "@codemirror/lang-go": "^6.0.1",
18 + "@codemirror/lang-html": "^6.4.11",
19 + "@codemirror/lang-java": "^6.0.2",
20 + "@codemirror/lang-javascript": "^6.2.4",
21 + "@codemirror/lang-json": "^6.0.2",
22 + "@codemirror/lang-markdown": "^6.5.0",
23 + "@codemirror/lang-php": "^6.0.2",
24 + "@codemirror/lang-python": "^6.2.1",
25 + "@codemirror/lang-rust": "^6.0.2",
26 + "@codemirror/lang-sql": "^6.10.0",
27 + "@codemirror/lang-xml": "^6.1.0",
28 + "@codemirror/language": "^6.12.1",
29 + "@codemirror/search": "^6.6.0",
30 + "@codemirror/state": "^6.5.4",
31 + "@codemirror/theme-one-dark": "^6.1.3",
32 + "@codemirror/view": "^6.39.14",
33 + "@prisma/client": "^5.22.0",
34 + "archiver": "^7.0.1",
35 + "bcryptjs": "^3.0.3",
36 + "diff": "^8.0.3",
37 + "framer-motion": "^12.34.0",
38 + "iron-session": "^8.0.4",
39 + "jszip": "^3.10.1",
40 + "lucide-react": "^0.564.0",
41 + "mime-types": "^3.0.2",
42 + "next": "14.2.35",
43 + "otplib": "^13.3.0",
44 + "papaparse": "^5.5.3",
45 + "pdf-lib": "^1.17.1",
46 + "pdfjs-dist": "^5.4.624",
47 + "prisma": "^5.22.0",
48 + "qrcode": "^1.5.4",
49 + "rate-limiter-flexible": "^9.1.1",
50 + "react": "^18",
51 + "react-dom": "^18",
52 + "react-dropzone": "^15.0.0",
53 + "react-hot-toast": "^2.6.0",
54 + "react-markdown": "^10.1.0",
55 + "react-syntax-highlighter": "^16.1.0",
56 + "sharp": "^0.34.5",
57 + "uuid": "^13.0.0",
58 + "wavesurfer.js": "^7.12.1"
59 + },
60 + "devDependencies": {
61 + "@types/archiver": "^7.0.0",
62 + "@types/bcryptjs": "^2.4.6",
63 + "@types/diff": "^7.0.2",
64 + "@types/mime-types": "^3.0.1",
65 + "@types/node": "^20",
66 + "@types/papaparse": "^5.5.2",
67 + "@types/qrcode": "^1.5.6",
68 + "@types/react": "^18",
69 + "@types/react-dom": "^18",
70 + "@types/react-syntax-highlighter": "^15.5.13",
71 + "@types/sharp": "^0.31.1",
72 + "@types/uuid": "^10.0.0",
73 + "eslint": "^8",
74 + "eslint-config-next": "14.2.35",
75 + "postcss": "^8",
76 + "tailwindcss": "^3.4.1",
77 + "tsx": "^4.21.0",
78 + "typescript": "^5"
79 + }
80 + },
81 + "node_modules/@alloc/quick-lru": {
82 + "version": "5.2.0",
83 + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
84 + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
85 + "dev": true,
86 + "license": "MIT",
87 + "engines": {
88 + "node": ">=10"
89 + },
90 + "funding": {
91 + "url": "https://github.com/sponsors/sindresorhus"
92 + }
93 + },
94 + "node_modules/@anthropic-ai/sdk": {
95 + "version": "0.75.0",
96 + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.75.0.tgz",
97 + "integrity": "sha512-Xr/BESTYMOnpyOK7Gxie2LWc+G5f9+xZEYVAuWtDdYDtINocoe046gszFMeRJZ2FfySyGxdF3OHKYwB6/+p4uQ==",
98 + "license": "MIT",
99 + "dependencies": {
100 + "json-schema-to-ts": "^3.1.1"
101 + },
102 + "bin": {
103 + "anthropic-ai-sdk": "bin/cli"
104 + },
105 + "peerDependencies": {
106 + "zod": "^3.25.0 || ^4.0.0"
107 + },
108 + "peerDependenciesMeta": {
109 + "zod": {
110 + "optional": true
111 + }
112 + }
113 + },
114 + "node_modules/@babel/runtime": {
115 + "version": "7.28.6",
116 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
117 + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
118 + "license": "MIT",
119 + "engines": {
120 + "node": ">=6.9.0"
121 + }
122 + },
123 + "node_modules/@codemirror/autocomplete": {
124 + "version": "6.20.0",
125 + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
126 + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
127 + "license": "MIT",
128 + "dependencies": {
129 + "@codemirror/language": "^6.0.0",
130 + "@codemirror/state": "^6.0.0",
131 + "@codemirror/view": "^6.17.0",
132 + "@lezer/common": "^1.0.0"
133 + }
134 + },
135 + "node_modules/@codemirror/commands": {
136 + "version": "6.10.2",
137 + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz",
138 + "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==",
139 + "license": "MIT",
140 + "dependencies": {
141 + "@codemirror/language": "^6.0.0",
142 + "@codemirror/state": "^6.4.0",
143 + "@codemirror/view": "^6.27.0",
144 + "@lezer/common": "^1.1.0"
145 + }
146 + },
147 + "node_modules/@codemirror/lang-cpp": {
148 + "version": "6.0.3",
149 + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz",
150 + "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==",
151 + "license": "MIT",
152 + "dependencies": {
153 + "@codemirror/language": "^6.0.0",
154 + "@lezer/cpp": "^1.0.0"
155 + }
156 + },
157 + "node_modules/@codemirror/lang-css": {
158 + "version": "6.3.1",
159 + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
160 + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
161 + "license": "MIT",
162 + "dependencies": {
163 + "@codemirror/autocomplete": "^6.0.0",
164 + "@codemirror/language": "^6.0.0",
165 + "@codemirror/state": "^6.0.0",
166 + "@lezer/common": "^1.0.2",
167 + "@lezer/css": "^1.1.7"
168 + }
169 + },
170 + "node_modules/@codemirror/lang-go": {
171 + "version": "6.0.1",
172 + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
173 + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
174 + "license": "MIT",
175 + "dependencies": {
176 + "@codemirror/autocomplete": "^6.0.0",
177 + "@codemirror/language": "^6.6.0",
178 + "@codemirror/state": "^6.0.0",
179 + "@lezer/common": "^1.0.0",
180 + "@lezer/go": "^1.0.0"
181 + }
182 + },
183 + "node_modules/@codemirror/lang-html": {
184 + "version": "6.4.11",
185 + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
186 + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
187 + "license": "MIT",
188 + "dependencies": {
189 + "@codemirror/autocomplete": "^6.0.0",
190 + "@codemirror/lang-css": "^6.0.0",
191 + "@codemirror/lang-javascript": "^6.0.0",
192 + "@codemirror/language": "^6.4.0",
193 + "@codemirror/state": "^6.0.0",
194 + "@codemirror/view": "^6.17.0",
195 + "@lezer/common": "^1.0.0",
196 + "@lezer/css": "^1.1.0",
197 + "@lezer/html": "^1.3.12"
198 + }
199 + },
200 + "node_modules/@codemirror/lang-java": {
201 + "version": "6.0.2",
202 + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
203 + "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
204 + "license": "MIT",
205 + "dependencies": {
206 + "@codemirror/language": "^6.0.0",
207 + "@lezer/java": "^1.0.0"
208 + }
209 + },
210 + "node_modules/@codemirror/lang-javascript": {
211 + "version": "6.2.4",
212 + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
213 + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
214 + "license": "MIT",
215 + "dependencies": {
216 + "@codemirror/autocomplete": "^6.0.0",
217 + "@codemirror/language": "^6.6.0",
218 + "@codemirror/lint": "^6.0.0",
219 + "@codemirror/state": "^6.0.0",
220 + "@codemirror/view": "^6.17.0",
221 + "@lezer/common": "^1.0.0",
222 + "@lezer/javascript": "^1.0.0"
223 + }
224 + },
225 + "node_modules/@codemirror/lang-json": {
226 + "version": "6.0.2",
227 + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
228 + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
229 + "license": "MIT",
230 + "dependencies": {
231 + "@codemirror/language": "^6.0.0",
232 + "@lezer/json": "^1.0.0"
233 + }
234 + },
235 + "node_modules/@codemirror/lang-markdown": {
236 + "version": "6.5.0",
237 + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
238 + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
239 + "license": "MIT",
240 + "dependencies": {
241 + "@codemirror/autocomplete": "^6.7.1",
242 + "@codemirror/lang-html": "^6.0.0",
243 + "@codemirror/language": "^6.3.0",
244 + "@codemirror/state": "^6.0.0",
245 + "@codemirror/view": "^6.0.0",
246 + "@lezer/common": "^1.2.1",
247 + "@lezer/markdown": "^1.0.0"
248 + }
249 + },
250 + "node_modules/@codemirror/lang-php": {
251 + "version": "6.0.2",
252 + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz",
253 + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==",
254 + "license": "MIT",
255 + "dependencies": {
256 + "@codemirror/lang-html": "^6.0.0",
257 + "@codemirror/language": "^6.0.0",
258 + "@codemirror/state": "^6.0.0",
259 + "@lezer/common": "^1.0.0",
260 + "@lezer/php": "^1.0.0"
261 + }
262 + },
263 + "node_modules/@codemirror/lang-python": {
264 + "version": "6.2.1",
265 + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
266 + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
267 + "license": "MIT",
268 + "dependencies": {
269 + "@codemirror/autocomplete": "^6.3.2",
270 + "@codemirror/language": "^6.8.0",
271 + "@codemirror/state": "^6.0.0",
272 + "@lezer/common": "^1.2.1",
273 + "@lezer/python": "^1.1.4"
274 + }
275 + },
276 + "node_modules/@codemirror/lang-rust": {
277 + "version": "6.0.2",
278 + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
279 + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
280 + "license": "MIT",
281 + "dependencies": {
282 + "@codemirror/language": "^6.0.0",
283 + "@lezer/rust": "^1.0.0"
284 + }
285 + },
286 + "node_modules/@codemirror/lang-sql": {
287 + "version": "6.10.0",
288 + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
289 + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
290 + "license": "MIT",
291 + "dependencies": {
292 + "@codemirror/autocomplete": "^6.0.0",
293 + "@codemirror/language": "^6.0.0",
294 + "@codemirror/state": "^6.0.0",
295 + "@lezer/common": "^1.2.0",
296 + "@lezer/highlight": "^1.0.0",
297 + "@lezer/lr": "^1.0.0"
298 + }
299 + },
300 + "node_modules/@codemirror/lang-xml": {
301 + "version": "6.1.0",
302 + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
303 + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
304 + "license": "MIT",
305 + "dependencies": {
306 + "@codemirror/autocomplete": "^6.0.0",
307 + "@codemirror/language": "^6.4.0",
308 + "@codemirror/state": "^6.0.0",
309 + "@codemirror/view": "^6.0.0",
310 + "@lezer/common": "^1.0.0",
311 + "@lezer/xml": "^1.0.0"
312 + }
313 + },
314 + "node_modules/@codemirror/language": {
315 + "version": "6.12.1",
316 + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
317 + "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
318 + "license": "MIT",
319 + "dependencies": {
320 + "@codemirror/state": "^6.0.0",
321 + "@codemirror/view": "^6.23.0",
322 + "@lezer/common": "^1.5.0",
323 + "@lezer/highlight": "^1.0.0",
324 + "@lezer/lr": "^1.0.0",
325 + "style-mod": "^4.0.0"
326 + }
327 + },
328 + "node_modules/@codemirror/lint": {
329 + "version": "6.9.4",
330 + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.4.tgz",
331 + "integrity": "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==",
332 + "license": "MIT",
333 + "dependencies": {
334 + "@codemirror/state": "^6.0.0",
335 + "@codemirror/view": "^6.35.0",
336 + "crelt": "^1.0.5"
337 + }
338 + },
339 + "node_modules/@codemirror/search": {
340 + "version": "6.6.0",
341 + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz",
342 + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
343 + "license": "MIT",
344 + "dependencies": {
345 + "@codemirror/state": "^6.0.0",
346 + "@codemirror/view": "^6.37.0",
347 + "crelt": "^1.0.5"
348 + }
349 + },
350 + "node_modules/@codemirror/state": {
351 + "version": "6.5.4",
352 + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
353 + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
354 + "license": "MIT",
355 + "dependencies": {
356 + "@marijn/find-cluster-break": "^1.0.0"
357 + }
358 + },
359 + "node_modules/@codemirror/theme-one-dark": {
360 + "version": "6.1.3",
361 + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
362 + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
363 + "license": "MIT",
364 + "dependencies": {
365 + "@codemirror/language": "^6.0.0",
366 + "@codemirror/state": "^6.0.0",
367 + "@codemirror/view": "^6.0.0",
368 + "@lezer/highlight": "^1.0.0"
369 + }
370 + },
371 + "node_modules/@codemirror/view": {
372 + "version": "6.39.14",
373 + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.14.tgz",
374 + "integrity": "sha512-WJcvgHm/6Q7dvGT0YFv/6PSkoc36QlR0VCESS6x9tGsnF1lWLmmYxOgX3HH6v8fo6AvSLgpcs+H0Olre6MKXlg==",
375 + "license": "MIT",
376 + "dependencies": {
377 + "@codemirror/state": "^6.5.0",
378 + "crelt": "^1.0.6",
379 + "style-mod": "^4.1.0",
380 + "w3c-keyname": "^2.2.4"
381 + }
382 + },
383 + "node_modules/@emnapi/core": {
384 + "version": "1.8.1",
385 + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
386 + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
387 + "dev": true,
388 + "license": "MIT",
389 + "optional": true,
390 + "dependencies": {
391 + "@emnapi/wasi-threads": "1.1.0",
392 + "tslib": "^2.4.0"
393 + }
394 + },
395 + "node_modules/@emnapi/runtime": {
396 + "version": "1.8.1",
397 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
398 + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
399 + "license": "MIT",
400 + "optional": true,
401 + "dependencies": {
402 + "tslib": "^2.4.0"
403 + }
404 + },
405 + "node_modules/@emnapi/wasi-threads": {
406 + "version": "1.1.0",
407 + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
408 + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
409 + "dev": true,
410 + "license": "MIT",
411 + "optional": true,
412 + "dependencies": {
413 + "tslib": "^2.4.0"
414 + }
415 + },
416 + "node_modules/@esbuild/aix-ppc64": {
417 + "version": "0.27.3",
418 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
419 + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
420 + "cpu": [
421 + "ppc64"
422 + ],
423 + "dev": true,
424 + "license": "MIT",
425 + "optional": true,
426 + "os": [
427 + "aix"
428 + ],
429 + "engines": {
430 + "node": ">=18"
431 + }
432 + },
433 + "node_modules/@esbuild/android-arm": {
434 + "version": "0.27.3",
435 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
436 + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
437 + "cpu": [
438 + "arm"
439 + ],
440 + "dev": true,
441 + "license": "MIT",
442 + "optional": true,
443 + "os": [
444 + "android"
445 + ],
446 + "engines": {
447 + "node": ">=18"
448 + }
449 + },
450 + "node_modules/@esbuild/android-arm64": {
451 + "version": "0.27.3",
452 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
453 + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
454 + "cpu": [
455 + "arm64"
456 + ],
457 + "dev": true,
458 + "license": "MIT",
459 + "optional": true,
460 + "os": [
461 + "android"
462 + ],
463 + "engines": {
464 + "node": ">=18"
465 + }
466 + },
467 + "node_modules/@esbuild/android-x64": {
468 + "version": "0.27.3",
469 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
470 + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
471 + "cpu": [
472 + "x64"
473 + ],
474 + "dev": true,
475 + "license": "MIT",
476 + "optional": true,
477 + "os": [
478 + "android"
479 + ],
480 + "engines": {
481 + "node": ">=18"
482 + }
483 + },
484 + "node_modules/@esbuild/darwin-arm64": {
485 + "version": "0.27.3",
486 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
487 + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
488 + "cpu": [
489 + "arm64"
490 + ],
491 + "dev": true,
492 + "license": "MIT",
493 + "optional": true,
494 + "os": [
495 + "darwin"
496 + ],
497 + "engines": {
498 + "node": ">=18"
499 + }
500 + },
501 + "node_modules/@esbuild/darwin-x64": {
502 + "version": "0.27.3",
503 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
504 + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
505 + "cpu": [
506 + "x64"
507 + ],
508 + "dev": true,
509 + "license": "MIT",
510 + "optional": true,
511 + "os": [
512 + "darwin"
513 + ],
514 + "engines": {
515 + "node": ">=18"
516 + }
517 + },
518 + "node_modules/@esbuild/freebsd-arm64": {
519 + "version": "0.27.3",
520 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
521 + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
522 + "cpu": [
523 + "arm64"
524 + ],
525 + "dev": true,
526 + "license": "MIT",
527 + "optional": true,
528 + "os": [
529 + "freebsd"
530 + ],
531 + "engines": {
532 + "node": ">=18"
533 + }
534 + },
535 + "node_modules/@esbuild/freebsd-x64": {
536 + "version": "0.27.3",
537 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
538 + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
539 + "cpu": [
540 + "x64"
541 + ],
542 + "dev": true,
543 + "license": "MIT",
544 + "optional": true,
545 + "os": [
546 + "freebsd"
547 + ],
548 + "engines": {
549 + "node": ">=18"
550 + }
551 + },
552 + "node_modules/@esbuild/linux-arm": {
553 + "version": "0.27.3",
554 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
555 + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
556 + "cpu": [
557 + "arm"
558 + ],
559 + "dev": true,
560 + "license": "MIT",
561 + "optional": true,
562 + "os": [
563 + "linux"
564 + ],
565 + "engines": {
566 + "node": ">=18"
567 + }
568 + },
569 + "node_modules/@esbuild/linux-arm64": {
570 + "version": "0.27.3",
571 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
572 + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
573 + "cpu": [
574 + "arm64"
575 + ],
576 + "dev": true,
577 + "license": "MIT",
578 + "optional": true,
579 + "os": [
580 + "linux"
581 + ],
582 + "engines": {
583 + "node": ">=18"
584 + }
585 + },
586 + "node_modules/@esbuild/linux-ia32": {
587 + "version": "0.27.3",
588 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
589 + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
590 + "cpu": [
591 + "ia32"
592 + ],
593 + "dev": true,
594 + "license": "MIT",
595 + "optional": true,
596 + "os": [
597 + "linux"
598 + ],
599 + "engines": {
600 + "node": ">=18"
601 + }
602 + },
603 + "node_modules/@esbuild/linux-loong64": {
604 + "version": "0.27.3",
605 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
606 + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
607 + "cpu": [
608 + "loong64"
609 + ],
610 + "dev": true,
611 + "license": "MIT",
612 + "optional": true,
613 + "os": [
614 + "linux"
615 + ],
616 + "engines": {
617 + "node": ">=18"
618 + }
619 + },
620 + "node_modules/@esbuild/linux-mips64el": {
621 + "version": "0.27.3",
622 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
623 + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
624 + "cpu": [
625 + "mips64el"
626 + ],
627 + "dev": true,
628 + "license": "MIT",
629 + "optional": true,
630 + "os": [
631 + "linux"
632 + ],
633 + "engines": {
634 + "node": ">=18"
635 + }
636 + },
637 + "node_modules/@esbuild/linux-ppc64": {
638 + "version": "0.27.3",
639 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
640 + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
641 + "cpu": [
642 + "ppc64"
643 + ],
644 + "dev": true,
645 + "license": "MIT",
646 + "optional": true,
647 + "os": [
648 + "linux"
649 + ],
650 + "engines": {
651 + "node": ">=18"
652 + }
653 + },
654 + "node_modules/@esbuild/linux-riscv64": {
655 + "version": "0.27.3",
656 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
657 + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
658 + "cpu": [
659 + "riscv64"
660 + ],
661 + "dev": true,
662 + "license": "MIT",
663 + "optional": true,
664 + "os": [
665 + "linux"
666 + ],
667 + "engines": {
668 + "node": ">=18"
669 + }
670 + },
671 + "node_modules/@esbuild/linux-s390x": {
672 + "version": "0.27.3",
673 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
674 + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
675 + "cpu": [
676 + "s390x"
677 + ],
678 + "dev": true,
679 + "license": "MIT",
680 + "optional": true,
681 + "os": [
682 + "linux"
683 + ],
684 + "engines": {
685 + "node": ">=18"
686 + }
687 + },
688 + "node_modules/@esbuild/linux-x64": {
689 + "version": "0.27.3",
690 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
691 + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
692 + "cpu": [
693 + "x64"
694 + ],
695 + "dev": true,
696 + "license": "MIT",
697 + "optional": true,
698 + "os": [
699 + "linux"
700 + ],
701 + "engines": {
702 + "node": ">=18"
703 + }
704 + },
705 + "node_modules/@esbuild/netbsd-arm64": {
706 + "version": "0.27.3",
707 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
708 + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
709 + "cpu": [
710 + "arm64"
711 + ],
712 + "dev": true,
713 + "license": "MIT",
714 + "optional": true,
715 + "os": [
716 + "netbsd"
717 + ],
718 + "engines": {
719 + "node": ">=18"
720 + }
721 + },
722 + "node_modules/@esbuild/netbsd-x64": {
723 + "version": "0.27.3",
724 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
725 + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
726 + "cpu": [
727 + "x64"
728 + ],
729 + "dev": true,
730 + "license": "MIT",
731 + "optional": true,
732 + "os": [
733 + "netbsd"
734 + ],
735 + "engines": {
736 + "node": ">=18"
737 + }
738 + },
739 + "node_modules/@esbuild/openbsd-arm64": {
740 + "version": "0.27.3",
741 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
742 + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
743 + "cpu": [
744 + "arm64"
745 + ],
746 + "dev": true,
747 + "license": "MIT",
748 + "optional": true,
749 + "os": [
750 + "openbsd"
751 + ],
752 + "engines": {
753 + "node": ">=18"
754 + }
755 + },
756 + "node_modules/@esbuild/openbsd-x64": {
757 + "version": "0.27.3",
758 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
759 + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
760 + "cpu": [
761 + "x64"
762 + ],
763 + "dev": true,
764 + "license": "MIT",
765 + "optional": true,
766 + "os": [
767 + "openbsd"
768 + ],
769 + "engines": {
770 + "node": ">=18"
771 + }
772 + },
773 + "node_modules/@esbuild/openharmony-arm64": {
774 + "version": "0.27.3",
775 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
776 + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
777 + "cpu": [
778 + "arm64"
779 + ],
780 + "dev": true,
781 + "license": "MIT",
782 + "optional": true,
783 + "os": [
784 + "openharmony"
785 + ],
786 + "engines": {
787 + "node": ">=18"
788 + }
789 + },
790 + "node_modules/@esbuild/sunos-x64": {
791 + "version": "0.27.3",
792 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
793 + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
794 + "cpu": [
795 + "x64"
796 + ],
797 + "dev": true,
798 + "license": "MIT",
799 + "optional": true,
800 + "os": [
801 + "sunos"
802 + ],
803 + "engines": {
804 + "node": ">=18"
805 + }
806 + },
807 + "node_modules/@esbuild/win32-arm64": {
808 + "version": "0.27.3",
809 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
810 + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
811 + "cpu": [
812 + "arm64"
813 + ],
814 + "dev": true,
815 + "license": "MIT",
816 + "optional": true,
817 + "os": [
818 + "win32"
819 + ],
820 + "engines": {
821 + "node": ">=18"
822 + }
823 + },
824 + "node_modules/@esbuild/win32-ia32": {
825 + "version": "0.27.3",
826 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
827 + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
828 + "cpu": [
829 + "ia32"
830 + ],
831 + "dev": true,
832 + "license": "MIT",
833 + "optional": true,
834 + "os": [
835 + "win32"
836 + ],
837 + "engines": {
838 + "node": ">=18"
839 + }
840 + },
841 + "node_modules/@esbuild/win32-x64": {
842 + "version": "0.27.3",
843 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
844 + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
845 + "cpu": [
846 + "x64"
847 + ],
848 + "dev": true,
849 + "license": "MIT",
850 + "optional": true,
851 + "os": [
852 + "win32"
853 + ],
854 + "engines": {
855 + "node": ">=18"
856 + }
857 + },
858 + "node_modules/@eslint-community/eslint-utils": {
859 + "version": "4.9.1",
860 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
861 + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
862 + "dev": true,
863 + "license": "MIT",
864 + "dependencies": {
865 + "eslint-visitor-keys": "^3.4.3"
866 + },
867 + "engines": {
868 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
869 + },
870 + "funding": {
871 + "url": "https://opencollective.com/eslint"
872 + },
873 + "peerDependencies": {
874 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
875 + }
876 + },
877 + "node_modules/@eslint-community/regexpp": {
878 + "version": "4.12.2",
879 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
880 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
881 + "dev": true,
882 + "license": "MIT",
883 + "engines": {
884 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
885 + }
886 + },
887 + "node_modules/@eslint/eslintrc": {
888 + "version": "2.1.4",
889 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
890 + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
891 + "dev": true,
892 + "license": "MIT",
893 + "dependencies": {
894 + "ajv": "^6.12.4",
895 + "debug": "^4.3.2",
896 + "espree": "^9.6.0",
897 + "globals": "^13.19.0",
898 + "ignore": "^5.2.0",
899 + "import-fresh": "^3.2.1",
900 + "js-yaml": "^4.1.0",
901 + "minimatch": "^3.1.2",
902 + "strip-json-comments": "^3.1.1"
903 + },
904 + "engines": {
905 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
906 + },
907 + "funding": {
908 + "url": "https://opencollective.com/eslint"
909 + }
910 + },
911 + "node_modules/@eslint/js": {
912 + "version": "8.57.1",
913 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
914 + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
915 + "dev": true,
916 + "license": "MIT",
917 + "engines": {
918 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
919 + }
920 + },
921 + "node_modules/@humanwhocodes/config-array": {
922 + "version": "0.13.0",
923 + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
924 + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
925 + "deprecated": "Use @eslint/config-array instead",
926 + "dev": true,
927 + "license": "Apache-2.0",
928 + "dependencies": {
929 + "@humanwhocodes/object-schema": "^2.0.3",
930 + "debug": "^4.3.1",
931 + "minimatch": "^3.0.5"
932 + },
933 + "engines": {
934 + "node": ">=10.10.0"
935 + }
936 + },
937 + "node_modules/@humanwhocodes/module-importer": {
938 + "version": "1.0.1",
939 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
940 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
941 + "dev": true,
942 + "license": "Apache-2.0",
943 + "engines": {
944 + "node": ">=12.22"
945 + },
946 + "funding": {
947 + "type": "github",
948 + "url": "https://github.com/sponsors/nzakas"
949 + }
950 + },
951 + "node_modules/@humanwhocodes/object-schema": {
952 + "version": "2.0.3",
953 + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
954 + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
955 + "deprecated": "Use @eslint/object-schema instead",
956 + "dev": true,
957 + "license": "BSD-3-Clause"
958 + },
959 + "node_modules/@img/colour": {
960 + "version": "1.0.0",
961 + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
962 + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
963 + "license": "MIT",
964 + "engines": {
965 + "node": ">=18"
966 + }
967 + },
968 + "node_modules/@img/sharp-darwin-arm64": {
969 + "version": "0.34.5",
970 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
971 + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
972 + "cpu": [
973 + "arm64"
974 + ],
975 + "license": "Apache-2.0",
976 + "optional": true,
977 + "os": [
978 + "darwin"
979 + ],
980 + "engines": {
981 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
982 + },
983 + "funding": {
984 + "url": "https://opencollective.com/libvips"
985 + },
986 + "optionalDependencies": {
987 + "@img/sharp-libvips-darwin-arm64": "1.2.4"
988 + }
989 + },
990 + "node_modules/@img/sharp-darwin-x64": {
991 + "version": "0.34.5",
992 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
993 + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
994 + "cpu": [
995 + "x64"
996 + ],
997 + "license": "Apache-2.0",
998 + "optional": true,
999 + "os": [
1000 + "darwin"
1001 + ],
1002 + "engines": {
1003 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1004 + },
1005 + "funding": {
1006 + "url": "https://opencollective.com/libvips"
1007 + },
1008 + "optionalDependencies": {
1009 + "@img/sharp-libvips-darwin-x64": "1.2.4"
1010 + }
1011 + },
1012 + "node_modules/@img/sharp-libvips-darwin-arm64": {
1013 + "version": "1.2.4",
1014 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
1015 + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
1016 + "cpu": [
1017 + "arm64"
1018 + ],
1019 + "license": "LGPL-3.0-or-later",
1020 + "optional": true,
1021 + "os": [
1022 + "darwin"
1023 + ],
1024 + "funding": {
1025 + "url": "https://opencollective.com/libvips"
1026 + }
1027 + },
1028 + "node_modules/@img/sharp-libvips-darwin-x64": {
1029 + "version": "1.2.4",
1030 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
1031 + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
1032 + "cpu": [
1033 + "x64"
1034 + ],
1035 + "license": "LGPL-3.0-or-later",
1036 + "optional": true,
1037 + "os": [
1038 + "darwin"
1039 + ],
1040 + "funding": {
1041 + "url": "https://opencollective.com/libvips"
1042 + }
1043 + },
1044 + "node_modules/@img/sharp-libvips-linux-arm": {
1045 + "version": "1.2.4",
1046 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
1047 + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
1048 + "cpu": [
1049 + "arm"
1050 + ],
1051 + "license": "LGPL-3.0-or-later",
1052 + "optional": true,
1053 + "os": [
1054 + "linux"
1055 + ],
1056 + "funding": {
1057 + "url": "https://opencollective.com/libvips"
1058 + }
1059 + },
1060 + "node_modules/@img/sharp-libvips-linux-arm64": {
1061 + "version": "1.2.4",
1062 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
1063 + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
1064 + "cpu": [
1065 + "arm64"
1066 + ],
1067 + "license": "LGPL-3.0-or-later",
1068 + "optional": true,
1069 + "os": [
1070 + "linux"
1071 + ],
1072 + "funding": {
1073 + "url": "https://opencollective.com/libvips"
1074 + }
1075 + },
1076 + "node_modules/@img/sharp-libvips-linux-ppc64": {
1077 + "version": "1.2.4",
1078 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
1079 + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
1080 + "cpu": [
1081 + "ppc64"
1082 + ],
1083 + "license": "LGPL-3.0-or-later",
1084 + "optional": true,
1085 + "os": [
1086 + "linux"
1087 + ],
1088 + "funding": {
1089 + "url": "https://opencollective.com/libvips"
1090 + }
1091 + },
1092 + "node_modules/@img/sharp-libvips-linux-riscv64": {
1093 + "version": "1.2.4",
1094 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
1095 + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
1096 + "cpu": [
1097 + "riscv64"
1098 + ],
1099 + "license": "LGPL-3.0-or-later",
1100 + "optional": true,
1101 + "os": [
1102 + "linux"
1103 + ],
1104 + "funding": {
1105 + "url": "https://opencollective.com/libvips"
1106 + }
1107 + },
1108 + "node_modules/@img/sharp-libvips-linux-s390x": {
1109 + "version": "1.2.4",
1110 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
1111 + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
1112 + "cpu": [
1113 + "s390x"
1114 + ],
1115 + "license": "LGPL-3.0-or-later",
1116 + "optional": true,
1117 + "os": [
1118 + "linux"
1119 + ],
1120 + "funding": {
1121 + "url": "https://opencollective.com/libvips"
1122 + }
1123 + },
1124 + "node_modules/@img/sharp-libvips-linux-x64": {
1125 + "version": "1.2.4",
1126 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
1127 + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
1128 + "cpu": [
1129 + "x64"
1130 + ],
1131 + "license": "LGPL-3.0-or-later",
1132 + "optional": true,
1133 + "os": [
1134 + "linux"
1135 + ],
1136 + "funding": {
1137 + "url": "https://opencollective.com/libvips"
1138 + }
1139 + },
1140 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
1141 + "version": "1.2.4",
1142 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
1143 + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
1144 + "cpu": [
1145 + "arm64"
1146 + ],
1147 + "license": "LGPL-3.0-or-later",
1148 + "optional": true,
1149 + "os": [
1150 + "linux"
1151 + ],
1152 + "funding": {
1153 + "url": "https://opencollective.com/libvips"
1154 + }
1155 + },
1156 + "node_modules/@img/sharp-libvips-linuxmusl-x64": {
1157 + "version": "1.2.4",
1158 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
1159 + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
1160 + "cpu": [
1161 + "x64"
1162 + ],
1163 + "license": "LGPL-3.0-or-later",
1164 + "optional": true,
1165 + "os": [
1166 + "linux"
1167 + ],
1168 + "funding": {
1169 + "url": "https://opencollective.com/libvips"
1170 + }
1171 + },
1172 + "node_modules/@img/sharp-linux-arm": {
1173 + "version": "0.34.5",
1174 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
1175 + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
1176 + "cpu": [
1177 + "arm"
1178 + ],
1179 + "license": "Apache-2.0",
1180 + "optional": true,
1181 + "os": [
1182 + "linux"
1183 + ],
1184 + "engines": {
1185 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1186 + },
1187 + "funding": {
1188 + "url": "https://opencollective.com/libvips"
1189 + },
1190 + "optionalDependencies": {
1191 + "@img/sharp-libvips-linux-arm": "1.2.4"
1192 + }
1193 + },
1194 + "node_modules/@img/sharp-linux-arm64": {
1195 + "version": "0.34.5",
1196 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
1197 + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
1198 + "cpu": [
1199 + "arm64"
1200 + ],
1201 + "license": "Apache-2.0",
1202 + "optional": true,
1203 + "os": [
1204 + "linux"
1205 + ],
1206 + "engines": {
1207 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1208 + },
1209 + "funding": {
1210 + "url": "https://opencollective.com/libvips"
1211 + },
1212 + "optionalDependencies": {
1213 + "@img/sharp-libvips-linux-arm64": "1.2.4"
1214 + }
1215 + },
1216 + "node_modules/@img/sharp-linux-ppc64": {
1217 + "version": "0.34.5",
1218 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
1219 + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
1220 + "cpu": [
1221 + "ppc64"
1222 + ],
1223 + "license": "Apache-2.0",
1224 + "optional": true,
1225 + "os": [
1226 + "linux"
1227 + ],
1228 + "engines": {
1229 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1230 + },
1231 + "funding": {
1232 + "url": "https://opencollective.com/libvips"
1233 + },
1234 + "optionalDependencies": {
1235 + "@img/sharp-libvips-linux-ppc64": "1.2.4"
1236 + }
1237 + },
1238 + "node_modules/@img/sharp-linux-riscv64": {
1239 + "version": "0.34.5",
1240 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
1241 + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
1242 + "cpu": [
1243 + "riscv64"
1244 + ],
1245 + "license": "Apache-2.0",
1246 + "optional": true,
1247 + "os": [
1248 + "linux"
1249 + ],
1250 + "engines": {
1251 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1252 + },
1253 + "funding": {
1254 + "url": "https://opencollective.com/libvips"
1255 + },
1256 + "optionalDependencies": {
1257 + "@img/sharp-libvips-linux-riscv64": "1.2.4"
1258 + }
1259 + },
1260 + "node_modules/@img/sharp-linux-s390x": {
1261 + "version": "0.34.5",
1262 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
1263 + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
1264 + "cpu": [
1265 + "s390x"
1266 + ],
1267 + "license": "Apache-2.0",
1268 + "optional": true,
1269 + "os": [
1270 + "linux"
1271 + ],
1272 + "engines": {
1273 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1274 + },
1275 + "funding": {
1276 + "url": "https://opencollective.com/libvips"
1277 + },
1278 + "optionalDependencies": {
1279 + "@img/sharp-libvips-linux-s390x": "1.2.4"
1280 + }
1281 + },
1282 + "node_modules/@img/sharp-linux-x64": {
1283 + "version": "0.34.5",
1284 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
1285 + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
1286 + "cpu": [
1287 + "x64"
1288 + ],
1289 + "license": "Apache-2.0",
1290 + "optional": true,
1291 + "os": [
1292 + "linux"
1293 + ],
1294 + "engines": {
1295 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1296 + },
1297 + "funding": {
1298 + "url": "https://opencollective.com/libvips"
1299 + },
1300 + "optionalDependencies": {
1301 + "@img/sharp-libvips-linux-x64": "1.2.4"
1302 + }
1303 + },
1304 + "node_modules/@img/sharp-linuxmusl-arm64": {
1305 + "version": "0.34.5",
1306 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
1307 + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
1308 + "cpu": [
1309 + "arm64"
1310 + ],
1311 + "license": "Apache-2.0",
1312 + "optional": true,
1313 + "os": [
1314 + "linux"
1315 + ],
1316 + "engines": {
1317 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1318 + },
1319 + "funding": {
1320 + "url": "https://opencollective.com/libvips"
1321 + },
1322 + "optionalDependencies": {
1323 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
1324 + }
1325 + },
1326 + "node_modules/@img/sharp-linuxmusl-x64": {
1327 + "version": "0.34.5",
1328 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
1329 + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
1330 + "cpu": [
1331 + "x64"
1332 + ],
1333 + "license": "Apache-2.0",
1334 + "optional": true,
1335 + "os": [
1336 + "linux"
1337 + ],
1338 + "engines": {
1339 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1340 + },
1341 + "funding": {
1342 + "url": "https://opencollective.com/libvips"
1343 + },
1344 + "optionalDependencies": {
1345 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
1346 + }
1347 + },
1348 + "node_modules/@img/sharp-wasm32": {
1349 + "version": "0.34.5",
1350 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
1351 + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
1352 + "cpu": [
1353 + "wasm32"
1354 + ],
1355 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
1356 + "optional": true,
1357 + "dependencies": {
1358 + "@emnapi/runtime": "^1.7.0"
1359 + },
1360 + "engines": {
1361 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1362 + },
1363 + "funding": {
1364 + "url": "https://opencollective.com/libvips"
1365 + }
1366 + },
1367 + "node_modules/@img/sharp-win32-arm64": {
1368 + "version": "0.34.5",
1369 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
1370 + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
1371 + "cpu": [
1372 + "arm64"
1373 + ],
1374 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1375 + "optional": true,
1376 + "os": [
1377 + "win32"
1378 + ],
1379 + "engines": {
1380 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1381 + },
1382 + "funding": {
1383 + "url": "https://opencollective.com/libvips"
1384 + }
1385 + },
1386 + "node_modules/@img/sharp-win32-ia32": {
1387 + "version": "0.34.5",
1388 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
1389 + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
1390 + "cpu": [
1391 + "ia32"
1392 + ],
1393 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1394 + "optional": true,
1395 + "os": [
1396 + "win32"
1397 + ],
1398 + "engines": {
1399 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1400 + },
1401 + "funding": {
1402 + "url": "https://opencollective.com/libvips"
1403 + }
1404 + },
1405 + "node_modules/@img/sharp-win32-x64": {
1406 + "version": "0.34.5",
1407 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
1408 + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
1409 + "cpu": [
1410 + "x64"
1411 + ],
1412 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1413 + "optional": true,
1414 + "os": [
1415 + "win32"
1416 + ],
1417 + "engines": {
1418 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1419 + },
1420 + "funding": {
1421 + "url": "https://opencollective.com/libvips"
1422 + }
1423 + },
1424 + "node_modules/@isaacs/cliui": {
1425 + "version": "8.0.2",
1426 + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
1427 + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
1428 + "license": "ISC",
1429 + "dependencies": {
1430 + "string-width": "^5.1.2",
1431 + "string-width-cjs": "npm:string-width@^4.2.0",
1432 + "strip-ansi": "^7.0.1",
1433 + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
1434 + "wrap-ansi": "^8.1.0",
1435 + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
1436 + },
1437 + "engines": {
1438 + "node": ">=12"
1439 + }
1440 + },
1441 + "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
1442 + "version": "6.2.2",
1443 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
1444 + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
1445 + "license": "MIT",
1446 + "engines": {
1447 + "node": ">=12"
1448 + },
1449 + "funding": {
1450 + "url": "https://github.com/chalk/ansi-regex?sponsor=1"
1451 + }
1452 + },
1453 + "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
1454 + "version": "7.1.2",
1455 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
1456 + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
1457 + "license": "MIT",
1458 + "dependencies": {
1459 + "ansi-regex": "^6.0.1"
1460 + },
1461 + "engines": {
1462 + "node": ">=12"
1463 + },
1464 + "funding": {
1465 + "url": "https://github.com/chalk/strip-ansi?sponsor=1"
1466 + }
1467 + },
1468 + "node_modules/@jridgewell/gen-mapping": {
1469 + "version": "0.3.13",
1470 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
1471 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
1472 + "dev": true,
1473 + "license": "MIT",
1474 + "dependencies": {
1475 + "@jridgewell/sourcemap-codec": "^1.5.0",
1476 + "@jridgewell/trace-mapping": "^0.3.24"
1477 + }
1478 + },
1479 + "node_modules/@jridgewell/resolve-uri": {
1480 + "version": "3.1.2",
1481 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
1482 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
1483 + "dev": true,
1484 + "license": "MIT",
1485 + "engines": {
1486 + "node": ">=6.0.0"
1487 + }
1488 + },
1489 + "node_modules/@jridgewell/sourcemap-codec": {
1490 + "version": "1.5.5",
1491 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
1492 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
1493 + "dev": true,
1494 + "license": "MIT"
1495 + },
1496 + "node_modules/@jridgewell/trace-mapping": {
1497 + "version": "0.3.31",
1498 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
1499 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
1500 + "dev": true,
1501 + "license": "MIT",
1502 + "dependencies": {
1503 + "@jridgewell/resolve-uri": "^3.1.0",
1504 + "@jridgewell/sourcemap-codec": "^1.4.14"
1505 + }
1506 + },
1507 + "node_modules/@lezer/common": {
1508 + "version": "1.5.1",
1509 + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
1510 + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
1511 + "license": "MIT"
1512 + },
1513 + "node_modules/@lezer/cpp": {
1514 + "version": "1.1.5",
1515 + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.5.tgz",
1516 + "integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==",
1517 + "license": "MIT",
1518 + "dependencies": {
1519 + "@lezer/common": "^1.2.0",
1520 + "@lezer/highlight": "^1.0.0",
1521 + "@lezer/lr": "^1.0.0"
1522 + }
1523 + },
1524 + "node_modules/@lezer/css": {
1525 + "version": "1.3.1",
1526 + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz",
1527 + "integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==",
1528 + "license": "MIT",
1529 + "dependencies": {
1530 + "@lezer/common": "^1.2.0",
1531 + "@lezer/highlight": "^1.0.0",
1532 + "@lezer/lr": "^1.3.0"
1533 + }
1534 + },
1535 + "node_modules/@lezer/go": {
1536 + "version": "1.0.1",
1537 + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
1538 + "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
1539 + "license": "MIT",
1540 + "dependencies": {
1541 + "@lezer/common": "^1.2.0",
1542 + "@lezer/highlight": "^1.0.0",
1543 + "@lezer/lr": "^1.3.0"
1544 + }
1545 + },
1546 + "node_modules/@lezer/highlight": {
1547 + "version": "1.2.3",
1548 + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
1549 + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
1550 + "license": "MIT",
1551 + "dependencies": {
1552 + "@lezer/common": "^1.3.0"
1553 + }
1554 + },
1555 + "node_modules/@lezer/html": {
1556 + "version": "1.3.13",
1557 + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
1558 + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
1559 + "license": "MIT",
1560 + "dependencies": {
1561 + "@lezer/common": "^1.2.0",
1562 + "@lezer/highlight": "^1.0.0",
1563 + "@lezer/lr": "^1.0.0"
1564 + }
1565 + },
1566 + "node_modules/@lezer/java": {
1567 + "version": "1.1.3",
1568 + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz",
1569 + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
1570 + "license": "MIT",
1571 + "dependencies": {
1572 + "@lezer/common": "^1.2.0",
1573 + "@lezer/highlight": "^1.0.0",
1574 + "@lezer/lr": "^1.0.0"
1575 + }
1576 + },
1577 + "node_modules/@lezer/javascript": {
1578 + "version": "1.5.4",
1579 + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
1580 + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
1581 + "license": "MIT",
1582 + "dependencies": {
1583 + "@lezer/common": "^1.2.0",
1584 + "@lezer/highlight": "^1.1.3",
1585 + "@lezer/lr": "^1.3.0"
1586 + }
1587 + },
1588 + "node_modules/@lezer/json": {
1589 + "version": "1.0.3",
1590 + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
1591 + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
1592 + "license": "MIT",
1593 + "dependencies": {
1594 + "@lezer/common": "^1.2.0",
1595 + "@lezer/highlight": "^1.0.0",
1596 + "@lezer/lr": "^1.0.0"
1597 + }
1598 + },
1599 + "node_modules/@lezer/lr": {
1600 + "version": "1.4.8",
1601 + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
1602 + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
1603 + "license": "MIT",
1604 + "dependencies": {
1605 + "@lezer/common": "^1.0.0"
1606 + }
1607 + },
1608 + "node_modules/@lezer/markdown": {
1609 + "version": "1.6.3",
1610 + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz",
1611 + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==",
1612 + "license": "MIT",
1613 + "dependencies": {
1614 + "@lezer/common": "^1.5.0",
1615 + "@lezer/highlight": "^1.0.0"
1616 + }
1617 + },
1618 + "node_modules/@lezer/php": {
1619 + "version": "1.0.5",
1620 + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz",
1621 + "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==",
1622 + "license": "MIT",
1623 + "dependencies": {
1624 + "@lezer/common": "^1.2.0",
1625 + "@lezer/highlight": "^1.0.0",
1626 + "@lezer/lr": "^1.1.0"
1627 + }
1628 + },
1629 + "node_modules/@lezer/python": {
1630 + "version": "1.1.18",
1631 + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
1632 + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
1633 + "license": "MIT",
1634 + "dependencies": {
1635 + "@lezer/common": "^1.2.0",
1636 + "@lezer/highlight": "^1.0.0",
1637 + "@lezer/lr": "^1.0.0"
1638 + }
1639 + },
1640 + "node_modules/@lezer/rust": {
1641 + "version": "1.0.2",
1642 + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz",
1643 + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
1644 + "license": "MIT",
1645 + "dependencies": {
1646 + "@lezer/common": "^1.2.0",
1647 + "@lezer/highlight": "^1.0.0",
1648 + "@lezer/lr": "^1.0.0"
1649 + }
1650 + },
1651 + "node_modules/@lezer/xml": {
1652 + "version": "1.0.6",
1653 + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
1654 + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
1655 + "license": "MIT",
1656 + "dependencies": {
1657 + "@lezer/common": "^1.2.0",
1658 + "@lezer/highlight": "^1.0.0",
1659 + "@lezer/lr": "^1.0.0"
1660 + }
1661 + },
1662 + "node_modules/@marijn/find-cluster-break": {
1663 + "version": "1.0.2",
1664 + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
1665 + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
1666 + "license": "MIT"
1667 + },
1668 + "node_modules/@napi-rs/canvas": {
1669 + "version": "0.1.93",
1670 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.93.tgz",
1671 + "integrity": "sha512-unVFo8CUlUeJCCxt50+j4yy91NF4x6n9zdGcvEsOFAWzowtZm3mgx8X2D7xjwV0cFSfxmpGPoe+JS77uzeFsxg==",
1672 + "license": "MIT",
1673 + "optional": true,
1674 + "workspaces": [
1675 + "e2e/*"
1676 + ],
1677 + "engines": {
1678 + "node": ">= 10"
1679 + },
1680 + "funding": {
1681 + "type": "github",
1682 + "url": "https://github.com/sponsors/Brooooooklyn"
1683 + },
1684 + "optionalDependencies": {
1685 + "@napi-rs/canvas-android-arm64": "0.1.93",
1686 + "@napi-rs/canvas-darwin-arm64": "0.1.93",
1687 + "@napi-rs/canvas-darwin-x64": "0.1.93",
1688 + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.93",
1689 + "@napi-rs/canvas-linux-arm64-gnu": "0.1.93",
1690 + "@napi-rs/canvas-linux-arm64-musl": "0.1.93",
1691 + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.93",
1692 + "@napi-rs/canvas-linux-x64-gnu": "0.1.93",
1693 + "@napi-rs/canvas-linux-x64-musl": "0.1.93",
1694 + "@napi-rs/canvas-win32-arm64-msvc": "0.1.93",
1695 + "@napi-rs/canvas-win32-x64-msvc": "0.1.93"
1696 + }
1697 + },
1698 + "node_modules/@napi-rs/canvas-android-arm64": {
1699 + "version": "0.1.93",
1700 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.93.tgz",
1701 + "integrity": "sha512-xRIoOPFvneR29Dtq5d9p2AJbijDCFeV4jQ+5Ms/xVAXJVb8R0Jlu+pPr/SkhrG+Mouaml4roPSXugTIeRl6CMA==",
1702 + "cpu": [
1703 + "arm64"
1704 + ],
1705 + "license": "MIT",
1706 + "optional": true,
1707 + "os": [
1708 + "android"
1709 + ],
1710 + "engines": {
1711 + "node": ">= 10"
1712 + },
1713 + "funding": {
1714 + "type": "github",
1715 + "url": "https://github.com/sponsors/Brooooooklyn"
1716 + }
1717 + },
1718 + "node_modules/@napi-rs/canvas-darwin-arm64": {
1719 + "version": "0.1.93",
1720 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.93.tgz",
1721 + "integrity": "sha512-daNDi76HN5grC6GXDmpxdfP+N2mQPd3sCfg62VyHwUuvbZh32P7R/IUjkzAxtYMtTza+Zvx9hfLJ3J7ENL6WMA==",
1722 + "cpu": [
1723 + "arm64"
1724 + ],
1725 + "license": "MIT",
1726 + "optional": true,
1727 + "os": [
1728 + "darwin"
1729 + ],
1730 + "engines": {
1731 + "node": ">= 10"
1732 + },
1733 + "funding": {
1734 + "type": "github",
1735 + "url": "https://github.com/sponsors/Brooooooklyn"
1736 + }
1737 + },
1738 + "node_modules/@napi-rs/canvas-darwin-x64": {
1739 + "version": "0.1.93",
1740 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.93.tgz",
1741 + "integrity": "sha512-1YfuNPIQLawsg/gSNdJRk4kQWUy9M/Gy8FGsOI79nhQEJ2PZdqpSPl5UNzf4elfuNXuVbEbmmjP68EQdUunDuQ==",
1742 + "cpu": [
1743 + "x64"
1744 + ],
1745 + "license": "MIT",
1746 + "optional": true,
1747 + "os": [
1748 + "darwin"
1749 + ],
1750 + "engines": {
1751 + "node": ">= 10"
1752 + },
1753 + "funding": {
1754 + "type": "github",
1755 + "url": "https://github.com/sponsors/Brooooooklyn"
1756 + }
1757 + },
1758 + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
1759 + "version": "0.1.93",
1760 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.93.tgz",
1761 + "integrity": "sha512-8kEkOQPZjuyHjupvXExuJZiuiVNecdABGq3DLI7aO1EvQFOOlWMm2d/8Q5qXdV73Tn+nu3m16+kPajsN1oJefQ==",
1762 + "cpu": [
1763 + "arm"
1764 + ],
1765 + "license": "MIT",
1766 + "optional": true,
1767 + "os": [
1768 + "linux"
1769 + ],
1770 + "engines": {
1771 + "node": ">= 10"
1772 + },
1773 + "funding": {
1774 + "type": "github",
1775 + "url": "https://github.com/sponsors/Brooooooklyn"
1776 + }
1777 + },
1778 + "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
1779 + "version": "0.1.93",
1780 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.93.tgz",
1781 + "integrity": "sha512-qIKLKkBkYSyWSYAoDThoxf5y1gr4X0g7W8rDU7d2HDeAAcotdVHUwuKkMeNe6+5VNk7/95EIhbslQjSxiCu32g==",
1782 + "cpu": [
1783 + "arm64"
1784 + ],
1785 + "license": "MIT",
1786 + "optional": true,
1787 + "os": [
1788 + "linux"
1789 + ],
1790 + "engines": {
1791 + "node": ">= 10"
1792 + },
1793 + "funding": {
1794 + "type": "github",
1795 + "url": "https://github.com/sponsors/Brooooooklyn"
1796 + }
1797 + },
1798 + "node_modules/@napi-rs/canvas-linux-arm64-musl": {
1799 + "version": "0.1.93",
1800 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.93.tgz",
1801 + "integrity": "sha512-mAwQBGM3qArS9XEO21AK4E1uGvCuUCXjhIZk0dlVvs49MQ6wAAuCkYKNFpSKeSicKrLWwBMfgWX4qZoPh+M00A==",
1802 + "cpu": [
1803 + "arm64"
1804 + ],
1805 + "license": "MIT",
1806 + "optional": true,
1807 + "os": [
1808 + "linux"
1809 + ],
1810 + "engines": {
1811 + "node": ">= 10"
1812 + },
1813 + "funding": {
1814 + "type": "github",
1815 + "url": "https://github.com/sponsors/Brooooooklyn"
1816 + }
1817 + },
1818 + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
1819 + "version": "0.1.93",
1820 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.93.tgz",
1821 + "integrity": "sha512-kaIH5MpPzOZfkM+QMsBxGdM9jlJT+N+fwz2IEaju/S+DL65E5TgPOx4QcD5dQ8vsMxlak6uDrudBc4ns5xzZCw==",
1822 + "cpu": [
1823 + "riscv64"
1824 + ],
1825 + "license": "MIT",
1826 + "optional": true,
1827 + "os": [
1828 + "linux"
1829 + ],
1830 + "engines": {
1831 + "node": ">= 10"
1832 + },
1833 + "funding": {
1834 + "type": "github",
1835 + "url": "https://github.com/sponsors/Brooooooklyn"
1836 + }
1837 + },
1838 + "node_modules/@napi-rs/canvas-linux-x64-gnu": {
1839 + "version": "0.1.93",
1840 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.93.tgz",
1841 + "integrity": "sha512-KtMZJqYWvOSeW5w3VSV2f5iGnwNdKJm4gwgVid4xNy1NFi+NJSyuglA1lX1u4wIPxizyxh8OW5c5Usf6oSOMNQ==",
1842 + "cpu": [
1843 + "x64"
1844 + ],
1845 + "license": "MIT",
1846 + "optional": true,
1847 + "os": [
1848 + "linux"
1849 + ],
1850 + "engines": {
1851 + "node": ">= 10"
1852 + },
1853 + "funding": {
1854 + "type": "github",
1855 + "url": "https://github.com/sponsors/Brooooooklyn"
1856 + }
1857 + },
1858 + "node_modules/@napi-rs/canvas-linux-x64-musl": {
1859 + "version": "0.1.93",
1860 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.93.tgz",
1861 + "integrity": "sha512-qRZhOvlDBooRLX6V3/t9X9B+plZK+OrPLgfFixu0A1RO/3VHbubOknfnMnocSDAqk/L6cRyKI83VP2ciR9UO7w==",
1862 + "cpu": [
1863 + "x64"
1864 + ],
1865 + "license": "MIT",
1866 + "optional": true,
1867 + "os": [
1868 + "linux"
1869 + ],
1870 + "engines": {
1871 + "node": ">= 10"
1872 + },
1873 + "funding": {
1874 + "type": "github",
1875 + "url": "https://github.com/sponsors/Brooooooklyn"
1876 + }
1877 + },
1878 + "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
1879 + "version": "0.1.93",
1880 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.93.tgz",
1881 + "integrity": "sha512-um5XE44vF8bjkQEsH2iRSUP9fDeQGYbn/qjM/v4whXG83qsqapAXlOPOQqSARZB1SiNvPUAuXoRsJLlKFmAEFw==",
1882 + "cpu": [
1883 + "arm64"
1884 + ],
1885 + "license": "MIT",
1886 + "optional": true,
1887 + "os": [
1888 + "win32"
1889 + ],
1890 + "engines": {
1891 + "node": ">= 10"
1892 + },
1893 + "funding": {
1894 + "type": "github",
1895 + "url": "https://github.com/sponsors/Brooooooklyn"
1896 + }
1897 + },
1898 + "node_modules/@napi-rs/canvas-win32-x64-msvc": {
1899 + "version": "0.1.93",
1900 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.93.tgz",
1901 + "integrity": "sha512-maHlizZgmKsAPJwjwBZMnsWfq3Ca9QutoteQwKe7YqsmbECoylrLCCOGCDOredstW4BRWqRTfCl6NJaVVeAQvQ==",
1902 + "cpu": [
1903 + "x64"
1904 + ],
1905 + "license": "MIT",
1906 + "optional": true,
1907 + "os": [
1908 + "win32"
1909 + ],
1910 + "engines": {
1911 + "node": ">= 10"
1912 + },
1913 + "funding": {
1914 + "type": "github",
1915 + "url": "https://github.com/sponsors/Brooooooklyn"
1916 + }
1917 + },
1918 + "node_modules/@napi-rs/wasm-runtime": {
1919 + "version": "0.2.12",
1920 + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
1921 + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
1922 + "dev": true,
1923 + "license": "MIT",
1924 + "optional": true,
1925 + "dependencies": {
1926 + "@emnapi/core": "^1.4.3",
1927 + "@emnapi/runtime": "^1.4.3",
1928 + "@tybys/wasm-util": "^0.10.0"
1929 + }
1930 + },
1931 + "node_modules/@next/env": {
1932 + "version": "14.2.35",
1933 + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
1934 + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
1935 + "license": "MIT"
1936 + },
1937 + "node_modules/@next/eslint-plugin-next": {
1938 + "version": "14.2.35",
1939 + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.35.tgz",
1940 + "integrity": "sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==",
1941 + "dev": true,
1942 + "license": "MIT",
1943 + "dependencies": {
1944 + "glob": "10.3.10"
1945 + }
1946 + },
1947 + "node_modules/@next/swc-darwin-arm64": {
1948 + "version": "14.2.33",
1949 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
1950 + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
1951 + "cpu": [
1952 + "arm64"
1953 + ],
1954 + "license": "MIT",
1955 + "optional": true,
1956 + "os": [
1957 + "darwin"
1958 + ],
1959 + "engines": {
1960 + "node": ">= 10"
1961 + }
1962 + },
1963 + "node_modules/@next/swc-darwin-x64": {
1964 + "version": "14.2.33",
1965 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
1966 + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
1967 + "cpu": [
1968 + "x64"
1969 + ],
1970 + "license": "MIT",
1971 + "optional": true,
1972 + "os": [
1973 + "darwin"
1974 + ],
1975 + "engines": {
1976 + "node": ">= 10"
1977 + }
1978 + },
1979 + "node_modules/@next/swc-linux-arm64-gnu": {
1980 + "version": "14.2.33",
1981 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
1982 + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
1983 + "cpu": [
1984 + "arm64"
1985 + ],
1986 + "license": "MIT",
1987 + "optional": true,
1988 + "os": [
1989 + "linux"
1990 + ],
1991 + "engines": {
1992 + "node": ">= 10"
1993 + }
1994 + },
1995 + "node_modules/@next/swc-linux-arm64-musl": {
1996 + "version": "14.2.33",
1997 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
1998 + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
1999 + "cpu": [
2000 + "arm64"
2001 + ],
2002 + "license": "MIT",
2003 + "optional": true,
2004 + "os": [
2005 + "linux"
2006 + ],
2007 + "engines": {
2008 + "node": ">= 10"
2009 + }
2010 + },
2011 + "node_modules/@next/swc-linux-x64-gnu": {
2012 + "version": "14.2.33",
2013 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
2014 + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
2015 + "cpu": [
2016 + "x64"
2017 + ],
2018 + "license": "MIT",
2019 + "optional": true,
2020 + "os": [
2021 + "linux"
2022 + ],
2023 + "engines": {
2024 + "node": ">= 10"
2025 + }
2026 + },
2027 + "node_modules/@next/swc-linux-x64-musl": {
2028 + "version": "14.2.33",
2029 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
2030 + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
2031 + "cpu": [
2032 + "x64"
2033 + ],
2034 + "license": "MIT",
2035 + "optional": true,
2036 + "os": [
2037 + "linux"
2038 + ],
2039 + "engines": {
2040 + "node": ">= 10"
2041 + }
2042 + },
2043 + "node_modules/@next/swc-win32-arm64-msvc": {
2044 + "version": "14.2.33",
2045 + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
2046 + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
2047 + "cpu": [
2048 + "arm64"
2049 + ],
2050 + "license": "MIT",
2051 + "optional": true,
2052 + "os": [
2053 + "win32"
2054 + ],
2055 + "engines": {
2056 + "node": ">= 10"
2057 + }
2058 + },
2059 + "node_modules/@next/swc-win32-ia32-msvc": {
2060 + "version": "14.2.33",
2061 + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
2062 + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
2063 + "cpu": [
2064 + "ia32"
2065 + ],
2066 + "license": "MIT",
2067 + "optional": true,
2068 + "os": [
2069 + "win32"
2070 + ],
2071 + "engines": {
2072 + "node": ">= 10"
2073 + }
2074 + },
2075 + "node_modules/@next/swc-win32-x64-msvc": {
2076 + "version": "14.2.33",
2077 + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
2078 + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
2079 + "cpu": [
2080 + "x64"
2081 + ],
2082 + "license": "MIT",
2083 + "optional": true,
2084 + "os": [
2085 + "win32"
2086 + ],
2087 + "engines": {
2088 + "node": ">= 10"
2089 + }
2090 + },
2091 + "node_modules/@noble/hashes": {
2092 + "version": "2.0.1",
2093 + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
2094 + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
2095 + "license": "MIT",
2096 + "engines": {
2097 + "node": ">= 20.19.0"
2098 + },
2099 + "funding": {
2100 + "url": "https://paulmillr.com/funding/"
2101 + }
2102 + },
2103 + "node_modules/@nodelib/fs.scandir": {
2104 + "version": "2.1.5",
2105 + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
2106 + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
2107 + "dev": true,
2108 + "license": "MIT",
2109 + "dependencies": {
2110 + "@nodelib/fs.stat": "2.0.5",
2111 + "run-parallel": "^1.1.9"
2112 + },
2113 + "engines": {
2114 + "node": ">= 8"
2115 + }
2116 + },
2117 + "node_modules/@nodelib/fs.stat": {
2118 + "version": "2.0.5",
2119 + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
2120 + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
2121 + "dev": true,
2122 + "license": "MIT",
2123 + "engines": {
2124 + "node": ">= 8"
2125 + }
2126 + },
2127 + "node_modules/@nodelib/fs.walk": {
2128 + "version": "1.2.8",
2129 + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
2130 + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
2131 + "dev": true,
2132 + "license": "MIT",
2133 + "dependencies": {
2134 + "@nodelib/fs.scandir": "2.1.5",
2135 + "fastq": "^1.6.0"
2136 + },
2137 + "engines": {
2138 + "node": ">= 8"
2139 + }
2140 + },
2141 + "node_modules/@nolyfill/is-core-module": {
2142 + "version": "1.0.39",
2143 + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
2144 + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
2145 + "dev": true,
2146 + "license": "MIT",
2147 + "engines": {
2148 + "node": ">=12.4.0"
2149 + }
2150 + },
2151 + "node_modules/@otplib/core": {
2152 + "version": "13.3.0",
2153 + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-13.3.0.tgz",
2154 + "integrity": "sha512-pnQDOuCmFVeF/XnboJq9TOJgLoo2idNPJKMymOF8vGqJJ+ReKRYM9bUGjNPRWC0tHjMwu1TXbnzyBp494JgRag==",
2155 + "license": "MIT"
2156 + },
2157 + "node_modules/@otplib/hotp": {
2158 + "version": "13.3.0",
2159 + "resolved": "https://registry.npmjs.org/@otplib/hotp/-/hotp-13.3.0.tgz",
2160 + "integrity": "sha512-XJMZGz2bg4QJwK7ulvl1GUI2VMn/flaIk/E/BTKAejHsX2kUtPF1bRhlZ2+elq8uU5Fs9Z9FHcQK2CPZNQbbUQ==",
2161 + "license": "MIT",
2162 + "dependencies": {
2163 + "@otplib/core": "13.3.0",
2164 + "@otplib/uri": "13.3.0"
2165 + }
2166 + },
2167 + "node_modules/@otplib/plugin-base32-scure": {
2168 + "version": "13.3.0",
2169 + "resolved": "https://registry.npmjs.org/@otplib/plugin-base32-scure/-/plugin-base32-scure-13.3.0.tgz",
2170 + "integrity": "sha512-/jYbL5S6GB0Ie3XGEWtLIr9s5ZICl/BfmNL7+8/W7usZaUU4GiyLd2S+JGsNCslPyqNekSudD864nDAvRI0s8w==",
2171 + "license": "MIT",
2172 + "dependencies": {
2173 + "@otplib/core": "13.3.0",
2174 + "@scure/base": "^2.0.0"
2175 + }
2176 + },
2177 + "node_modules/@otplib/plugin-crypto-noble": {
2178 + "version": "13.3.0",
2179 + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto-noble/-/plugin-crypto-noble-13.3.0.tgz",
2180 + "integrity": "sha512-wmV+jBVncepgwv99G7Plrdzd0tHfbpXk2U+OD7MO7DzpDqOYEgOPi+IIneksJSTL8QvWdfi+uQEuhnER4fKouA==",
2181 + "license": "MIT",
2182 + "dependencies": {
2183 + "@noble/hashes": "^2.0.1",
2184 + "@otplib/core": "13.3.0"
2185 + }
2186 + },
2187 + "node_modules/@otplib/totp": {
2188 + "version": "13.3.0",
2189 + "resolved": "https://registry.npmjs.org/@otplib/totp/-/totp-13.3.0.tgz",
2190 + "integrity": "sha512-XfjGNoN8d9S3Ove2j7AwkVV7+QDFsV7Lm7YwSiezNaHffkWtJ60aJYpmf+01dARdPST71U2ptueMsRJso4sq4A==",
2191 + "license": "MIT",
2192 + "dependencies": {
2193 + "@otplib/core": "13.3.0",
2194 + "@otplib/hotp": "13.3.0",
2195 + "@otplib/uri": "13.3.0"
2196 + }
2197 + },
2198 + "node_modules/@otplib/uri": {
2199 + "version": "13.3.0",
2200 + "resolved": "https://registry.npmjs.org/@otplib/uri/-/uri-13.3.0.tgz",
2201 + "integrity": "sha512-3oh6nBXy+cm3UX9cxEAGZiDrfxHU2gfelYFV+XNCx+8dq39VaQVymwlU2yjPZiMAi/3agaUeEftf2RwM5F+Cyg==",
2202 + "license": "MIT",
2203 + "dependencies": {
2204 + "@otplib/core": "13.3.0"
2205 + }
2206 + },
2207 + "node_modules/@pdf-lib/standard-fonts": {
2208 + "version": "1.0.0",
2209 + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
2210 + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
2211 + "license": "MIT",
2212 + "dependencies": {
2213 + "pako": "^1.0.6"
2214 + }
2215 + },
2216 + "node_modules/@pdf-lib/upng": {
2217 + "version": "1.0.1",
2218 + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
2219 + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
2220 + "license": "MIT",
2221 + "dependencies": {
2222 + "pako": "^1.0.10"
2223 + }
2224 + },
2225 + "node_modules/@pkgjs/parseargs": {
2226 + "version": "0.11.0",
2227 + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
2228 + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
2229 + "license": "MIT",
2230 + "optional": true,
2231 + "engines": {
2232 + "node": ">=14"
2233 + }
2234 + },
2235 + "node_modules/@prisma/client": {
2236 + "version": "5.22.0",
2237 + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
2238 + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
2239 + "hasInstallScript": true,
2240 + "license": "Apache-2.0",
2241 + "engines": {
2242 + "node": ">=16.13"
2243 + },
2244 + "peerDependencies": {
2245 + "prisma": "*"
2246 + },
2247 + "peerDependenciesMeta": {
2248 + "prisma": {
2249 + "optional": true
2250 + }
2251 + }
2252 + },
2253 + "node_modules/@prisma/debug": {
2254 + "version": "5.22.0",
2255 + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
2256 + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
2257 + "license": "Apache-2.0"
2258 + },
2259 + "node_modules/@prisma/engines": {
2260 + "version": "5.22.0",
2261 + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
2262 + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
2263 + "hasInstallScript": true,
2264 + "license": "Apache-2.0",
2265 + "dependencies": {
2266 + "@prisma/debug": "5.22.0",
2267 + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
2268 + "@prisma/fetch-engine": "5.22.0",
2269 + "@prisma/get-platform": "5.22.0"
2270 + }
2271 + },
2272 + "node_modules/@prisma/engines-version": {
2273 + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
2274 + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
2275 + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
2276 + "license": "Apache-2.0"
2277 + },
2278 + "node_modules/@prisma/fetch-engine": {
2279 + "version": "5.22.0",
2280 + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
2281 + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
2282 + "license": "Apache-2.0",
2283 + "dependencies": {
2284 + "@prisma/debug": "5.22.0",
2285 + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
2286 + "@prisma/get-platform": "5.22.0"
2287 + }
2288 + },
2289 + "node_modules/@prisma/get-platform": {
2290 + "version": "5.22.0",
2291 + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
2292 + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
2293 + "license": "Apache-2.0",
2294 + "dependencies": {
2295 + "@prisma/debug": "5.22.0"
2296 + }
2297 + },
2298 + "node_modules/@rtsao/scc": {
2299 + "version": "1.1.0",
2300 + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
2301 + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
2302 + "dev": true,
2303 + "license": "MIT"
2304 + },
2305 + "node_modules/@rushstack/eslint-patch": {
2306 + "version": "1.15.0",
2307 + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz",
2308 + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==",
2309 + "dev": true,
2310 + "license": "MIT"
2311 + },
2312 + "node_modules/@scure/base": {
2313 + "version": "2.0.0",
2314 + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
2315 + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==",
2316 + "license": "MIT",
2317 + "funding": {
2318 + "url": "https://paulmillr.com/funding/"
2319 + }
2320 + },
2321 + "node_modules/@swc/counter": {
2322 + "version": "0.1.3",
2323 + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
2324 + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
2325 + "license": "Apache-2.0"
2326 + },
2327 + "node_modules/@swc/helpers": {
2328 + "version": "0.5.5",
2329 + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
2330 + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
2331 + "license": "Apache-2.0",
2332 + "dependencies": {
2333 + "@swc/counter": "^0.1.3",
2334 + "tslib": "^2.4.0"
2335 + }
2336 + },
2337 + "node_modules/@tybys/wasm-util": {
2338 + "version": "0.10.1",
2339 + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
2340 + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
2341 + "dev": true,
2342 + "license": "MIT",
2343 + "optional": true,
2344 + "dependencies": {
2345 + "tslib": "^2.4.0"
2346 + }
2347 + },
2348 + "node_modules/@types/archiver": {
2349 + "version": "7.0.0",
2350 + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz",
2351 + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==",
2352 + "dev": true,
2353 + "license": "MIT",
2354 + "dependencies": {
2355 + "@types/readdir-glob": "*"
2356 + }
2357 + },
2358 + "node_modules/@types/bcryptjs": {
2359 + "version": "2.4.6",
2360 + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
2361 + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
2362 + "dev": true,
2363 + "license": "MIT"
2364 + },
2365 + "node_modules/@types/debug": {
2366 + "version": "4.1.12",
2367 + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
2368 + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
2369 + "license": "MIT",
2370 + "dependencies": {
2371 + "@types/ms": "*"
2372 + }
2373 + },
2374 + "node_modules/@types/diff": {
2375 + "version": "7.0.2",
2376 + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz",
2377 + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==",
2378 + "dev": true,
2379 + "license": "MIT"
2380 + },
2381 + "node_modules/@types/estree": {
2382 + "version": "1.0.8",
2383 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
2384 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
2385 + "license": "MIT"
2386 + },
2387 + "node_modules/@types/estree-jsx": {
2388 + "version": "1.0.5",
2389 + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
2390 + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
2391 + "license": "MIT",
2392 + "dependencies": {
2393 + "@types/estree": "*"
2394 + }
2395 + },
2396 + "node_modules/@types/hast": {
2397 + "version": "3.0.4",
2398 + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
2399 + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
2400 + "license": "MIT",
2401 + "dependencies": {
2402 + "@types/unist": "*"
2403 + }
2404 + },
2405 + "node_modules/@types/json5": {
2406 + "version": "0.0.29",
2407 + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
2408 + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
2409 + "dev": true,
2410 + "license": "MIT"
2411 + },
2412 + "node_modules/@types/mdast": {
2413 + "version": "4.0.4",
2414 + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
2415 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
2416 + "license": "MIT",
2417 + "dependencies": {
2418 + "@types/unist": "*"
2419 + }
2420 + },
2421 + "node_modules/@types/mime-types": {
2422 + "version": "3.0.1",
2423 + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz",
2424 + "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==",
2425 + "dev": true,
2426 + "license": "MIT"
2427 + },
2428 + "node_modules/@types/ms": {
2429 + "version": "2.1.0",
2430 + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
2431 + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
2432 + "license": "MIT"
2433 + },
2434 + "node_modules/@types/node": {
2435 + "version": "20.19.33",
2436 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
2437 + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
2438 + "dev": true,
2439 + "license": "MIT",
2440 + "dependencies": {
2441 + "undici-types": "~6.21.0"
2442 + }
2443 + },
2444 + "node_modules/@types/papaparse": {
2445 + "version": "5.5.2",
2446 + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz",
2447 + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==",
2448 + "dev": true,
2449 + "license": "MIT",
2450 + "dependencies": {
2451 + "@types/node": "*"
2452 + }
2453 + },
2454 + "node_modules/@types/prismjs": {
2455 + "version": "1.26.6",
2456 + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
2457 + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
2458 + "license": "MIT"
2459 + },
2460 + "node_modules/@types/prop-types": {
2461 + "version": "15.7.15",
2462 + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
2463 + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
2464 + "license": "MIT"
2465 + },
2466 + "node_modules/@types/qrcode": {
2467 + "version": "1.5.6",
2468 + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
2469 + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
2470 + "dev": true,
2471 + "license": "MIT",
2472 + "dependencies": {
2473 + "@types/node": "*"
2474 + }
2475 + },
2476 + "node_modules/@types/react": {
2477 + "version": "18.3.28",
2478 + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
2479 + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
2480 + "license": "MIT",
2481 + "dependencies": {
2482 + "@types/prop-types": "*",
2483 + "csstype": "^3.2.2"
2484 + }
2485 + },
2486 + "node_modules/@types/react-dom": {
2487 + "version": "18.3.7",
2488 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
2489 + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
2490 + "dev": true,
2491 + "license": "MIT",
2492 + "peerDependencies": {
2493 + "@types/react": "^18.0.0"
2494 + }
2495 + },
2496 + "node_modules/@types/react-syntax-highlighter": {
2497 + "version": "15.5.13",
2498 + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
2499 + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
2500 + "dev": true,
2501 + "license": "MIT",
2502 + "dependencies": {
2503 + "@types/react": "*"
2504 + }
2505 + },
2506 + "node_modules/@types/readdir-glob": {
2507 + "version": "1.1.5",
2508 + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
2509 + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
2510 + "dev": true,
2511 + "license": "MIT",
2512 + "dependencies": {
2513 + "@types/node": "*"
2514 + }
2515 + },
2516 + "node_modules/@types/sharp": {
2517 + "version": "0.31.1",
2518 + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz",
2519 + "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==",
2520 + "dev": true,
2521 + "license": "MIT",
2522 + "dependencies": {
2523 + "@types/node": "*"
2524 + }
2525 + },
2526 + "node_modules/@types/unist": {
2527 + "version": "3.0.3",
2528 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
2529 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
2530 + "license": "MIT"
2531 + },
2532 + "node_modules/@types/uuid": {
2533 + "version": "10.0.0",
2534 + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
2535 + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
2536 + "dev": true,
2537 + "license": "MIT"
2538 + },
2539 + "node_modules/@typescript-eslint/eslint-plugin": {
2540 + "version": "8.55.0",
2541 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
2542 + "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==",
2543 + "dev": true,
2544 + "license": "MIT",
2545 + "dependencies": {
2546 + "@eslint-community/regexpp": "^4.12.2",
2547 + "@typescript-eslint/scope-manager": "8.55.0",
2548 + "@typescript-eslint/type-utils": "8.55.0",
2549 + "@typescript-eslint/utils": "8.55.0",
2550 + "@typescript-eslint/visitor-keys": "8.55.0",
2551 + "ignore": "^7.0.5",
2552 + "natural-compare": "^1.4.0",
2553 + "ts-api-utils": "^2.4.0"
2554 + },
2555 + "engines": {
2556 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2557 + },
2558 + "funding": {
2559 + "type": "opencollective",
2560 + "url": "https://opencollective.com/typescript-eslint"
2561 + },
2562 + "peerDependencies": {
2563 + "@typescript-eslint/parser": "^8.55.0",
2564 + "eslint": "^8.57.0 || ^9.0.0",
2565 + "typescript": ">=4.8.4 <6.0.0"
2566 + }
2567 + },
2568 + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
2569 + "version": "7.0.5",
2570 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
2571 + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
2572 + "dev": true,
2573 + "license": "MIT",
2574 + "engines": {
2575 + "node": ">= 4"
2576 + }
2577 + },
2578 + "node_modules/@typescript-eslint/parser": {
2579 + "version": "8.55.0",
2580 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz",
2581 + "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
2582 + "dev": true,
2583 + "license": "MIT",
2584 + "dependencies": {
2585 + "@typescript-eslint/scope-manager": "8.55.0",
2586 + "@typescript-eslint/types": "8.55.0",
2587 + "@typescript-eslint/typescript-estree": "8.55.0",
2588 + "@typescript-eslint/visitor-keys": "8.55.0",
2589 + "debug": "^4.4.3"
2590 + },
2591 + "engines": {
2592 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2593 + },
2594 + "funding": {
2595 + "type": "opencollective",
2596 + "url": "https://opencollective.com/typescript-eslint"
2597 + },
2598 + "peerDependencies": {
2599 + "eslint": "^8.57.0 || ^9.0.0",
2600 + "typescript": ">=4.8.4 <6.0.0"
2601 + }
2602 + },
2603 + "node_modules/@typescript-eslint/project-service": {
2604 + "version": "8.55.0",
2605 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz",
2606 + "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==",
2607 + "dev": true,
2608 + "license": "MIT",
2609 + "dependencies": {
2610 + "@typescript-eslint/tsconfig-utils": "^8.55.0",
2611 + "@typescript-eslint/types": "^8.55.0",
2612 + "debug": "^4.4.3"
2613 + },
2614 + "engines": {
2615 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2616 + },
2617 + "funding": {
2618 + "type": "opencollective",
2619 + "url": "https://opencollective.com/typescript-eslint"
2620 + },
2621 + "peerDependencies": {
2622 + "typescript": ">=4.8.4 <6.0.0"
2623 + }
2624 + },
2625 + "node_modules/@typescript-eslint/scope-manager": {
2626 + "version": "8.55.0",
2627 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz",
2628 + "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==",
2629 + "dev": true,
2630 + "license": "MIT",
2631 + "dependencies": {
2632 + "@typescript-eslint/types": "8.55.0",
2633 + "@typescript-eslint/visitor-keys": "8.55.0"
2634 + },
2635 + "engines": {
2636 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2637 + },
2638 + "funding": {
2639 + "type": "opencollective",
2640 + "url": "https://opencollective.com/typescript-eslint"
2641 + }
2642 + },
2643 + "node_modules/@typescript-eslint/tsconfig-utils": {
2644 + "version": "8.55.0",
2645 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz",
2646 + "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==",
2647 + "dev": true,
2648 + "license": "MIT",
2649 + "engines": {
2650 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2651 + },
2652 + "funding": {
2653 + "type": "opencollective",
2654 + "url": "https://opencollective.com/typescript-eslint"
2655 + },
2656 + "peerDependencies": {
2657 + "typescript": ">=4.8.4 <6.0.0"
2658 + }
2659 + },
2660 + "node_modules/@typescript-eslint/type-utils": {
2661 + "version": "8.55.0",
2662 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz",
2663 + "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==",
2664 + "dev": true,
2665 + "license": "MIT",
2666 + "dependencies": {
2667 + "@typescript-eslint/types": "8.55.0",
2668 + "@typescript-eslint/typescript-estree": "8.55.0",
2669 + "@typescript-eslint/utils": "8.55.0",
2670 + "debug": "^4.4.3",
2671 + "ts-api-utils": "^2.4.0"
2672 + },
2673 + "engines": {
2674 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2675 + },
2676 + "funding": {
2677 + "type": "opencollective",
2678 + "url": "https://opencollective.com/typescript-eslint"
2679 + },
2680 + "peerDependencies": {
2681 + "eslint": "^8.57.0 || ^9.0.0",
2682 + "typescript": ">=4.8.4 <6.0.0"
2683 + }
2684 + },
2685 + "node_modules/@typescript-eslint/types": {
2686 + "version": "8.55.0",
2687 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz",
2688 + "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==",
2689 + "dev": true,
2690 + "license": "MIT",
2691 + "engines": {
2692 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2693 + },
2694 + "funding": {
2695 + "type": "opencollective",
2696 + "url": "https://opencollective.com/typescript-eslint"
2697 + }
2698 + },
2699 + "node_modules/@typescript-eslint/typescript-estree": {
2700 + "version": "8.55.0",
2701 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz",
2702 + "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==",
2703 + "dev": true,
2704 + "license": "MIT",
2705 + "dependencies": {
2706 + "@typescript-eslint/project-service": "8.55.0",
2707 + "@typescript-eslint/tsconfig-utils": "8.55.0",
2708 + "@typescript-eslint/types": "8.55.0",
2709 + "@typescript-eslint/visitor-keys": "8.55.0",
2710 + "debug": "^4.4.3",
2711 + "minimatch": "^9.0.5",
2712 + "semver": "^7.7.3",
2713 + "tinyglobby": "^0.2.15",
2714 + "ts-api-utils": "^2.4.0"
2715 + },
2716 + "engines": {
2717 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2718 + },
2719 + "funding": {
2720 + "type": "opencollective",
2721 + "url": "https://opencollective.com/typescript-eslint"
2722 + },
2723 + "peerDependencies": {
2724 + "typescript": ">=4.8.4 <6.0.0"
2725 + }
2726 + },
2727 + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
2728 + "version": "2.0.2",
2729 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
2730 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
2731 + "dev": true,
2732 + "license": "MIT",
2733 + "dependencies": {
2734 + "balanced-match": "^1.0.0"
2735 + }
2736 + },
2737 + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
2738 + "version": "9.0.5",
2739 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
2740 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
2741 + "dev": true,
2742 + "license": "ISC",
2743 + "dependencies": {
2744 + "brace-expansion": "^2.0.1"
2745 + },
2746 + "engines": {
2747 + "node": ">=16 || 14 >=14.17"
2748 + },
2749 + "funding": {
2750 + "url": "https://github.com/sponsors/isaacs"
2751 + }
2752 + },
2753 + "node_modules/@typescript-eslint/utils": {
2754 + "version": "8.55.0",
2755 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz",
2756 + "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==",
2757 + "dev": true,
2758 + "license": "MIT",
2759 + "dependencies": {
2760 + "@eslint-community/eslint-utils": "^4.9.1",
2761 + "@typescript-eslint/scope-manager": "8.55.0",
2762 + "@typescript-eslint/types": "8.55.0",
2763 + "@typescript-eslint/typescript-estree": "8.55.0"
2764 + },
2765 + "engines": {
2766 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2767 + },
2768 + "funding": {
2769 + "type": "opencollective",
2770 + "url": "https://opencollective.com/typescript-eslint"
2771 + },
2772 + "peerDependencies": {
2773 + "eslint": "^8.57.0 || ^9.0.0",
2774 + "typescript": ">=4.8.4 <6.0.0"
2775 + }
2776 + },
2777 + "node_modules/@typescript-eslint/visitor-keys": {
2778 + "version": "8.55.0",
2779 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz",
2780 + "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==",
2781 + "dev": true,
2782 + "license": "MIT",
2783 + "dependencies": {
2784 + "@typescript-eslint/types": "8.55.0",
2785 + "eslint-visitor-keys": "^4.2.1"
2786 + },
2787 + "engines": {
2788 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2789 + },
2790 + "funding": {
2791 + "type": "opencollective",
2792 + "url": "https://opencollective.com/typescript-eslint"
2793 + }
2794 + },
2795 + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
2796 + "version": "4.2.1",
2797 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
2798 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
2799 + "dev": true,
2800 + "license": "Apache-2.0",
2801 + "engines": {
2802 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2803 + },
2804 + "funding": {
2805 + "url": "https://opencollective.com/eslint"
2806 + }
2807 + },
2808 + "node_modules/@ungap/structured-clone": {
2809 + "version": "1.3.0",
2810 + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
2811 + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
2812 + "license": "ISC"
2813 + },
2814 + "node_modules/@unrs/resolver-binding-android-arm-eabi": {
2815 + "version": "1.11.1",
2816 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
2817 + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
2818 + "cpu": [
2819 + "arm"
2820 + ],
2821 + "dev": true,
2822 + "license": "MIT",
2823 + "optional": true,
2824 + "os": [
2825 + "android"
2826 + ]
2827 + },
2828 + "node_modules/@unrs/resolver-binding-android-arm64": {
2829 + "version": "1.11.1",
2830 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
2831 + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
2832 + "cpu": [
2833 + "arm64"
2834 + ],
2835 + "dev": true,
2836 + "license": "MIT",
2837 + "optional": true,
2838 + "os": [
2839 + "android"
2840 + ]
2841 + },
2842 + "node_modules/@unrs/resolver-binding-darwin-arm64": {
2843 + "version": "1.11.1",
2844 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
2845 + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
2846 + "cpu": [
2847 + "arm64"
2848 + ],
2849 + "dev": true,
2850 + "license": "MIT",
2851 + "optional": true,
2852 + "os": [
2853 + "darwin"
2854 + ]
2855 + },
2856 + "node_modules/@unrs/resolver-binding-darwin-x64": {
2857 + "version": "1.11.1",
2858 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
2859 + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
2860 + "cpu": [
2861 + "x64"
2862 + ],
2863 + "dev": true,
2864 + "license": "MIT",
2865 + "optional": true,
2866 + "os": [
2867 + "darwin"
2868 + ]
2869 + },
2870 + "node_modules/@unrs/resolver-binding-freebsd-x64": {
2871 + "version": "1.11.1",
2872 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
2873 + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
2874 + "cpu": [
2875 + "x64"
2876 + ],
2877 + "dev": true,
2878 + "license": "MIT",
2879 + "optional": true,
2880 + "os": [
2881 + "freebsd"
2882 + ]
2883 + },
2884 + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
2885 + "version": "1.11.1",
2886 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
2887 + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
2888 + "cpu": [
2889 + "arm"
2890 + ],
2891 + "dev": true,
2892 + "license": "MIT",
2893 + "optional": true,
2894 + "os": [
2895 + "linux"
2896 + ]
2897 + },
2898 + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
2899 + "version": "1.11.1",
2900 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
2901 + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
2902 + "cpu": [
2903 + "arm"
2904 + ],
2905 + "dev": true,
2906 + "license": "MIT",
2907 + "optional": true,
2908 + "os": [
2909 + "linux"
2910 + ]
2911 + },
2912 + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
2913 + "version": "1.11.1",
2914 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
2915 + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
2916 + "cpu": [
2917 + "arm64"
2918 + ],
2919 + "dev": true,
2920 + "license": "MIT",
2921 + "optional": true,
2922 + "os": [
2923 + "linux"
2924 + ]
2925 + },
2926 + "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
2927 + "version": "1.11.1",
2928 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
2929 + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
2930 + "cpu": [
2931 + "arm64"
2932 + ],
2933 + "dev": true,
2934 + "license": "MIT",
2935 + "optional": true,
2936 + "os": [
2937 + "linux"
2938 + ]
2939 + },
2940 + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
2941 + "version": "1.11.1",
2942 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
2943 + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
2944 + "cpu": [
2945 + "ppc64"
2946 + ],
2947 + "dev": true,
2948 + "license": "MIT",
2949 + "optional": true,
2950 + "os": [
2951 + "linux"
2952 + ]
2953 + },
2954 + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
2955 + "version": "1.11.1",
2956 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
2957 + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
2958 + "cpu": [
2959 + "riscv64"
2960 + ],
2961 + "dev": true,
2962 + "license": "MIT",
2963 + "optional": true,
2964 + "os": [
2965 + "linux"
2966 + ]
2967 + },
2968 + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
2969 + "version": "1.11.1",
2970 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
2971 + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
2972 + "cpu": [
2973 + "riscv64"
2974 + ],
2975 + "dev": true,
2976 + "license": "MIT",
2977 + "optional": true,
2978 + "os": [
2979 + "linux"
2980 + ]
2981 + },
2982 + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
2983 + "version": "1.11.1",
2984 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
2985 + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
2986 + "cpu": [
2987 + "s390x"
2988 + ],
2989 + "dev": true,
2990 + "license": "MIT",
2991 + "optional": true,
2992 + "os": [
2993 + "linux"
2994 + ]
2995 + },
2996 + "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
2997 + "version": "1.11.1",
2998 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
2999 + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
3000 + "cpu": [
3001 + "x64"
3002 + ],
3003 + "dev": true,
3004 + "license": "MIT",
3005 + "optional": true,
3006 + "os": [
3007 + "linux"
3008 + ]
3009 + },
3010 + "node_modules/@unrs/resolver-binding-linux-x64-musl": {
3011 + "version": "1.11.1",
3012 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
3013 + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
3014 + "cpu": [
3015 + "x64"
3016 + ],
3017 + "dev": true,
3018 + "license": "MIT",
3019 + "optional": true,
3020 + "os": [
3021 + "linux"
3022 + ]
3023 + },
3024 + "node_modules/@unrs/resolver-binding-wasm32-wasi": {
3025 + "version": "1.11.1",
3026 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
3027 + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
3028 + "cpu": [
3029 + "wasm32"
3030 + ],
3031 + "dev": true,
3032 + "license": "MIT",
3033 + "optional": true,
3034 + "dependencies": {
3035 + "@napi-rs/wasm-runtime": "^0.2.11"
3036 + },
3037 + "engines": {
3038 + "node": ">=14.0.0"
3039 + }
3040 + },
3041 + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
3042 + "version": "1.11.1",
3043 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
3044 + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
3045 + "cpu": [
3046 + "arm64"
3047 + ],
3048 + "dev": true,
3049 + "license": "MIT",
3050 + "optional": true,
3051 + "os": [
3052 + "win32"
3053 + ]
3054 + },
3055 + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
3056 + "version": "1.11.1",
3057 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
3058 + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
3059 + "cpu": [
3060 + "ia32"
3061 + ],
3062 + "dev": true,
3063 + "license": "MIT",
3064 + "optional": true,
3065 + "os": [
3066 + "win32"
3067 + ]
3068 + },
3069 + "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
3070 + "version": "1.11.1",
3071 + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
3072 + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==",
3073 + "cpu": [
3074 + "x64"
3075 + ],
3076 + "dev": true,
3077 + "license": "MIT",
3078 + "optional": true,
3079 + "os": [
3080 + "win32"
3081 + ]
3082 + },
3083 + "node_modules/abort-controller": {
3084 + "version": "3.0.0",
3085 + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
3086 + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
3087 + "license": "MIT",
3088 + "dependencies": {
3089 + "event-target-shim": "^5.0.0"
3090 + },
3091 + "engines": {
3092 + "node": ">=6.5"
3093 + }
3094 + },
3095 + "node_modules/acorn": {
3096 + "version": "8.15.0",
3097 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
3098 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
3099 + "dev": true,
3100 + "license": "MIT",
3101 + "bin": {
3102 + "acorn": "bin/acorn"
3103 + },
3104 + "engines": {
3105 + "node": ">=0.4.0"
3106 + }
3107 + },
3108 + "node_modules/acorn-jsx": {
3109 + "version": "5.3.2",
3110 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
3111 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
3112 + "dev": true,
3113 + "license": "MIT",
3114 + "peerDependencies": {
3115 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
3116 + }
3117 + },
3118 + "node_modules/ajv": {
3119 + "version": "6.12.6",
3120 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
3121 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
3122 + "dev": true,
3123 + "license": "MIT",
3124 + "dependencies": {
3125 + "fast-deep-equal": "^3.1.1",
3126 + "fast-json-stable-stringify": "^2.0.0",
3127 + "json-schema-traverse": "^0.4.1",
3128 + "uri-js": "^4.2.2"
3129 + },
3130 + "funding": {
3131 + "type": "github",
3132 + "url": "https://github.com/sponsors/epoberezkin"
3133 + }
3134 + },
3135 + "node_modules/ansi-regex": {
3136 + "version": "5.0.1",
3137 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
3138 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
3139 + "license": "MIT",
3140 + "engines": {
3141 + "node": ">=8"
3142 + }
3143 + },
3144 + "node_modules/ansi-styles": {
3145 + "version": "4.3.0",
3146 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
3147 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
3148 + "license": "MIT",
3149 + "dependencies": {
3150 + "color-convert": "^2.0.1"
3151 + },
3152 + "engines": {
3153 + "node": ">=8"
3154 + },
3155 + "funding": {
3156 + "url": "https://github.com/chalk/ansi-styles?sponsor=1"
3157 + }
3158 + },
3159 + "node_modules/any-promise": {
3160 + "version": "1.3.0",
3161 + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
3162 + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
3163 + "dev": true,
3164 + "license": "MIT"
3165 + },
3166 + "node_modules/anymatch": {
3167 + "version": "3.1.3",
3168 + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
3169 + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
3170 + "dev": true,
3171 + "license": "ISC",
3172 + "dependencies": {
3173 + "normalize-path": "^3.0.0",
3174 + "picomatch": "^2.0.4"
3175 + },
3176 + "engines": {
3177 + "node": ">= 8"
3178 + }
3179 + },
3180 + "node_modules/archiver": {
3181 + "version": "7.0.1",
3182 + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
3183 + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
3184 + "license": "MIT",
3185 + "dependencies": {
3186 + "archiver-utils": "^5.0.2",
3187 + "async": "^3.2.4",
3188 + "buffer-crc32": "^1.0.0",
3189 + "readable-stream": "^4.0.0",
3190 + "readdir-glob": "^1.1.2",
3191 + "tar-stream": "^3.0.0",
3192 + "zip-stream": "^6.0.1"
3193 + },
3194 + "engines": {
3195 + "node": ">= 14"
3196 + }
3197 + },
3198 + "node_modules/archiver-utils": {
3199 + "version": "5.0.2",
3200 + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
3201 + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
3202 + "license": "MIT",
3203 + "dependencies": {
3204 + "glob": "^10.0.0",
3205 + "graceful-fs": "^4.2.0",
3206 + "is-stream": "^2.0.1",
3207 + "lazystream": "^1.0.0",
3208 + "lodash": "^4.17.15",
3209 + "normalize-path": "^3.0.0",
3210 + "readable-stream": "^4.0.0"
3211 + },
3212 + "engines": {
3213 + "node": ">= 14"
3214 + }
3215 + },
3216 + "node_modules/archiver-utils/node_modules/readable-stream": {
3217 + "version": "4.7.0",
3218 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
3219 + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
3220 + "license": "MIT",
3221 + "dependencies": {
3222 + "abort-controller": "^3.0.0",
3223 + "buffer": "^6.0.3",
3224 + "events": "^3.3.0",
3225 + "process": "^0.11.10",
3226 + "string_decoder": "^1.3.0"
3227 + },
3228 + "engines": {
3229 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
3230 + }
3231 + },
3232 + "node_modules/archiver-utils/node_modules/safe-buffer": {
3233 + "version": "5.2.1",
3234 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
3235 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
3236 + "funding": [
3237 + {
3238 + "type": "github",
3239 + "url": "https://github.com/sponsors/feross"
3240 + },
3241 + {
3242 + "type": "patreon",
3243 + "url": "https://www.patreon.com/feross"
3244 + },
3245 + {
3246 + "type": "consulting",
3247 + "url": "https://feross.org/support"
3248 + }
3249 + ],
3250 + "license": "MIT"
3251 + },
3252 + "node_modules/archiver-utils/node_modules/string_decoder": {
3253 + "version": "1.3.0",
3254 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
3255 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
3256 + "license": "MIT",
3257 + "dependencies": {
3258 + "safe-buffer": "~5.2.0"
3259 + }
3260 + },
3261 + "node_modules/archiver/node_modules/readable-stream": {
3262 + "version": "4.7.0",
3263 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
3264 + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
3265 + "license": "MIT",
3266 + "dependencies": {
3267 + "abort-controller": "^3.0.0",
3268 + "buffer": "^6.0.3",
3269 + "events": "^3.3.0",
3270 + "process": "^0.11.10",
3271 + "string_decoder": "^1.3.0"
3272 + },
3273 + "engines": {
3274 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
3275 + }
3276 + },
3277 + "node_modules/archiver/node_modules/safe-buffer": {
3278 + "version": "5.2.1",
3279 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
3280 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
3281 + "funding": [
3282 + {
3283 + "type": "github",
3284 + "url": "https://github.com/sponsors/feross"
3285 + },
3286 + {
3287 + "type": "patreon",
3288 + "url": "https://www.patreon.com/feross"
3289 + },
3290 + {
3291 + "type": "consulting",
3292 + "url": "https://feross.org/support"
3293 + }
3294 + ],
3295 + "license": "MIT"
3296 + },
3297 + "node_modules/archiver/node_modules/string_decoder": {
3298 + "version": "1.3.0",
3299 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
3300 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
3301 + "license": "MIT",
3302 + "dependencies": {
3303 + "safe-buffer": "~5.2.0"
3304 + }
3305 + },
3306 + "node_modules/arg": {
3307 + "version": "5.0.2",
3308 + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
3309 + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
3310 + "dev": true,
3311 + "license": "MIT"
3312 + },
3313 + "node_modules/argparse": {
3314 + "version": "2.0.1",
3315 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
3316 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
3317 + "dev": true,
3318 + "license": "Python-2.0"
3319 + },
3320 + "node_modules/aria-query": {
3321 + "version": "5.3.2",
3322 + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
3323 + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
3324 + "dev": true,
3325 + "license": "Apache-2.0",
3326 + "engines": {
3327 + "node": ">= 0.4"
3328 + }
3329 + },
3330 + "node_modules/array-buffer-byte-length": {
3331 + "version": "1.0.2",
3332 + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
3333 + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
3334 + "dev": true,
3335 + "license": "MIT",
3336 + "dependencies": {
3337 + "call-bound": "^1.0.3",
3338 + "is-array-buffer": "^3.0.5"
3339 + },
3340 + "engines": {
3341 + "node": ">= 0.4"
3342 + },
3343 + "funding": {
3344 + "url": "https://github.com/sponsors/ljharb"
3345 + }
3346 + },
3347 + "node_modules/array-includes": {
3348 + "version": "3.1.9",
3349 + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
3350 + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
3351 + "dev": true,
3352 + "license": "MIT",
3353 + "dependencies": {
3354 + "call-bind": "^1.0.8",
3355 + "call-bound": "^1.0.4",
3356 + "define-properties": "^1.2.1",
3357 + "es-abstract": "^1.24.0",
3358 + "es-object-atoms": "^1.1.1",
3359 + "get-intrinsic": "^1.3.0",
3360 + "is-string": "^1.1.1",
3361 + "math-intrinsics": "^1.1.0"
3362 + },
3363 + "engines": {
3364 + "node": ">= 0.4"
3365 + },
3366 + "funding": {
3367 + "url": "https://github.com/sponsors/ljharb"
3368 + }
3369 + },
3370 + "node_modules/array.prototype.findlast": {
3371 + "version": "1.2.5",
3372 + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
3373 + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
3374 + "dev": true,
3375 + "license": "MIT",
3376 + "dependencies": {
3377 + "call-bind": "^1.0.7",
3378 + "define-properties": "^1.2.1",
3379 + "es-abstract": "^1.23.2",
3380 + "es-errors": "^1.3.0",
3381 + "es-object-atoms": "^1.0.0",
3382 + "es-shim-unscopables": "^1.0.2"
3383 + },
3384 + "engines": {
3385 + "node": ">= 0.4"
3386 + },
3387 + "funding": {
3388 + "url": "https://github.com/sponsors/ljharb"
3389 + }
3390 + },
3391 + "node_modules/array.prototype.findlastindex": {
3392 + "version": "1.2.6",
3393 + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
3394 + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
3395 + "dev": true,
3396 + "license": "MIT",
3397 + "dependencies": {
3398 + "call-bind": "^1.0.8",
3399 + "call-bound": "^1.0.4",
3400 + "define-properties": "^1.2.1",
3401 + "es-abstract": "^1.23.9",
3402 + "es-errors": "^1.3.0",
3403 + "es-object-atoms": "^1.1.1",
3404 + "es-shim-unscopables": "^1.1.0"
3405 + },
3406 + "engines": {
3407 + "node": ">= 0.4"
3408 + },
3409 + "funding": {
3410 + "url": "https://github.com/sponsors/ljharb"
3411 + }
3412 + },
3413 + "node_modules/array.prototype.flat": {
3414 + "version": "1.3.3",
3415 + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
3416 + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
3417 + "dev": true,
3418 + "license": "MIT",
3419 + "dependencies": {
3420 + "call-bind": "^1.0.8",
3421 + "define-properties": "^1.2.1",
3422 + "es-abstract": "^1.23.5",
3423 + "es-shim-unscopables": "^1.0.2"
3424 + },
3425 + "engines": {
3426 + "node": ">= 0.4"
3427 + },
3428 + "funding": {
3429 + "url": "https://github.com/sponsors/ljharb"
3430 + }
3431 + },
3432 + "node_modules/array.prototype.flatmap": {
3433 + "version": "1.3.3",
3434 + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
3435 + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
3436 + "dev": true,
3437 + "license": "MIT",
3438 + "dependencies": {
3439 + "call-bind": "^1.0.8",
3440 + "define-properties": "^1.2.1",
3441 + "es-abstract": "^1.23.5",
3442 + "es-shim-unscopables": "^1.0.2"
3443 + },
3444 + "engines": {
3445 + "node": ">= 0.4"
3446 + },
3447 + "funding": {
3448 + "url": "https://github.com/sponsors/ljharb"
3449 + }
3450 + },
3451 + "node_modules/array.prototype.tosorted": {
3452 + "version": "1.1.4",
3453 + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
3454 + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
3455 + "dev": true,
3456 + "license": "MIT",
3457 + "dependencies": {
3458 + "call-bind": "^1.0.7",
3459 + "define-properties": "^1.2.1",
3460 + "es-abstract": "^1.23.3",
3461 + "es-errors": "^1.3.0",
3462 + "es-shim-unscopables": "^1.0.2"
3463 + },
3464 + "engines": {
3465 + "node": ">= 0.4"
3466 + }
3467 + },
3468 + "node_modules/arraybuffer.prototype.slice": {
3469 + "version": "1.0.4",
3470 + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
3471 + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
3472 + "dev": true,
3473 + "license": "MIT",
3474 + "dependencies": {
3475 + "array-buffer-byte-length": "^1.0.1",
3476 + "call-bind": "^1.0.8",
3477 + "define-properties": "^1.2.1",
3478 + "es-abstract": "^1.23.5",
3479 + "es-errors": "^1.3.0",
3480 + "get-intrinsic": "^1.2.6",
3481 + "is-array-buffer": "^3.0.4"
3482 + },
3483 + "engines": {
3484 + "node": ">= 0.4"
3485 + },
3486 + "funding": {
3487 + "url": "https://github.com/sponsors/ljharb"
3488 + }
3489 + },
3490 + "node_modules/ast-types-flow": {
3491 + "version": "0.0.8",
3492 + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
3493 + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
3494 + "dev": true,
3495 + "license": "MIT"
3496 + },
3497 + "node_modules/async": {
3498 + "version": "3.2.6",
3499 + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
3500 + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
3501 + "license": "MIT"
3502 + },
3503 + "node_modules/async-function": {
3504 + "version": "1.0.0",
3505 + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
3506 + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
3507 + "dev": true,
3508 + "license": "MIT",
3509 + "engines": {
3510 + "node": ">= 0.4"
3511 + }
3512 + },
3513 + "node_modules/attr-accept": {
3514 + "version": "2.2.5",
3515 + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
3516 + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
3517 + "license": "MIT",
3518 + "engines": {
3519 + "node": ">=4"
3520 + }
3521 + },
3522 + "node_modules/available-typed-arrays": {
3523 + "version": "1.0.7",
3524 + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
3525 + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
3526 + "dev": true,
3527 + "license": "MIT",
3528 + "dependencies": {
3529 + "possible-typed-array-names": "^1.0.0"
3530 + },
3531 + "engines": {
3532 + "node": ">= 0.4"
3533 + },
3534 + "funding": {
3535 + "url": "https://github.com/sponsors/ljharb"
3536 + }
3537 + },
3538 + "node_modules/axe-core": {
3539 + "version": "4.11.1",
3540 + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz",
3541 + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==",
3542 + "dev": true,
3543 + "license": "MPL-2.0",
3544 + "engines": {
3545 + "node": ">=4"
3546 + }
3547 + },
3548 + "node_modules/axobject-query": {
3549 + "version": "4.1.0",
3550 + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
3551 + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
3552 + "dev": true,
3553 + "license": "Apache-2.0",
3554 + "engines": {
3555 + "node": ">= 0.4"
3556 + }
3557 + },
3558 + "node_modules/bail": {
3559 + "version": "2.0.2",
3560 + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
3561 + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
3562 + "license": "MIT",
3563 + "funding": {
3564 + "type": "github",
3565 + "url": "https://github.com/sponsors/wooorm"
3566 + }
3567 + },
3568 + "node_modules/balanced-match": {
3569 + "version": "1.0.2",
3570 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
3571 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
3572 + "license": "MIT"
3573 + },
3574 + "node_modules/bare-events": {
3575 + "version": "2.8.2",
3576 + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
3577 + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
3578 + "license": "Apache-2.0",
3579 + "peerDependencies": {
3580 + "bare-abort-controller": "*"
3581 + },
3582 + "peerDependenciesMeta": {
3583 + "bare-abort-controller": {
3584 + "optional": true
3585 + }
3586 + }
3587 + },
3588 + "node_modules/base64-js": {
3589 + "version": "1.5.1",
3590 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
3591 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
3592 + "funding": [
3593 + {
3594 + "type": "github",
3595 + "url": "https://github.com/sponsors/feross"
3596 + },
3597 + {
3598 + "type": "patreon",
3599 + "url": "https://www.patreon.com/feross"
3600 + },
3601 + {
3602 + "type": "consulting",
3603 + "url": "https://feross.org/support"
3604 + }
3605 + ],
3606 + "license": "MIT"
3607 + },
3608 + "node_modules/bcryptjs": {
3609 + "version": "3.0.3",
3610 + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
3611 + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
3612 + "license": "BSD-3-Clause",
3613 + "bin": {
3614 + "bcrypt": "bin/bcrypt"
3615 + }
3616 + },
3617 + "node_modules/binary-extensions": {
3618 + "version": "2.3.0",
3619 + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
3620 + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
3621 + "dev": true,
3622 + "license": "MIT",
3623 + "engines": {
3624 + "node": ">=8"
3625 + },
3626 + "funding": {
3627 + "url": "https://github.com/sponsors/sindresorhus"
3628 + }
3629 + },
3630 + "node_modules/brace-expansion": {
3631 + "version": "1.1.12",
3632 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
3633 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
3634 + "dev": true,
3635 + "license": "MIT",
3636 + "dependencies": {
3637 + "balanced-match": "^1.0.0",
3638 + "concat-map": "0.0.1"
3639 + }
3640 + },
3641 + "node_modules/braces": {
3642 + "version": "3.0.3",
3643 + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
3644 + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
3645 + "dev": true,
3646 + "license": "MIT",
3647 + "dependencies": {
3648 + "fill-range": "^7.1.1"
3649 + },
3650 + "engines": {
3651 + "node": ">=8"
3652 + }
3653 + },
3654 + "node_modules/buffer": {
3655 + "version": "6.0.3",
3656 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
3657 + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
3658 + "funding": [
3659 + {
3660 + "type": "github",
3661 + "url": "https://github.com/sponsors/feross"
3662 + },
3663 + {
3664 + "type": "patreon",
3665 + "url": "https://www.patreon.com/feross"
3666 + },
3667 + {
3668 + "type": "consulting",
3669 + "url": "https://feross.org/support"
3670 + }
3671 + ],
3672 + "license": "MIT",
3673 + "dependencies": {
3674 + "base64-js": "^1.3.1",
3675 + "ieee754": "^1.2.1"
3676 + }
3677 + },
3678 + "node_modules/buffer-crc32": {
3679 + "version": "1.0.0",
3680 + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
3681 + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
3682 + "license": "MIT",
3683 + "engines": {
3684 + "node": ">=8.0.0"
3685 + }
3686 + },
3687 + "node_modules/busboy": {
3688 + "version": "1.6.0",
3689 + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
3690 + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
3691 + "dependencies": {
3692 + "streamsearch": "^1.1.0"
3693 + },
3694 + "engines": {
3695 + "node": ">=10.16.0"
3696 + }
3697 + },
3698 + "node_modules/call-bind": {
3699 + "version": "1.0.8",
3700 + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
3701 + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
3702 + "dev": true,
3703 + "license": "MIT",
3704 + "dependencies": {
3705 + "call-bind-apply-helpers": "^1.0.0",
3706 + "es-define-property": "^1.0.0",
3707 + "get-intrinsic": "^1.2.4",
3708 + "set-function-length": "^1.2.2"
3709 + },
3710 + "engines": {
3711 + "node": ">= 0.4"
3712 + },
3713 + "funding": {
3714 + "url": "https://github.com/sponsors/ljharb"
3715 + }
3716 + },
3717 + "node_modules/call-bind-apply-helpers": {
3718 + "version": "1.0.2",
3719 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
3720 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
3721 + "dev": true,
3722 + "license": "MIT",
3723 + "dependencies": {
3724 + "es-errors": "^1.3.0",
3725 + "function-bind": "^1.1.2"
3726 + },
3727 + "engines": {
3728 + "node": ">= 0.4"
3729 + }
3730 + },
3731 + "node_modules/call-bound": {
3732 + "version": "1.0.4",
3733 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
3734 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
3735 + "dev": true,
3736 + "license": "MIT",
3737 + "dependencies": {
3738 + "call-bind-apply-helpers": "^1.0.2",
3739 + "get-intrinsic": "^1.3.0"
3740 + },
3741 + "engines": {
3742 + "node": ">= 0.4"
3743 + },
3744 + "funding": {
3745 + "url": "https://github.com/sponsors/ljharb"
3746 + }
3747 + },
3748 + "node_modules/callsites": {
3749 + "version": "3.1.0",
3750 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
3751 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
3752 + "dev": true,
3753 + "license": "MIT",
3754 + "engines": {
3755 + "node": ">=6"
3756 + }
3757 + },
3758 + "node_modules/camelcase": {
3759 + "version": "5.3.1",
3760 + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
3761 + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
3762 + "license": "MIT",
3763 + "engines": {
3764 + "node": ">=6"
3765 + }
3766 + },
3767 + "node_modules/camelcase-css": {
3768 + "version": "2.0.1",
3769 + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
3770 + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
3771 + "dev": true,
3772 + "license": "MIT",
3773 + "engines": {
3774 + "node": ">= 6"
3775 + }
3776 + },
3777 + "node_modules/caniuse-lite": {
3778 + "version": "1.0.30001769",
3779 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
3780 + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
3781 + "funding": [
3782 + {
3783 + "type": "opencollective",
3784 + "url": "https://opencollective.com/browserslist"
3785 + },
3786 + {
3787 + "type": "tidelift",
3788 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
3789 + },
3790 + {
3791 + "type": "github",
3792 + "url": "https://github.com/sponsors/ai"
3793 + }
3794 + ],
3795 + "license": "CC-BY-4.0"
3796 + },
3797 + "node_modules/ccount": {
3798 + "version": "2.0.1",
3799 + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
3800 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
3801 + "license": "MIT",
3802 + "funding": {
3803 + "type": "github",
3804 + "url": "https://github.com/sponsors/wooorm"
3805 + }
3806 + },
3807 + "node_modules/chalk": {
3808 + "version": "4.1.2",
3809 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
3810 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
3811 + "dev": true,
3812 + "license": "MIT",
3813 + "dependencies": {
3814 + "ansi-styles": "^4.1.0",
3815 + "supports-color": "^7.1.0"
3816 + },
3817 + "engines": {
3818 + "node": ">=10"
3819 + },
3820 + "funding": {
3821 + "url": "https://github.com/chalk/chalk?sponsor=1"
3822 + }
3823 + },
3824 + "node_modules/character-entities": {
3825 + "version": "2.0.2",
3826 + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
3827 + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
3828 + "license": "MIT",
3829 + "funding": {
3830 + "type": "github",
3831 + "url": "https://github.com/sponsors/wooorm"
3832 + }
3833 + },
3834 + "node_modules/character-entities-html4": {
3835 + "version": "2.1.0",
3836 + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
3837 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
3838 + "license": "MIT",
3839 + "funding": {
3840 + "type": "github",
3841 + "url": "https://github.com/sponsors/wooorm"
3842 + }
3843 + },
3844 + "node_modules/character-entities-legacy": {
3845 + "version": "3.0.0",
3846 + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
3847 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
3848 + "license": "MIT",
3849 + "funding": {
3850 + "type": "github",
3851 + "url": "https://github.com/sponsors/wooorm"
3852 + }
3853 + },
3854 + "node_modules/character-reference-invalid": {
3855 + "version": "2.0.1",
3856 + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
3857 + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
3858 + "license": "MIT",
3859 + "funding": {
3860 + "type": "github",
3861 + "url": "https://github.com/sponsors/wooorm"
3862 + }
3863 + },
3864 + "node_modules/chokidar": {
3865 + "version": "3.6.0",
3866 + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
3867 + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
3868 + "dev": true,
3869 + "license": "MIT",
3870 + "dependencies": {
3871 + "anymatch": "~3.1.2",
3872 + "braces": "~3.0.2",
3873 + "glob-parent": "~5.1.2",
3874 + "is-binary-path": "~2.1.0",
3875 + "is-glob": "~4.0.1",
3876 + "normalize-path": "~3.0.0",
3877 + "readdirp": "~3.6.0"
3878 + },
3879 + "engines": {
3880 + "node": ">= 8.10.0"
3881 + },
3882 + "funding": {
3883 + "url": "https://paulmillr.com/funding/"
3884 + },
3885 + "optionalDependencies": {
3886 + "fsevents": "~2.3.2"
3887 + }
3888 + },
3889 + "node_modules/chokidar/node_modules/glob-parent": {
3890 + "version": "5.1.2",
3891 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
3892 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
3893 + "dev": true,
3894 + "license": "ISC",
3895 + "dependencies": {
3896 + "is-glob": "^4.0.1"
3897 + },
3898 + "engines": {
3899 + "node": ">= 6"
3900 + }
3901 + },
3902 + "node_modules/client-only": {
3903 + "version": "0.0.1",
3904 + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
3905 + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
3906 + "license": "MIT"
3907 + },
3908 + "node_modules/cliui": {
3909 + "version": "6.0.0",
3910 + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
3911 + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
3912 + "license": "ISC",
3913 + "dependencies": {
3914 + "string-width": "^4.2.0",
3915 + "strip-ansi": "^6.0.0",
3916 + "wrap-ansi": "^6.2.0"
3917 + }
3918 + },
3919 + "node_modules/cliui/node_modules/emoji-regex": {
3920 + "version": "8.0.0",
3921 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
3922 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
3923 + "license": "MIT"
3924 + },
3925 + "node_modules/cliui/node_modules/string-width": {
3926 + "version": "4.2.3",
3927 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
3928 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
3929 + "license": "MIT",
3930 + "dependencies": {
3931 + "emoji-regex": "^8.0.0",
3932 + "is-fullwidth-code-point": "^3.0.0",
3933 + "strip-ansi": "^6.0.1"
3934 + },
3935 + "engines": {
3936 + "node": ">=8"
3937 + }
3938 + },
3939 + "node_modules/cliui/node_modules/wrap-ansi": {
3940 + "version": "6.2.0",
3941 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
3942 + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
3943 + "license": "MIT",
3944 + "dependencies": {
3945 + "ansi-styles": "^4.0.0",
3946 + "string-width": "^4.1.0",
3947 + "strip-ansi": "^6.0.0"
3948 + },
3949 + "engines": {
3950 + "node": ">=8"
3951 + }
3952 + },
3953 + "node_modules/color-convert": {
3954 + "version": "2.0.1",
3955 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
3956 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
3957 + "license": "MIT",
3958 + "dependencies": {
3959 + "color-name": "~1.1.4"
3960 + },
3961 + "engines": {
3962 + "node": ">=7.0.0"
3963 + }
3964 + },
3965 + "node_modules/color-name": {
3966 + "version": "1.1.4",
3967 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
3968 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
3969 + "license": "MIT"
3970 + },
3971 + "node_modules/comma-separated-tokens": {
3972 + "version": "2.0.3",
3973 + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
3974 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
3975 + "license": "MIT",
3976 + "funding": {
3977 + "type": "github",
3978 + "url": "https://github.com/sponsors/wooorm"
3979 + }
3980 + },
3981 + "node_modules/commander": {
3982 + "version": "4.1.1",
3983 + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
3984 + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
3985 + "dev": true,
3986 + "license": "MIT",
3987 + "engines": {
3988 + "node": ">= 6"
3989 + }
3990 + },
3991 + "node_modules/compress-commons": {
3992 + "version": "6.0.2",
3993 + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
3994 + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
3995 + "license": "MIT",
3996 + "dependencies": {
3997 + "crc-32": "^1.2.0",
3998 + "crc32-stream": "^6.0.0",
3999 + "is-stream": "^2.0.1",
4000 + "normalize-path": "^3.0.0",
4001 + "readable-stream": "^4.0.0"
4002 + },
4003 + "engines": {
4004 + "node": ">= 14"
4005 + }
4006 + },
4007 + "node_modules/compress-commons/node_modules/readable-stream": {
4008 + "version": "4.7.0",
4009 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
4010 + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
4011 + "license": "MIT",
4012 + "dependencies": {
4013 + "abort-controller": "^3.0.0",
4014 + "buffer": "^6.0.3",
4015 + "events": "^3.3.0",
4016 + "process": "^0.11.10",
4017 + "string_decoder": "^1.3.0"
4018 + },
4019 + "engines": {
4020 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
4021 + }
4022 + },
4023 + "node_modules/compress-commons/node_modules/safe-buffer": {
4024 + "version": "5.2.1",
4025 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
4026 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
4027 + "funding": [
4028 + {
4029 + "type": "github",
4030 + "url": "https://github.com/sponsors/feross"
4031 + },
4032 + {
4033 + "type": "patreon",
4034 + "url": "https://www.patreon.com/feross"
4035 + },
4036 + {
4037 + "type": "consulting",
4038 + "url": "https://feross.org/support"
4039 + }
4040 + ],
4041 + "license": "MIT"
4042 + },
4043 + "node_modules/compress-commons/node_modules/string_decoder": {
4044 + "version": "1.3.0",
4045 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
4046 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
4047 + "license": "MIT",
4048 + "dependencies": {
4049 + "safe-buffer": "~5.2.0"
4050 + }
4051 + },
4052 + "node_modules/concat-map": {
4053 + "version": "0.0.1",
4054 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
4055 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
4056 + "dev": true,
4057 + "license": "MIT"
4058 + },
4059 + "node_modules/cookie": {
4060 + "version": "0.7.2",
4061 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
4062 + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
4063 + "license": "MIT",
4064 + "engines": {
4065 + "node": ">= 0.6"
4066 + }
4067 + },
4068 + "node_modules/core-util-is": {
4069 + "version": "1.0.3",
4070 + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
4071 + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
4072 + "license": "MIT"
4073 + },
4074 + "node_modules/crc-32": {
4075 + "version": "1.2.2",
4076 + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
4077 + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
4078 + "license": "Apache-2.0",
4079 + "bin": {
4080 + "crc32": "bin/crc32.njs"
4081 + },
4082 + "engines": {
4083 + "node": ">=0.8"
4084 + }
4085 + },
4086 + "node_modules/crc32-stream": {
4087 + "version": "6.0.0",
4088 + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
4089 + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
4090 + "license": "MIT",
4091 + "dependencies": {
4092 + "crc-32": "^1.2.0",
4093 + "readable-stream": "^4.0.0"
4094 + },
4095 + "engines": {
4096 + "node": ">= 14"
4097 + }
4098 + },
4099 + "node_modules/crc32-stream/node_modules/readable-stream": {
4100 + "version": "4.7.0",
4101 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
4102 + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
4103 + "license": "MIT",
4104 + "dependencies": {
4105 + "abort-controller": "^3.0.0",
4106 + "buffer": "^6.0.3",
4107 + "events": "^3.3.0",
4108 + "process": "^0.11.10",
4109 + "string_decoder": "^1.3.0"
4110 + },
4111 + "engines": {
4112 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
4113 + }
4114 + },
4115 + "node_modules/crc32-stream/node_modules/safe-buffer": {
4116 + "version": "5.2.1",
4117 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
4118 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
4119 + "funding": [
4120 + {
4121 + "type": "github",
4122 + "url": "https://github.com/sponsors/feross"
4123 + },
4124 + {
4125 + "type": "patreon",
4126 + "url": "https://www.patreon.com/feross"
4127 + },
4128 + {
4129 + "type": "consulting",
4130 + "url": "https://feross.org/support"
4131 + }
4132 + ],
4133 + "license": "MIT"
4134 + },
4135 + "node_modules/crc32-stream/node_modules/string_decoder": {
4136 + "version": "1.3.0",
4137 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
4138 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
4139 + "license": "MIT",
4140 + "dependencies": {
4141 + "safe-buffer": "~5.2.0"
4142 + }
4143 + },
4144 + "node_modules/crelt": {
4145 + "version": "1.0.6",
4146 + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
4147 + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
4148 + "license": "MIT"
4149 + },
4150 + "node_modules/cross-spawn": {
4151 + "version": "7.0.6",
4152 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
4153 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
4154 + "license": "MIT",
4155 + "dependencies": {
4156 + "path-key": "^3.1.0",
4157 + "shebang-command": "^2.0.0",
4158 + "which": "^2.0.1"
4159 + },
4160 + "engines": {
4161 + "node": ">= 8"
4162 + }
4163 + },
4164 + "node_modules/cssesc": {
4165 + "version": "3.0.0",
4166 + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
4167 + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
4168 + "dev": true,
4169 + "license": "MIT",
4170 + "bin": {
4171 + "cssesc": "bin/cssesc"
4172 + },
4173 + "engines": {
4174 + "node": ">=4"
4175 + }
4176 + },
4177 + "node_modules/csstype": {
4178 + "version": "3.2.3",
4179 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
4180 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
4181 + "license": "MIT"
4182 + },
4183 + "node_modules/damerau-levenshtein": {
4184 + "version": "1.0.8",
4185 + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
4186 + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
4187 + "dev": true,
4188 + "license": "BSD-2-Clause"
4189 + },
4190 + "node_modules/data-view-buffer": {
4191 + "version": "1.0.2",
4192 + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
4193 + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
4194 + "dev": true,
4195 + "license": "MIT",
4196 + "dependencies": {
4197 + "call-bound": "^1.0.3",
4198 + "es-errors": "^1.3.0",
4199 + "is-data-view": "^1.0.2"
4200 + },
4201 + "engines": {
4202 + "node": ">= 0.4"
4203 + },
4204 + "funding": {
4205 + "url": "https://github.com/sponsors/ljharb"
4206 + }
4207 + },
4208 + "node_modules/data-view-byte-length": {
4209 + "version": "1.0.2",
4210 + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
4211 + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
4212 + "dev": true,
4213 + "license": "MIT",
4214 + "dependencies": {
4215 + "call-bound": "^1.0.3",
4216 + "es-errors": "^1.3.0",
4217 + "is-data-view": "^1.0.2"
4218 + },
4219 + "engines": {
4220 + "node": ">= 0.4"
4221 + },
4222 + "funding": {
4223 + "url": "https://github.com/sponsors/inspect-js"
4224 + }
4225 + },
4226 + "node_modules/data-view-byte-offset": {
4227 + "version": "1.0.1",
4228 + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
4229 + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
4230 + "dev": true,
4231 + "license": "MIT",
4232 + "dependencies": {
4233 + "call-bound": "^1.0.2",
4234 + "es-errors": "^1.3.0",
4235 + "is-data-view": "^1.0.1"
4236 + },
4237 + "engines": {
4238 + "node": ">= 0.4"
4239 + },
4240 + "funding": {
4241 + "url": "https://github.com/sponsors/ljharb"
4242 + }
4243 + },
4244 + "node_modules/debug": {
4245 + "version": "4.4.3",
4246 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
4247 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
4248 + "license": "MIT",
4249 + "dependencies": {
4250 + "ms": "^2.1.3"
4251 + },
4252 + "engines": {
4253 + "node": ">=6.0"
4254 + },
4255 + "peerDependenciesMeta": {
4256 + "supports-color": {
4257 + "optional": true
4258 + }
4259 + }
4260 + },
4261 + "node_modules/decamelize": {
4262 + "version": "1.2.0",
4263 + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
4264 + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
4265 + "license": "MIT",
4266 + "engines": {
4267 + "node": ">=0.10.0"
4268 + }
4269 + },
4270 + "node_modules/decode-named-character-reference": {
4271 + "version": "1.3.0",
4272 + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
4273 + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
4274 + "license": "MIT",
4275 + "dependencies": {
4276 + "character-entities": "^2.0.0"
4277 + },
4278 + "funding": {
4279 + "type": "github",
4280 + "url": "https://github.com/sponsors/wooorm"
4281 + }
4282 + },
4283 + "node_modules/deep-is": {
4284 + "version": "0.1.4",
4285 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
4286 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
4287 + "dev": true,
4288 + "license": "MIT"
4289 + },
4290 + "node_modules/define-data-property": {
4291 + "version": "1.1.4",
4292 + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
4293 + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
4294 + "dev": true,
4295 + "license": "MIT",
4296 + "dependencies": {
4297 + "es-define-property": "^1.0.0",
4298 + "es-errors": "^1.3.0",
4299 + "gopd": "^1.0.1"
4300 + },
4301 + "engines": {
4302 + "node": ">= 0.4"
4303 + },
4304 + "funding": {
4305 + "url": "https://github.com/sponsors/ljharb"
4306 + }
4307 + },
4308 + "node_modules/define-properties": {
4309 + "version": "1.2.1",
4310 + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
4311 + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
4312 + "dev": true,
4313 + "license": "MIT",
4314 + "dependencies": {
4315 + "define-data-property": "^1.0.1",
4316 + "has-property-descriptors": "^1.0.0",
4317 + "object-keys": "^1.1.1"
4318 + },
4319 + "engines": {
4320 + "node": ">= 0.4"
4321 + },
4322 + "funding": {
4323 + "url": "https://github.com/sponsors/ljharb"
4324 + }
4325 + },
4326 + "node_modules/dequal": {
4327 + "version": "2.0.3",
4328 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
4329 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
4330 + "license": "MIT",
4331 + "engines": {
4332 + "node": ">=6"
4333 + }
4334 + },
4335 + "node_modules/detect-libc": {
4336 + "version": "2.1.2",
4337 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
4338 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
4339 + "license": "Apache-2.0",
4340 + "engines": {
4341 + "node": ">=8"
4342 + }
4343 + },
4344 + "node_modules/devlop": {
4345 + "version": "1.1.0",
4346 + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
4347 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
4348 + "license": "MIT",
4349 + "dependencies": {
4350 + "dequal": "^2.0.0"
4351 + },
4352 + "funding": {
4353 + "type": "github",
4354 + "url": "https://github.com/sponsors/wooorm"
4355 + }
4356 + },
4357 + "node_modules/didyoumean": {
4358 + "version": "1.2.2",
4359 + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
4360 + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
4361 + "dev": true,
4362 + "license": "Apache-2.0"
4363 + },
4364 + "node_modules/diff": {
4365 + "version": "8.0.3",
4366 + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
4367 + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
4368 + "license": "BSD-3-Clause",
4369 + "engines": {
4370 + "node": ">=0.3.1"
4371 + }
4372 + },
4373 + "node_modules/dijkstrajs": {
4374 + "version": "1.0.3",
4375 + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
4376 + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
4377 + "license": "MIT"
4378 + },
4379 + "node_modules/dlv": {
4380 + "version": "1.1.3",
4381 + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
4382 + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
4383 + "dev": true,
4384 + "license": "MIT"
4385 + },
4386 + "node_modules/doctrine": {
4387 + "version": "3.0.0",
4388 + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
4389 + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
4390 + "dev": true,
4391 + "license": "Apache-2.0",
4392 + "dependencies": {
4393 + "esutils": "^2.0.2"
4394 + },
4395 + "engines": {
4396 + "node": ">=6.0.0"
4397 + }
4398 + },
4399 + "node_modules/dunder-proto": {
4400 + "version": "1.0.1",
4401 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
4402 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
4403 + "dev": true,
4404 + "license": "MIT",
4405 + "dependencies": {
4406 + "call-bind-apply-helpers": "^1.0.1",
4407 + "es-errors": "^1.3.0",
4408 + "gopd": "^1.2.0"
4409 + },
4410 + "engines": {
4411 + "node": ">= 0.4"
4412 + }
4413 + },
4414 + "node_modules/eastasianwidth": {
4415 + "version": "0.2.0",
4416 + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
4417 + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
4418 + "license": "MIT"
4419 + },
4420 + "node_modules/emoji-regex": {
4421 + "version": "9.2.2",
4422 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
4423 + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
4424 + "license": "MIT"
4425 + },
4426 + "node_modules/es-abstract": {
4427 + "version": "1.24.1",
4428 + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
4429 + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
4430 + "dev": true,
4431 + "license": "MIT",
4432 + "dependencies": {
4433 + "array-buffer-byte-length": "^1.0.2",
4434 + "arraybuffer.prototype.slice": "^1.0.4",
4435 + "available-typed-arrays": "^1.0.7",
4436 + "call-bind": "^1.0.8",
4437 + "call-bound": "^1.0.4",
4438 + "data-view-buffer": "^1.0.2",
4439 + "data-view-byte-length": "^1.0.2",
4440 + "data-view-byte-offset": "^1.0.1",
4441 + "es-define-property": "^1.0.1",
4442 + "es-errors": "^1.3.0",
4443 + "es-object-atoms": "^1.1.1",
4444 + "es-set-tostringtag": "^2.1.0",
4445 + "es-to-primitive": "^1.3.0",
4446 + "function.prototype.name": "^1.1.8",
4447 + "get-intrinsic": "^1.3.0",
4448 + "get-proto": "^1.0.1",
4449 + "get-symbol-description": "^1.1.0",
4450 + "globalthis": "^1.0.4",
4451 + "gopd": "^1.2.0",
4452 + "has-property-descriptors": "^1.0.2",
4453 + "has-proto": "^1.2.0",
4454 + "has-symbols": "^1.1.0",
4455 + "hasown": "^2.0.2",
4456 + "internal-slot": "^1.1.0",
4457 + "is-array-buffer": "^3.0.5",
4458 + "is-callable": "^1.2.7",
4459 + "is-data-view": "^1.0.2",
4460 + "is-negative-zero": "^2.0.3",
4461 + "is-regex": "^1.2.1",
4462 + "is-set": "^2.0.3",
4463 + "is-shared-array-buffer": "^1.0.4",
4464 + "is-string": "^1.1.1",
4465 + "is-typed-array": "^1.1.15",
4466 + "is-weakref": "^1.1.1",
4467 + "math-intrinsics": "^1.1.0",
4468 + "object-inspect": "^1.13.4",
4469 + "object-keys": "^1.1.1",
4470 + "object.assign": "^4.1.7",
4471 + "own-keys": "^1.0.1",
4472 + "regexp.prototype.flags": "^1.5.4",
4473 + "safe-array-concat": "^1.1.3",
4474 + "safe-push-apply": "^1.0.0",
4475 + "safe-regex-test": "^1.1.0",
4476 + "set-proto": "^1.0.0",
4477 + "stop-iteration-iterator": "^1.1.0",
4478 + "string.prototype.trim": "^1.2.10",
4479 + "string.prototype.trimend": "^1.0.9",
4480 + "string.prototype.trimstart": "^1.0.8",
4481 + "typed-array-buffer": "^1.0.3",
4482 + "typed-array-byte-length": "^1.0.3",
4483 + "typed-array-byte-offset": "^1.0.4",
4484 + "typed-array-length": "^1.0.7",
4485 + "unbox-primitive": "^1.1.0",
4486 + "which-typed-array": "^1.1.19"
4487 + },
4488 + "engines": {
4489 + "node": ">= 0.4"
4490 + },
4491 + "funding": {
4492 + "url": "https://github.com/sponsors/ljharb"
4493 + }
4494 + },
4495 + "node_modules/es-define-property": {
4496 + "version": "1.0.1",
4497 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
4498 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
4499 + "dev": true,
4500 + "license": "MIT",
4501 + "engines": {
4502 + "node": ">= 0.4"
4503 + }
4504 + },
4505 + "node_modules/es-errors": {
4506 + "version": "1.3.0",
4507 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
4508 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
4509 + "dev": true,
4510 + "license": "MIT",
4511 + "engines": {
4512 + "node": ">= 0.4"
4513 + }
4514 + },
4515 + "node_modules/es-iterator-helpers": {
4516 + "version": "1.2.2",
4517 + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
4518 + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
4519 + "dev": true,
4520 + "license": "MIT",
4521 + "dependencies": {
4522 + "call-bind": "^1.0.8",
4523 + "call-bound": "^1.0.4",
4524 + "define-properties": "^1.2.1",
4525 + "es-abstract": "^1.24.1",
4526 + "es-errors": "^1.3.0",
4527 + "es-set-tostringtag": "^2.1.0",
4528 + "function-bind": "^1.1.2",
4529 + "get-intrinsic": "^1.3.0",
4530 + "globalthis": "^1.0.4",
4531 + "gopd": "^1.2.0",
4532 + "has-property-descriptors": "^1.0.2",
4533 + "has-proto": "^1.2.0",
4534 + "has-symbols": "^1.1.0",
4535 + "internal-slot": "^1.1.0",
4536 + "iterator.prototype": "^1.1.5",
4537 + "safe-array-concat": "^1.1.3"
4538 + },
4539 + "engines": {
4540 + "node": ">= 0.4"
4541 + }
4542 + },
4543 + "node_modules/es-object-atoms": {
4544 + "version": "1.1.1",
4545 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
4546 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
4547 + "dev": true,
4548 + "license": "MIT",
4549 + "dependencies": {
4550 + "es-errors": "^1.3.0"
4551 + },
4552 + "engines": {
4553 + "node": ">= 0.4"
4554 + }
4555 + },
4556 + "node_modules/es-set-tostringtag": {
4557 + "version": "2.1.0",
4558 + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
4559 + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
4560 + "dev": true,
4561 + "license": "MIT",
4562 + "dependencies": {
4563 + "es-errors": "^1.3.0",
4564 + "get-intrinsic": "^1.2.6",
4565 + "has-tostringtag": "^1.0.2",
4566 + "hasown": "^2.0.2"
4567 + },
4568 + "engines": {
4569 + "node": ">= 0.4"
4570 + }
4571 + },
4572 + "node_modules/es-shim-unscopables": {
4573 + "version": "1.1.0",
4574 + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
4575 + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
4576 + "dev": true,
4577 + "license": "MIT",
4578 + "dependencies": {
4579 + "hasown": "^2.0.2"
4580 + },
4581 + "engines": {
4582 + "node": ">= 0.4"
4583 + }
4584 + },
4585 + "node_modules/es-to-primitive": {
4586 + "version": "1.3.0",
4587 + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
4588 + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
4589 + "dev": true,
4590 + "license": "MIT",
4591 + "dependencies": {
4592 + "is-callable": "^1.2.7",
4593 + "is-date-object": "^1.0.5",
4594 + "is-symbol": "^1.0.4"
4595 + },
4596 + "engines": {
4597 + "node": ">= 0.4"
4598 + },
4599 + "funding": {
4600 + "url": "https://github.com/sponsors/ljharb"
4601 + }
4602 + },
4603 + "node_modules/esbuild": {
4604 + "version": "0.27.3",
4605 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
4606 + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
4607 + "dev": true,
4608 + "hasInstallScript": true,
4609 + "license": "MIT",
4610 + "bin": {
4611 + "esbuild": "bin/esbuild"
4612 + },
4613 + "engines": {
4614 + "node": ">=18"
4615 + },
4616 + "optionalDependencies": {
4617 + "@esbuild/aix-ppc64": "0.27.3",
4618 + "@esbuild/android-arm": "0.27.3",
4619 + "@esbuild/android-arm64": "0.27.3",
4620 + "@esbuild/android-x64": "0.27.3",
4621 + "@esbuild/darwin-arm64": "0.27.3",
4622 + "@esbuild/darwin-x64": "0.27.3",
4623 + "@esbuild/freebsd-arm64": "0.27.3",
4624 + "@esbuild/freebsd-x64": "0.27.3",
4625 + "@esbuild/linux-arm": "0.27.3",
4626 + "@esbuild/linux-arm64": "0.27.3",
4627 + "@esbuild/linux-ia32": "0.27.3",
4628 + "@esbuild/linux-loong64": "0.27.3",
4629 + "@esbuild/linux-mips64el": "0.27.3",
4630 + "@esbuild/linux-ppc64": "0.27.3",
4631 + "@esbuild/linux-riscv64": "0.27.3",
4632 + "@esbuild/linux-s390x": "0.27.3",
4633 + "@esbuild/linux-x64": "0.27.3",
4634 + "@esbuild/netbsd-arm64": "0.27.3",
4635 + "@esbuild/netbsd-x64": "0.27.3",
4636 + "@esbuild/openbsd-arm64": "0.27.3",
4637 + "@esbuild/openbsd-x64": "0.27.3",
4638 + "@esbuild/openharmony-arm64": "0.27.3",
4639 + "@esbuild/sunos-x64": "0.27.3",
4640 + "@esbuild/win32-arm64": "0.27.3",
4641 + "@esbuild/win32-ia32": "0.27.3",
4642 + "@esbuild/win32-x64": "0.27.3"
4643 + }
4644 + },
4645 + "node_modules/escape-string-regexp": {
4646 + "version": "4.0.0",
4647 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
4648 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
4649 + "dev": true,
4650 + "license": "MIT",
4651 + "engines": {
4652 + "node": ">=10"
4653 + },
4654 + "funding": {
4655 + "url": "https://github.com/sponsors/sindresorhus"
4656 + }
4657 + },
4658 + "node_modules/eslint": {
4659 + "version": "8.57.1",
4660 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
4661 + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
4662 + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
4663 + "dev": true,
4664 + "license": "MIT",
4665 + "dependencies": {
4666 + "@eslint-community/eslint-utils": "^4.2.0",
4667 + "@eslint-community/regexpp": "^4.6.1",
4668 + "@eslint/eslintrc": "^2.1.4",
4669 + "@eslint/js": "8.57.1",
4670 + "@humanwhocodes/config-array": "^0.13.0",
4671 + "@humanwhocodes/module-importer": "^1.0.1",
4672 + "@nodelib/fs.walk": "^1.2.8",
4673 + "@ungap/structured-clone": "^1.2.0",
4674 + "ajv": "^6.12.4",
4675 + "chalk": "^4.0.0",
4676 + "cross-spawn": "^7.0.2",
4677 + "debug": "^4.3.2",
4678 + "doctrine": "^3.0.0",
4679 + "escape-string-regexp": "^4.0.0",
4680 + "eslint-scope": "^7.2.2",
4681 + "eslint-visitor-keys": "^3.4.3",
4682 + "espree": "^9.6.1",
4683 + "esquery": "^1.4.2",
4684 + "esutils": "^2.0.2",
4685 + "fast-deep-equal": "^3.1.3",
4686 + "file-entry-cache": "^6.0.1",
4687 + "find-up": "^5.0.0",
4688 + "glob-parent": "^6.0.2",
4689 + "globals": "^13.19.0",
4690 + "graphemer": "^1.4.0",
4691 + "ignore": "^5.2.0",
4692 + "imurmurhash": "^0.1.4",
4693 + "is-glob": "^4.0.0",
4694 + "is-path-inside": "^3.0.3",
4695 + "js-yaml": "^4.1.0",
4696 + "json-stable-stringify-without-jsonify": "^1.0.1",
4697 + "levn": "^0.4.1",
4698 + "lodash.merge": "^4.6.2",
4699 + "minimatch": "^3.1.2",
4700 + "natural-compare": "^1.4.0",
4701 + "optionator": "^0.9.3",
4702 + "strip-ansi": "^6.0.1",
4703 + "text-table": "^0.2.0"
4704 + },
4705 + "bin": {
4706 + "eslint": "bin/eslint.js"
4707 + },
4708 + "engines": {
4709 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
4710 + },
4711 + "funding": {
4712 + "url": "https://opencollective.com/eslint"
4713 + }
4714 + },
4715 + "node_modules/eslint-config-next": {
4716 + "version": "14.2.35",
4717 + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.35.tgz",
4718 + "integrity": "sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==",
4719 + "dev": true,
4720 + "license": "MIT",
4721 + "dependencies": {
4722 + "@next/eslint-plugin-next": "14.2.35",
4723 + "@rushstack/eslint-patch": "^1.3.3",
4724 + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
4725 + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
4726 + "eslint-import-resolver-node": "^0.3.6",
4727 + "eslint-import-resolver-typescript": "^3.5.2",
4728 + "eslint-plugin-import": "^2.28.1",
4729 + "eslint-plugin-jsx-a11y": "^6.7.1",
4730 + "eslint-plugin-react": "^7.33.2",
4731 + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
4732 + },
4733 + "peerDependencies": {
4734 + "eslint": "^7.23.0 || ^8.0.0",
4735 + "typescript": ">=3.3.1"
4736 + },
4737 + "peerDependenciesMeta": {
4738 + "typescript": {
4739 + "optional": true
4740 + }
4741 + }
4742 + },
4743 + "node_modules/eslint-import-resolver-node": {
4744 + "version": "0.3.9",
4745 + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
4746 + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
4747 + "dev": true,
4748 + "license": "MIT",
4749 + "dependencies": {
4750 + "debug": "^3.2.7",
4751 + "is-core-module": "^2.13.0",
4752 + "resolve": "^1.22.4"
4753 + }
4754 + },
4755 + "node_modules/eslint-import-resolver-node/node_modules/debug": {
4756 + "version": "3.2.7",
4757 + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
4758 + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
4759 + "dev": true,
4760 + "license": "MIT",
4761 + "dependencies": {
4762 + "ms": "^2.1.1"
4763 + }
4764 + },
4765 + "node_modules/eslint-import-resolver-typescript": {
4766 + "version": "3.10.1",
4767 + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
4768 + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
4769 + "dev": true,
4770 + "license": "ISC",
4771 + "dependencies": {
4772 + "@nolyfill/is-core-module": "1.0.39",
4773 + "debug": "^4.4.0",
4774 + "get-tsconfig": "^4.10.0",
4775 + "is-bun-module": "^2.0.0",
4776 + "stable-hash": "^0.0.5",
4777 + "tinyglobby": "^0.2.13",
4778 + "unrs-resolver": "^1.6.2"
4779 + },
4780 + "engines": {
4781 + "node": "^14.18.0 || >=16.0.0"
4782 + },
4783 + "funding": {
4784 + "url": "https://opencollective.com/eslint-import-resolver-typescript"
4785 + },
4786 + "peerDependencies": {
4787 + "eslint": "*",
4788 + "eslint-plugin-import": "*",
4789 + "eslint-plugin-import-x": "*"
4790 + },
4791 + "peerDependenciesMeta": {
4792 + "eslint-plugin-import": {
4793 + "optional": true
4794 + },
4795 + "eslint-plugin-import-x": {
4796 + "optional": true
4797 + }
4798 + }
4799 + },
4800 + "node_modules/eslint-module-utils": {
4801 + "version": "2.12.1",
4802 + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
4803 + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
4804 + "dev": true,
4805 + "license": "MIT",
4806 + "dependencies": {
4807 + "debug": "^3.2.7"
4808 + },
4809 + "engines": {
4810 + "node": ">=4"
4811 + },
4812 + "peerDependenciesMeta": {
4813 + "eslint": {
4814 + "optional": true
4815 + }
4816 + }
4817 + },
4818 + "node_modules/eslint-module-utils/node_modules/debug": {
4819 + "version": "3.2.7",
4820 + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
4821 + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
4822 + "dev": true,
4823 + "license": "MIT",
4824 + "dependencies": {
4825 + "ms": "^2.1.1"
4826 + }
4827 + },
4828 + "node_modules/eslint-plugin-import": {
4829 + "version": "2.32.0",
4830 + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
4831 + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
4832 + "dev": true,
4833 + "license": "MIT",
4834 + "dependencies": {
4835 + "@rtsao/scc": "^1.1.0",
4836 + "array-includes": "^3.1.9",
4837 + "array.prototype.findlastindex": "^1.2.6",
4838 + "array.prototype.flat": "^1.3.3",
4839 + "array.prototype.flatmap": "^1.3.3",
4840 + "debug": "^3.2.7",
4841 + "doctrine": "^2.1.0",
4842 + "eslint-import-resolver-node": "^0.3.9",
4843 + "eslint-module-utils": "^2.12.1",
4844 + "hasown": "^2.0.2",
4845 + "is-core-module": "^2.16.1",
4846 + "is-glob": "^4.0.3",
4847 + "minimatch": "^3.1.2",
4848 + "object.fromentries": "^2.0.8",
4849 + "object.groupby": "^1.0.3",
4850 + "object.values": "^1.2.1",
4851 + "semver": "^6.3.1",
4852 + "string.prototype.trimend": "^1.0.9",
4853 + "tsconfig-paths": "^3.15.0"
4854 + },
4855 + "engines": {
4856 + "node": ">=4"
4857 + },
4858 + "peerDependencies": {
4859 + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
4860 + }
4861 + },
4862 + "node_modules/eslint-plugin-import/node_modules/debug": {
4863 + "version": "3.2.7",
4864 + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
4865 + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
4866 + "dev": true,
4867 + "license": "MIT",
4868 + "dependencies": {
4869 + "ms": "^2.1.1"
4870 + }
4871 + },
4872 + "node_modules/eslint-plugin-import/node_modules/doctrine": {
4873 + "version": "2.1.0",
4874 + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
4875 + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
4876 + "dev": true,
4877 + "license": "Apache-2.0",
4878 + "dependencies": {
4879 + "esutils": "^2.0.2"
4880 + },
4881 + "engines": {
4882 + "node": ">=0.10.0"
4883 + }
4884 + },
4885 + "node_modules/eslint-plugin-import/node_modules/semver": {
4886 + "version": "6.3.1",
4887 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
4888 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
4889 + "dev": true,
4890 + "license": "ISC",
4891 + "bin": {
4892 + "semver": "bin/semver.js"
4893 + }
4894 + },
4895 + "node_modules/eslint-plugin-jsx-a11y": {
4896 + "version": "6.10.2",
4897 + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
4898 + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
4899 + "dev": true,
4900 + "license": "MIT",
4901 + "dependencies": {
4902 + "aria-query": "^5.3.2",
4903 + "array-includes": "^3.1.8",
4904 + "array.prototype.flatmap": "^1.3.2",
4905 + "ast-types-flow": "^0.0.8",
4906 + "axe-core": "^4.10.0",
4907 + "axobject-query": "^4.1.0",
4908 + "damerau-levenshtein": "^1.0.8",
4909 + "emoji-regex": "^9.2.2",
4910 + "hasown": "^2.0.2",
4911 + "jsx-ast-utils": "^3.3.5",
4912 + "language-tags": "^1.0.9",
4913 + "minimatch": "^3.1.2",
4914 + "object.fromentries": "^2.0.8",
4915 + "safe-regex-test": "^1.0.3",
4916 + "string.prototype.includes": "^2.0.1"
4917 + },
4918 + "engines": {
4919 + "node": ">=4.0"
4920 + },
4921 + "peerDependencies": {
4922 + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
4923 + }
4924 + },
4925 + "node_modules/eslint-plugin-react": {
4926 + "version": "7.37.5",
4927 + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
4928 + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
4929 + "dev": true,
4930 + "license": "MIT",
4931 + "dependencies": {
4932 + "array-includes": "^3.1.8",
4933 + "array.prototype.findlast": "^1.2.5",
4934 + "array.prototype.flatmap": "^1.3.3",
4935 + "array.prototype.tosorted": "^1.1.4",
4936 + "doctrine": "^2.1.0",
4937 + "es-iterator-helpers": "^1.2.1",
4938 + "estraverse": "^5.3.0",
4939 + "hasown": "^2.0.2",
4940 + "jsx-ast-utils": "^2.4.1 || ^3.0.0",
4941 + "minimatch": "^3.1.2",
4942 + "object.entries": "^1.1.9",
4943 + "object.fromentries": "^2.0.8",
4944 + "object.values": "^1.2.1",
4945 + "prop-types": "^15.8.1",
4946 + "resolve": "^2.0.0-next.5",
4947 + "semver": "^6.3.1",
4948 + "string.prototype.matchall": "^4.0.12",
4949 + "string.prototype.repeat": "^1.0.0"
4950 + },
4951 + "engines": {
4952 + "node": ">=4"
4953 + },
4954 + "peerDependencies": {
4955 + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
4956 + }
4957 + },
4958 + "node_modules/eslint-plugin-react-hooks": {
4959 + "version": "5.0.0-canary-7118f5dd7-20230705",
4960 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz",
4961 + "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==",
4962 + "dev": true,
4963 + "license": "MIT",
4964 + "engines": {
4965 + "node": ">=10"
4966 + },
4967 + "peerDependencies": {
4968 + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
4969 + }
4970 + },
4971 + "node_modules/eslint-plugin-react/node_modules/doctrine": {
4972 + "version": "2.1.0",
4973 + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
4974 + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
4975 + "dev": true,
4976 + "license": "Apache-2.0",
4977 + "dependencies": {
4978 + "esutils": "^2.0.2"
4979 + },
4980 + "engines": {
4981 + "node": ">=0.10.0"
4982 + }
4983 + },
4984 + "node_modules/eslint-plugin-react/node_modules/resolve": {
4985 + "version": "2.0.0-next.5",
4986 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
4987 + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
4988 + "dev": true,
4989 + "license": "MIT",
4990 + "dependencies": {
4991 + "is-core-module": "^2.13.0",
4992 + "path-parse": "^1.0.7",
4993 + "supports-preserve-symlinks-flag": "^1.0.0"
4994 + },
4995 + "bin": {
4996 + "resolve": "bin/resolve"
4997 + },
4998 + "funding": {
4999 + "url": "https://github.com/sponsors/ljharb"
5000 + }
5001 + },
5002 + "node_modules/eslint-plugin-react/node_modules/semver": {
5003 + "version": "6.3.1",
5004 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
5005 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
5006 + "dev": true,
5007 + "license": "ISC",
5008 + "bin": {
5009 + "semver": "bin/semver.js"
5010 + }
5011 + },
5012 + "node_modules/eslint-scope": {
5013 + "version": "7.2.2",
5014 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
5015 + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
5016 + "dev": true,
5017 + "license": "BSD-2-Clause",
5018 + "dependencies": {
5019 + "esrecurse": "^4.3.0",
5020 + "estraverse": "^5.2.0"
5021 + },
5022 + "engines": {
5023 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
5024 + },
5025 + "funding": {
5026 + "url": "https://opencollective.com/eslint"
5027 + }
5028 + },
5029 + "node_modules/eslint-visitor-keys": {
5030 + "version": "3.4.3",
5031 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
5032 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
5033 + "dev": true,
5034 + "license": "Apache-2.0",
5035 + "engines": {
5036 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
5037 + },
5038 + "funding": {
5039 + "url": "https://opencollective.com/eslint"
5040 + }
5041 + },
5042 + "node_modules/espree": {
5043 + "version": "9.6.1",
5044 + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
5045 + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
5046 + "dev": true,
5047 + "license": "BSD-2-Clause",
5048 + "dependencies": {
5049 + "acorn": "^8.9.0",
5050 + "acorn-jsx": "^5.3.2",
5051 + "eslint-visitor-keys": "^3.4.1"
5052 + },
5053 + "engines": {
5054 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
5055 + },
5056 + "funding": {
5057 + "url": "https://opencollective.com/eslint"
5058 + }
5059 + },
5060 + "node_modules/esquery": {
5061 + "version": "1.7.0",
5062 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
5063 + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
5064 + "dev": true,
5065 + "license": "BSD-3-Clause",
5066 + "dependencies": {
5067 + "estraverse": "^5.1.0"
5068 + },
5069 + "engines": {
5070 + "node": ">=0.10"
5071 + }
5072 + },
5073 + "node_modules/esrecurse": {
5074 + "version": "4.3.0",
5075 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
5076 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
5077 + "dev": true,
5078 + "license": "BSD-2-Clause",
5079 + "dependencies": {
5080 + "estraverse": "^5.2.0"
5081 + },
5082 + "engines": {
5083 + "node": ">=4.0"
5084 + }
5085 + },
5086 + "node_modules/estraverse": {
5087 + "version": "5.3.0",
5088 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
5089 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
5090 + "dev": true,
5091 + "license": "BSD-2-Clause",
5092 + "engines": {
5093 + "node": ">=4.0"
5094 + }
5095 + },
5096 + "node_modules/estree-util-is-identifier-name": {
5097 + "version": "3.0.0",
5098 + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
5099 + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
5100 + "license": "MIT",
5101 + "funding": {
5102 + "type": "opencollective",
5103 + "url": "https://opencollective.com/unified"
5104 + }
5105 + },
5106 + "node_modules/esutils": {
5107 + "version": "2.0.3",
5108 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
5109 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
5110 + "dev": true,
5111 + "license": "BSD-2-Clause",
5112 + "engines": {
5113 + "node": ">=0.10.0"
5114 + }
5115 + },
5116 + "node_modules/event-target-shim": {
5117 + "version": "5.0.1",
5118 + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
5119 + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
5120 + "license": "MIT",
5121 + "engines": {
5122 + "node": ">=6"
5123 + }
5124 + },
5125 + "node_modules/events": {
5126 + "version": "3.3.0",
5127 + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
5128 + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
5129 + "license": "MIT",
5130 + "engines": {
5131 + "node": ">=0.8.x"
5132 + }
5133 + },
5134 + "node_modules/events-universal": {
5135 + "version": "1.0.1",
5136 + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
5137 + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
5138 + "license": "Apache-2.0",
5139 + "dependencies": {
5140 + "bare-events": "^2.7.0"
5141 + }
5142 + },
5143 + "node_modules/extend": {
5144 + "version": "3.0.2",
5145 + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
5146 + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
5147 + "license": "MIT"
5148 + },
5149 + "node_modules/fast-deep-equal": {
5150 + "version": "3.1.3",
5151 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
5152 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
5153 + "dev": true,
5154 + "license": "MIT"
5155 + },
5156 + "node_modules/fast-fifo": {
5157 + "version": "1.3.2",
5158 + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
5159 + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
5160 + "license": "MIT"
5161 + },
5162 + "node_modules/fast-glob": {
5163 + "version": "3.3.3",
5164 + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
5165 + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
5166 + "dev": true,
5167 + "license": "MIT",
5168 + "dependencies": {
5169 + "@nodelib/fs.stat": "^2.0.2",
5170 + "@nodelib/fs.walk": "^1.2.3",
5171 + "glob-parent": "^5.1.2",
5172 + "merge2": "^1.3.0",
5173 + "micromatch": "^4.0.8"
5174 + },
5175 + "engines": {
5176 + "node": ">=8.6.0"
5177 + }
5178 + },
5179 + "node_modules/fast-glob/node_modules/glob-parent": {
5180 + "version": "5.1.2",
5181 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
5182 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
5183 + "dev": true,
5184 + "license": "ISC",
5185 + "dependencies": {
5186 + "is-glob": "^4.0.1"
5187 + },
5188 + "engines": {
5189 + "node": ">= 6"
5190 + }
5191 + },
5192 + "node_modules/fast-json-stable-stringify": {
5193 + "version": "2.1.0",
5194 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
5195 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
5196 + "dev": true,
5197 + "license": "MIT"
5198 + },
5199 + "node_modules/fast-levenshtein": {
5200 + "version": "2.0.6",
5201 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
5202 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
5203 + "dev": true,
5204 + "license": "MIT"
5205 + },
5206 + "node_modules/fastq": {
5207 + "version": "1.20.1",
5208 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
5209 + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
5210 + "dev": true,
5211 + "license": "ISC",
5212 + "dependencies": {
5213 + "reusify": "^1.0.4"
5214 + }
5215 + },
5216 + "node_modules/fault": {
5217 + "version": "1.0.4",
5218 + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
5219 + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
5220 + "license": "MIT",
5221 + "dependencies": {
5222 + "format": "^0.2.0"
5223 + },
5224 + "funding": {
5225 + "type": "github",
5226 + "url": "https://github.com/sponsors/wooorm"
5227 + }
5228 + },
5229 + "node_modules/file-entry-cache": {
5230 + "version": "6.0.1",
5231 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
5232 + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
5233 + "dev": true,
5234 + "license": "MIT",
5235 + "dependencies": {
5236 + "flat-cache": "^3.0.4"
5237 + },
5238 + "engines": {
5239 + "node": "^10.12.0 || >=12.0.0"
5240 + }
5241 + },
5242 + "node_modules/file-selector": {
5243 + "version": "2.1.2",
5244 + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
5245 + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
5246 + "license": "MIT",
5247 + "dependencies": {
5248 + "tslib": "^2.7.0"
5249 + },
5250 + "engines": {
5251 + "node": ">= 12"
5252 + }
5253 + },
5254 + "node_modules/fill-range": {
5255 + "version": "7.1.1",
5256 + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
5257 + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
5258 + "dev": true,
5259 + "license": "MIT",
5260 + "dependencies": {
5261 + "to-regex-range": "^5.0.1"
5262 + },
5263 + "engines": {
5264 + "node": ">=8"
5265 + }
5266 + },
5267 + "node_modules/find-up": {
5268 + "version": "5.0.0",
5269 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
5270 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
5271 + "dev": true,
5272 + "license": "MIT",
5273 + "dependencies": {
5274 + "locate-path": "^6.0.0",
5275 + "path-exists": "^4.0.0"
5276 + },
5277 + "engines": {
5278 + "node": ">=10"
5279 + },
5280 + "funding": {
5281 + "url": "https://github.com/sponsors/sindresorhus"
5282 + }
5283 + },
5284 + "node_modules/flat-cache": {
5285 + "version": "3.2.0",
5286 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
5287 + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
5288 + "dev": true,
5289 + "license": "MIT",
5290 + "dependencies": {
5291 + "flatted": "^3.2.9",
5292 + "keyv": "^4.5.3",
5293 + "rimraf": "^3.0.2"
5294 + },
5295 + "engines": {
5296 + "node": "^10.12.0 || >=12.0.0"
5297 + }
5298 + },
5299 + "node_modules/flatted": {
5300 + "version": "3.3.3",
5301 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
5302 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
5303 + "dev": true,
5304 + "license": "ISC"
5305 + },
5306 + "node_modules/for-each": {
5307 + "version": "0.3.5",
5308 + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
5309 + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
5310 + "dev": true,
5311 + "license": "MIT",
5312 + "dependencies": {
5313 + "is-callable": "^1.2.7"
5314 + },
5315 + "engines": {
5316 + "node": ">= 0.4"
5317 + },
5318 + "funding": {
5319 + "url": "https://github.com/sponsors/ljharb"
5320 + }
5321 + },
5322 + "node_modules/foreground-child": {
5323 + "version": "3.3.1",
5324 + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
5325 + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
5326 + "license": "ISC",
5327 + "dependencies": {
5328 + "cross-spawn": "^7.0.6",
5329 + "signal-exit": "^4.0.1"
5330 + },
5331 + "engines": {
5332 + "node": ">=14"
5333 + },
5334 + "funding": {
5335 + "url": "https://github.com/sponsors/isaacs"
5336 + }
5337 + },
5338 + "node_modules/format": {
5339 + "version": "0.2.2",
5340 + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
5341 + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
5342 + "engines": {
5343 + "node": ">=0.4.x"
5344 + }
5345 + },
5346 + "node_modules/framer-motion": {
5347 + "version": "12.34.0",
5348 + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.0.tgz",
5349 + "integrity": "sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==",
5350 + "license": "MIT",
5351 + "dependencies": {
5352 + "motion-dom": "^12.34.0",
5353 + "motion-utils": "^12.29.2",
5354 + "tslib": "^2.4.0"
5355 + },
5356 + "peerDependencies": {
5357 + "@emotion/is-prop-valid": "*",
5358 + "react": "^18.0.0 || ^19.0.0",
5359 + "react-dom": "^18.0.0 || ^19.0.0"
5360 + },
5361 + "peerDependenciesMeta": {
5362 + "@emotion/is-prop-valid": {
5363 + "optional": true
5364 + },
5365 + "react": {
5366 + "optional": true
5367 + },
5368 + "react-dom": {
5369 + "optional": true
5370 + }
5371 + }
5372 + },
5373 + "node_modules/fs.realpath": {
5374 + "version": "1.0.0",
5375 + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
5376 + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
5377 + "dev": true,
5378 + "license": "ISC"
5379 + },
5380 + "node_modules/fsevents": {
5381 + "version": "2.3.3",
5382 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
5383 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
5384 + "hasInstallScript": true,
5385 + "license": "MIT",
5386 + "optional": true,
5387 + "os": [
5388 + "darwin"
5389 + ],
5390 + "engines": {
5391 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
5392 + }
5393 + },
5394 + "node_modules/function-bind": {
5395 + "version": "1.1.2",
5396 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
5397 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
5398 + "dev": true,
5399 + "license": "MIT",
5400 + "funding": {
5401 + "url": "https://github.com/sponsors/ljharb"
5402 + }
5403 + },
5404 + "node_modules/function.prototype.name": {
5405 + "version": "1.1.8",
5406 + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
5407 + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
5408 + "dev": true,
5409 + "license": "MIT",
5410 + "dependencies": {
5411 + "call-bind": "^1.0.8",
5412 + "call-bound": "^1.0.3",
5413 + "define-properties": "^1.2.1",
5414 + "functions-have-names": "^1.2.3",
5415 + "hasown": "^2.0.2",
5416 + "is-callable": "^1.2.7"
5417 + },
5418 + "engines": {
5419 + "node": ">= 0.4"
5420 + },
5421 + "funding": {
5422 + "url": "https://github.com/sponsors/ljharb"
5423 + }
5424 + },
5425 + "node_modules/functions-have-names": {
5426 + "version": "1.2.3",
5427 + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
5428 + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
5429 + "dev": true,
5430 + "license": "MIT",
5431 + "funding": {
5432 + "url": "https://github.com/sponsors/ljharb"
5433 + }
5434 + },
5435 + "node_modules/generator-function": {
5436 + "version": "2.0.1",
5437 + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
5438 + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
5439 + "dev": true,
5440 + "license": "MIT",
5441 + "engines": {
5442 + "node": ">= 0.4"
5443 + }
5444 + },
5445 + "node_modules/get-caller-file": {
5446 + "version": "2.0.5",
5447 + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
5448 + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
5449 + "license": "ISC",
5450 + "engines": {
5451 + "node": "6.* || 8.* || >= 10.*"
5452 + }
5453 + },
5454 + "node_modules/get-intrinsic": {
5455 + "version": "1.3.0",
5456 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
5457 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
5458 + "dev": true,
5459 + "license": "MIT",
5460 + "dependencies": {
5461 + "call-bind-apply-helpers": "^1.0.2",
5462 + "es-define-property": "^1.0.1",
5463 + "es-errors": "^1.3.0",
5464 + "es-object-atoms": "^1.1.1",
5465 + "function-bind": "^1.1.2",
5466 + "get-proto": "^1.0.1",
5467 + "gopd": "^1.2.0",
5468 + "has-symbols": "^1.1.0",
5469 + "hasown": "^2.0.2",
5470 + "math-intrinsics": "^1.1.0"
5471 + },
5472 + "engines": {
5473 + "node": ">= 0.4"
5474 + },
5475 + "funding": {
5476 + "url": "https://github.com/sponsors/ljharb"
5477 + }
5478 + },
5479 + "node_modules/get-proto": {
5480 + "version": "1.0.1",
5481 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
5482 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
5483 + "dev": true,
5484 + "license": "MIT",
5485 + "dependencies": {
5486 + "dunder-proto": "^1.0.1",
5487 + "es-object-atoms": "^1.0.0"
5488 + },
5489 + "engines": {
5490 + "node": ">= 0.4"
5491 + }
5492 + },
5493 + "node_modules/get-symbol-description": {
5494 + "version": "1.1.0",
5495 + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
5496 + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
5497 + "dev": true,
5498 + "license": "MIT",
5499 + "dependencies": {
5500 + "call-bound": "^1.0.3",
5501 + "es-errors": "^1.3.0",
5502 + "get-intrinsic": "^1.2.6"
5503 + },
5504 + "engines": {
5505 + "node": ">= 0.4"
5506 + },
5507 + "funding": {
5508 + "url": "https://github.com/sponsors/ljharb"
5509 + }
5510 + },
5511 + "node_modules/get-tsconfig": {
5512 + "version": "4.13.6",
5513 + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
5514 + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
5515 + "dev": true,
5516 + "license": "MIT",
5517 + "dependencies": {
5518 + "resolve-pkg-maps": "^1.0.0"
5519 + },
5520 + "funding": {
5521 + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
5522 + }
5523 + },
5524 + "node_modules/glob": {
5525 + "version": "10.3.10",
5526 + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
5527 + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
5528 + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
5529 + "license": "ISC",
5530 + "dependencies": {
5531 + "foreground-child": "^3.1.0",
5532 + "jackspeak": "^2.3.5",
5533 + "minimatch": "^9.0.1",
5534 + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
5535 + "path-scurry": "^1.10.1"
5536 + },
5537 + "bin": {
5538 + "glob": "dist/esm/bin.mjs"
5539 + },
5540 + "engines": {
5541 + "node": ">=16 || 14 >=14.17"
5542 + },
5543 + "funding": {
5544 + "url": "https://github.com/sponsors/isaacs"
5545 + }
5546 + },
5547 + "node_modules/glob-parent": {
5548 + "version": "6.0.2",
5549 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
5550 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
5551 + "dev": true,
5552 + "license": "ISC",
5553 + "dependencies": {
5554 + "is-glob": "^4.0.3"
5555 + },
5556 + "engines": {
5557 + "node": ">=10.13.0"
5558 + }
5559 + },
5560 + "node_modules/glob/node_modules/brace-expansion": {
5561 + "version": "2.0.2",
5562 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
5563 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
5564 + "license": "MIT",
5565 + "dependencies": {
5566 + "balanced-match": "^1.0.0"
5567 + }
5568 + },
5569 + "node_modules/glob/node_modules/minimatch": {
5570 + "version": "9.0.5",
5571 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
5572 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
5573 + "license": "ISC",
5574 + "dependencies": {
5575 + "brace-expansion": "^2.0.1"
5576 + },
5577 + "engines": {
5578 + "node": ">=16 || 14 >=14.17"
5579 + },
5580 + "funding": {
5581 + "url": "https://github.com/sponsors/isaacs"
5582 + }
5583 + },
5584 + "node_modules/globals": {
5585 + "version": "13.24.0",
5586 + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
5587 + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
5588 + "dev": true,
5589 + "license": "MIT",
5590 + "dependencies": {
5591 + "type-fest": "^0.20.2"
5592 + },
5593 + "engines": {
5594 + "node": ">=8"
5595 + },
5596 + "funding": {
5597 + "url": "https://github.com/sponsors/sindresorhus"
5598 + }
5599 + },
5600 + "node_modules/globalthis": {
5601 + "version": "1.0.4",
5602 + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
5603 + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
5604 + "dev": true,
5605 + "license": "MIT",
5606 + "dependencies": {
5607 + "define-properties": "^1.2.1",
5608 + "gopd": "^1.0.1"
5609 + },
5610 + "engines": {
5611 + "node": ">= 0.4"
5612 + },
5613 + "funding": {
5614 + "url": "https://github.com/sponsors/ljharb"
5615 + }
5616 + },
5617 + "node_modules/goober": {
5618 + "version": "2.1.18",
5619 + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
5620 + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
5621 + "license": "MIT",
5622 + "peerDependencies": {
5623 + "csstype": "^3.0.10"
5624 + }
5625 + },
5626 + "node_modules/gopd": {
5627 + "version": "1.2.0",
5628 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
5629 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
5630 + "dev": true,
5631 + "license": "MIT",
5632 + "engines": {
5633 + "node": ">= 0.4"
5634 + },
5635 + "funding": {
5636 + "url": "https://github.com/sponsors/ljharb"
5637 + }
5638 + },
5639 + "node_modules/graceful-fs": {
5640 + "version": "4.2.11",
5641 + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
5642 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
5643 + "license": "ISC"
5644 + },
5645 + "node_modules/graphemer": {
5646 + "version": "1.4.0",
5647 + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
5648 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
5649 + "dev": true,
5650 + "license": "MIT"
5651 + },
5652 + "node_modules/has-bigints": {
5653 + "version": "1.1.0",
5654 + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
5655 + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
5656 + "dev": true,
5657 + "license": "MIT",
5658 + "engines": {
5659 + "node": ">= 0.4"
5660 + },
5661 + "funding": {
5662 + "url": "https://github.com/sponsors/ljharb"
5663 + }
5664 + },
5665 + "node_modules/has-flag": {
5666 + "version": "4.0.0",
5667 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
5668 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
5669 + "dev": true,
5670 + "license": "MIT",
5671 + "engines": {
5672 + "node": ">=8"
5673 + }
5674 + },
5675 + "node_modules/has-property-descriptors": {
5676 + "version": "1.0.2",
5677 + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
5678 + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
5679 + "dev": true,
5680 + "license": "MIT",
5681 + "dependencies": {
5682 + "es-define-property": "^1.0.0"
5683 + },
5684 + "funding": {
5685 + "url": "https://github.com/sponsors/ljharb"
5686 + }
5687 + },
5688 + "node_modules/has-proto": {
5689 + "version": "1.2.0",
5690 + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
5691 + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
5692 + "dev": true,
5693 + "license": "MIT",
5694 + "dependencies": {
5695 + "dunder-proto": "^1.0.0"
5696 + },
5697 + "engines": {
5698 + "node": ">= 0.4"
5699 + },
5700 + "funding": {
5701 + "url": "https://github.com/sponsors/ljharb"
5702 + }
5703 + },
5704 + "node_modules/has-symbols": {
5705 + "version": "1.1.0",
5706 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
5707 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
5708 + "dev": true,
5709 + "license": "MIT",
5710 + "engines": {
5711 + "node": ">= 0.4"
5712 + },
5713 + "funding": {
5714 + "url": "https://github.com/sponsors/ljharb"
5715 + }
5716 + },
5717 + "node_modules/has-tostringtag": {
5718 + "version": "1.0.2",
5719 + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
5720 + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
5721 + "dev": true,
5722 + "license": "MIT",
5723 + "dependencies": {
5724 + "has-symbols": "^1.0.3"
5725 + },
5726 + "engines": {
5727 + "node": ">= 0.4"
5728 + },
5729 + "funding": {
5730 + "url": "https://github.com/sponsors/ljharb"
5731 + }
5732 + },
5733 + "node_modules/hasown": {
5734 + "version": "2.0.2",
5735 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
5736 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
5737 + "dev": true,
5738 + "license": "MIT",
5739 + "dependencies": {
5740 + "function-bind": "^1.1.2"
5741 + },
5742 + "engines": {
5743 + "node": ">= 0.4"
5744 + }
5745 + },
5746 + "node_modules/hast-util-parse-selector": {
5747 + "version": "4.0.0",
5748 + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
5749 + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
5750 + "license": "MIT",
5751 + "dependencies": {
5752 + "@types/hast": "^3.0.0"
5753 + },
5754 + "funding": {
5755 + "type": "opencollective",
5756 + "url": "https://opencollective.com/unified"
5757 + }
5758 + },
5759 + "node_modules/hast-util-to-jsx-runtime": {
5760 + "version": "2.3.6",
5761 + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
5762 + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
5763 + "license": "MIT",
5764 + "dependencies": {
5765 + "@types/estree": "^1.0.0",
5766 + "@types/hast": "^3.0.0",
5767 + "@types/unist": "^3.0.0",
5768 + "comma-separated-tokens": "^2.0.0",
5769 + "devlop": "^1.0.0",
5770 + "estree-util-is-identifier-name": "^3.0.0",
5771 + "hast-util-whitespace": "^3.0.0",
5772 + "mdast-util-mdx-expression": "^2.0.0",
5773 + "mdast-util-mdx-jsx": "^3.0.0",
5774 + "mdast-util-mdxjs-esm": "^2.0.0",
5775 + "property-information": "^7.0.0",
5776 + "space-separated-tokens": "^2.0.0",
5777 + "style-to-js": "^1.0.0",
5778 + "unist-util-position": "^5.0.0",
5779 + "vfile-message": "^4.0.0"
5780 + },
5781 + "funding": {
5782 + "type": "opencollective",
5783 + "url": "https://opencollective.com/unified"
5784 + }
5785 + },
5786 + "node_modules/hast-util-whitespace": {
5787 + "version": "3.0.0",
5788 + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
5789 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
5790 + "license": "MIT",
5791 + "dependencies": {
5792 + "@types/hast": "^3.0.0"
5793 + },
5794 + "funding": {
5795 + "type": "opencollective",
5796 + "url": "https://opencollective.com/unified"
5797 + }
5798 + },
5799 + "node_modules/hastscript": {
5800 + "version": "9.0.1",
5801 + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
5802 + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
5803 + "license": "MIT",
5804 + "dependencies": {
5805 + "@types/hast": "^3.0.0",
5806 + "comma-separated-tokens": "^2.0.0",
5807 + "hast-util-parse-selector": "^4.0.0",
5808 + "property-information": "^7.0.0",
5809 + "space-separated-tokens": "^2.0.0"
5810 + },
5811 + "funding": {
5812 + "type": "opencollective",
5813 + "url": "https://opencollective.com/unified"
5814 + }
5815 + },
5816 + "node_modules/highlight.js": {
5817 + "version": "10.7.3",
5818 + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
5819 + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
5820 + "license": "BSD-3-Clause",
5821 + "engines": {
5822 + "node": "*"
5823 + }
5824 + },
5825 + "node_modules/highlightjs-vue": {
5826 + "version": "1.0.0",
5827 + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
5828 + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
5829 + "license": "CC0-1.0"
5830 + },
5831 + "node_modules/html-url-attributes": {
5832 + "version": "3.0.1",
5833 + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
5834 + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
5835 + "license": "MIT",
5836 + "funding": {
5837 + "type": "opencollective",
5838 + "url": "https://opencollective.com/unified"
5839 + }
5840 + },
5841 + "node_modules/ieee754": {
5842 + "version": "1.2.1",
5843 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
5844 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
5845 + "funding": [
5846 + {
5847 + "type": "github",
5848 + "url": "https://github.com/sponsors/feross"
5849 + },
5850 + {
5851 + "type": "patreon",
5852 + "url": "https://www.patreon.com/feross"
5853 + },
5854 + {
5855 + "type": "consulting",
5856 + "url": "https://feross.org/support"
5857 + }
5858 + ],
5859 + "license": "BSD-3-Clause"
5860 + },
5861 + "node_modules/ignore": {
5862 + "version": "5.3.2",
5863 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
5864 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
5865 + "dev": true,
5866 + "license": "MIT",
5867 + "engines": {
5868 + "node": ">= 4"
5869 + }
5870 + },
5871 + "node_modules/immediate": {
5872 + "version": "3.0.6",
5873 + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
5874 + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
5875 + "license": "MIT"
5876 + },
5877 + "node_modules/import-fresh": {
5878 + "version": "3.3.1",
5879 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
5880 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
5881 + "dev": true,
5882 + "license": "MIT",
5883 + "dependencies": {
5884 + "parent-module": "^1.0.0",
5885 + "resolve-from": "^4.0.0"
5886 + },
5887 + "engines": {
5888 + "node": ">=6"
5889 + },
5890 + "funding": {
5891 + "url": "https://github.com/sponsors/sindresorhus"
5892 + }
5893 + },
5894 + "node_modules/imurmurhash": {
5895 + "version": "0.1.4",
5896 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
5897 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
5898 + "dev": true,
5899 + "license": "MIT",
5900 + "engines": {
5901 + "node": ">=0.8.19"
5902 + }
5903 + },
5904 + "node_modules/inflight": {
5905 + "version": "1.0.6",
5906 + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
5907 + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
5908 + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
5909 + "dev": true,
5910 + "license": "ISC",
5911 + "dependencies": {
5912 + "once": "^1.3.0",
5913 + "wrappy": "1"
5914 + }
5915 + },
5916 + "node_modules/inherits": {
5917 + "version": "2.0.4",
5918 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
5919 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
5920 + "license": "ISC"
5921 + },
5922 + "node_modules/inline-style-parser": {
5923 + "version": "0.2.7",
5924 + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
5925 + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
5926 + "license": "MIT"
5927 + },
5928 + "node_modules/internal-slot": {
5929 + "version": "1.1.0",
5930 + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
5931 + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
5932 + "dev": true,
5933 + "license": "MIT",
5934 + "dependencies": {
5935 + "es-errors": "^1.3.0",
5936 + "hasown": "^2.0.2",
5937 + "side-channel": "^1.1.0"
5938 + },
5939 + "engines": {
5940 + "node": ">= 0.4"
5941 + }
5942 + },
5943 + "node_modules/iron-session": {
5944 + "version": "8.0.4",
5945 + "resolved": "https://registry.npmjs.org/iron-session/-/iron-session-8.0.4.tgz",
5946 + "integrity": "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==",
5947 + "funding": [
5948 + "https://github.com/sponsors/vvo",
5949 + "https://github.com/sponsors/brc-dd"
5950 + ],
5951 + "license": "MIT",
5952 + "dependencies": {
5953 + "cookie": "^0.7.2",
5954 + "iron-webcrypto": "^1.2.1",
5955 + "uncrypto": "^0.1.3"
5956 + }
5957 + },
5958 + "node_modules/iron-webcrypto": {
5959 + "version": "1.2.1",
5960 + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
5961 + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
5962 + "license": "MIT",
5963 + "funding": {
5964 + "url": "https://github.com/sponsors/brc-dd"
5965 + }
5966 + },
5967 + "node_modules/is-alphabetical": {
5968 + "version": "2.0.1",
5969 + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
5970 + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
5971 + "license": "MIT",
5972 + "funding": {
5973 + "type": "github",
5974 + "url": "https://github.com/sponsors/wooorm"
5975 + }
5976 + },
5977 + "node_modules/is-alphanumerical": {
5978 + "version": "2.0.1",
5979 + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
5980 + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
5981 + "license": "MIT",
5982 + "dependencies": {
5983 + "is-alphabetical": "^2.0.0",
5984 + "is-decimal": "^2.0.0"
5985 + },
5986 + "funding": {
5987 + "type": "github",
5988 + "url": "https://github.com/sponsors/wooorm"
5989 + }
5990 + },
5991 + "node_modules/is-array-buffer": {
5992 + "version": "3.0.5",
5993 + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
5994 + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
5995 + "dev": true,
5996 + "license": "MIT",
5997 + "dependencies": {
5998 + "call-bind": "^1.0.8",
5999 + "call-bound": "^1.0.3",
6000 + "get-intrinsic": "^1.2.6"
6001 + },
6002 + "engines": {
6003 + "node": ">= 0.4"
6004 + },
6005 + "funding": {
6006 + "url": "https://github.com/sponsors/ljharb"
6007 + }
6008 + },
6009 + "node_modules/is-async-function": {
6010 + "version": "2.1.1",
6011 + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
6012 + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
6013 + "dev": true,
6014 + "license": "MIT",
6015 + "dependencies": {
6016 + "async-function": "^1.0.0",
6017 + "call-bound": "^1.0.3",
6018 + "get-proto": "^1.0.1",
6019 + "has-tostringtag": "^1.0.2",
6020 + "safe-regex-test": "^1.1.0"
6021 + },
6022 + "engines": {
6023 + "node": ">= 0.4"
6024 + },
6025 + "funding": {
6026 + "url": "https://github.com/sponsors/ljharb"
6027 + }
6028 + },
6029 + "node_modules/is-bigint": {
6030 + "version": "1.1.0",
6031 + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
6032 + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
6033 + "dev": true,
6034 + "license": "MIT",
6035 + "dependencies": {
6036 + "has-bigints": "^1.0.2"
6037 + },
6038 + "engines": {
6039 + "node": ">= 0.4"
6040 + },
6041 + "funding": {
6042 + "url": "https://github.com/sponsors/ljharb"
6043 + }
6044 + },
6045 + "node_modules/is-binary-path": {
6046 + "version": "2.1.0",
6047 + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
6048 + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
6049 + "dev": true,
6050 + "license": "MIT",
6051 + "dependencies": {
6052 + "binary-extensions": "^2.0.0"
6053 + },
6054 + "engines": {
6055 + "node": ">=8"
6056 + }
6057 + },
6058 + "node_modules/is-boolean-object": {
6059 + "version": "1.2.2",
6060 + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
6061 + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
6062 + "dev": true,
6063 + "license": "MIT",
6064 + "dependencies": {
6065 + "call-bound": "^1.0.3",
6066 + "has-tostringtag": "^1.0.2"
6067 + },
6068 + "engines": {
6069 + "node": ">= 0.4"
6070 + },
6071 + "funding": {
6072 + "url": "https://github.com/sponsors/ljharb"
6073 + }
6074 + },
6075 + "node_modules/is-bun-module": {
6076 + "version": "2.0.0",
6077 + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
6078 + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
6079 + "dev": true,
6080 + "license": "MIT",
6081 + "dependencies": {
6082 + "semver": "^7.7.1"
6083 + }
6084 + },
6085 + "node_modules/is-callable": {
6086 + "version": "1.2.7",
6087 + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
6088 + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
6089 + "dev": true,
6090 + "license": "MIT",
6091 + "engines": {
6092 + "node": ">= 0.4"
6093 + },
6094 + "funding": {
6095 + "url": "https://github.com/sponsors/ljharb"
6096 + }
6097 + },
6098 + "node_modules/is-core-module": {
6099 + "version": "2.16.1",
6100 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
6101 + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
6102 + "dev": true,
6103 + "license": "MIT",
6104 + "dependencies": {
6105 + "hasown": "^2.0.2"
6106 + },
6107 + "engines": {
6108 + "node": ">= 0.4"
6109 + },
6110 + "funding": {
6111 + "url": "https://github.com/sponsors/ljharb"
6112 + }
6113 + },
6114 + "node_modules/is-data-view": {
6115 + "version": "1.0.2",
6116 + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
6117 + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
6118 + "dev": true,
6119 + "license": "MIT",
6120 + "dependencies": {
6121 + "call-bound": "^1.0.2",
6122 + "get-intrinsic": "^1.2.6",
6123 + "is-typed-array": "^1.1.13"
6124 + },
6125 + "engines": {
6126 + "node": ">= 0.4"
6127 + },
6128 + "funding": {
6129 + "url": "https://github.com/sponsors/ljharb"
6130 + }
6131 + },
6132 + "node_modules/is-date-object": {
6133 + "version": "1.1.0",
6134 + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
6135 + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
6136 + "dev": true,
6137 + "license": "MIT",
6138 + "dependencies": {
6139 + "call-bound": "^1.0.2",
6140 + "has-tostringtag": "^1.0.2"
6141 + },
6142 + "engines": {
6143 + "node": ">= 0.4"
6144 + },
6145 + "funding": {
6146 + "url": "https://github.com/sponsors/ljharb"
6147 + }
6148 + },
6149 + "node_modules/is-decimal": {
6150 + "version": "2.0.1",
6151 + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
6152 + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
6153 + "license": "MIT",
6154 + "funding": {
6155 + "type": "github",
6156 + "url": "https://github.com/sponsors/wooorm"
6157 + }
6158 + },
6159 + "node_modules/is-extglob": {
6160 + "version": "2.1.1",
6161 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
6162 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
6163 + "dev": true,
6164 + "license": "MIT",
6165 + "engines": {
6166 + "node": ">=0.10.0"
6167 + }
6168 + },
6169 + "node_modules/is-finalizationregistry": {
6170 + "version": "1.1.1",
6171 + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
6172 + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
6173 + "dev": true,
6174 + "license": "MIT",
6175 + "dependencies": {
6176 + "call-bound": "^1.0.3"
6177 + },
6178 + "engines": {
6179 + "node": ">= 0.4"
6180 + },
6181 + "funding": {
6182 + "url": "https://github.com/sponsors/ljharb"
6183 + }
6184 + },
6185 + "node_modules/is-fullwidth-code-point": {
6186 + "version": "3.0.0",
6187 + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
6188 + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
6189 + "license": "MIT",
6190 + "engines": {
6191 + "node": ">=8"
6192 + }
6193 + },
6194 + "node_modules/is-generator-function": {
6195 + "version": "1.1.2",
6196 + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
6197 + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
6198 + "dev": true,
6199 + "license": "MIT",
6200 + "dependencies": {
6201 + "call-bound": "^1.0.4",
6202 + "generator-function": "^2.0.0",
6203 + "get-proto": "^1.0.1",
6204 + "has-tostringtag": "^1.0.2",
6205 + "safe-regex-test": "^1.1.0"
6206 + },
6207 + "engines": {
6208 + "node": ">= 0.4"
6209 + },
6210 + "funding": {
6211 + "url": "https://github.com/sponsors/ljharb"
6212 + }
6213 + },
6214 + "node_modules/is-glob": {
6215 + "version": "4.0.3",
6216 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
6217 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
6218 + "dev": true,
6219 + "license": "MIT",
6220 + "dependencies": {
6221 + "is-extglob": "^2.1.1"
6222 + },
6223 + "engines": {
6224 + "node": ">=0.10.0"
6225 + }
6226 + },
6227 + "node_modules/is-hexadecimal": {
6228 + "version": "2.0.1",
6229 + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
6230 + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
6231 + "license": "MIT",
6232 + "funding": {
6233 + "type": "github",
6234 + "url": "https://github.com/sponsors/wooorm"
6235 + }
6236 + },
6237 + "node_modules/is-map": {
6238 + "version": "2.0.3",
6239 + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
6240 + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
6241 + "dev": true,
6242 + "license": "MIT",
6243 + "engines": {
6244 + "node": ">= 0.4"
6245 + },
6246 + "funding": {
6247 + "url": "https://github.com/sponsors/ljharb"
6248 + }
6249 + },
6250 + "node_modules/is-negative-zero": {
6251 + "version": "2.0.3",
6252 + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
6253 + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
6254 + "dev": true,
6255 + "license": "MIT",
6256 + "engines": {
6257 + "node": ">= 0.4"
6258 + },
6259 + "funding": {
6260 + "url": "https://github.com/sponsors/ljharb"
6261 + }
6262 + },
6263 + "node_modules/is-number": {
6264 + "version": "7.0.0",
6265 + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
6266 + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
6267 + "dev": true,
6268 + "license": "MIT",
6269 + "engines": {
6270 + "node": ">=0.12.0"
6271 + }
6272 + },
6273 + "node_modules/is-number-object": {
6274 + "version": "1.1.1",
6275 + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
6276 + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
6277 + "dev": true,
6278 + "license": "MIT",
6279 + "dependencies": {
6280 + "call-bound": "^1.0.3",
6281 + "has-tostringtag": "^1.0.2"
6282 + },
6283 + "engines": {
6284 + "node": ">= 0.4"
6285 + },
6286 + "funding": {
6287 + "url": "https://github.com/sponsors/ljharb"
6288 + }
6289 + },
6290 + "node_modules/is-path-inside": {
6291 + "version": "3.0.3",
6292 + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
6293 + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
6294 + "dev": true,
6295 + "license": "MIT",
6296 + "engines": {
6297 + "node": ">=8"
6298 + }
6299 + },
6300 + "node_modules/is-plain-obj": {
6301 + "version": "4.1.0",
6302 + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
6303 + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
6304 + "license": "MIT",
6305 + "engines": {
6306 + "node": ">=12"
6307 + },
6308 + "funding": {
6309 + "url": "https://github.com/sponsors/sindresorhus"
6310 + }
6311 + },
6312 + "node_modules/is-regex": {
6313 + "version": "1.2.1",
6314 + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
6315 + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
6316 + "dev": true,
6317 + "license": "MIT",
6318 + "dependencies": {
6319 + "call-bound": "^1.0.2",
6320 + "gopd": "^1.2.0",
6321 + "has-tostringtag": "^1.0.2",
6322 + "hasown": "^2.0.2"
6323 + },
6324 + "engines": {
6325 + "node": ">= 0.4"
6326 + },
6327 + "funding": {
6328 + "url": "https://github.com/sponsors/ljharb"
6329 + }
6330 + },
6331 + "node_modules/is-set": {
6332 + "version": "2.0.3",
6333 + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
6334 + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
6335 + "dev": true,
6336 + "license": "MIT",
6337 + "engines": {
6338 + "node": ">= 0.4"
6339 + },
6340 + "funding": {
6341 + "url": "https://github.com/sponsors/ljharb"
6342 + }
6343 + },
6344 + "node_modules/is-shared-array-buffer": {
6345 + "version": "1.0.4",
6346 + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
6347 + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
6348 + "dev": true,
6349 + "license": "MIT",
6350 + "dependencies": {
6351 + "call-bound": "^1.0.3"
6352 + },
6353 + "engines": {
6354 + "node": ">= 0.4"
6355 + },
6356 + "funding": {
6357 + "url": "https://github.com/sponsors/ljharb"
6358 + }
6359 + },
6360 + "node_modules/is-stream": {
6361 + "version": "2.0.1",
6362 + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
6363 + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
6364 + "license": "MIT",
6365 + "engines": {
6366 + "node": ">=8"
6367 + },
6368 + "funding": {
6369 + "url": "https://github.com/sponsors/sindresorhus"
6370 + }
6371 + },
6372 + "node_modules/is-string": {
6373 + "version": "1.1.1",
6374 + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
6375 + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
6376 + "dev": true,
6377 + "license": "MIT",
6378 + "dependencies": {
6379 + "call-bound": "^1.0.3",
6380 + "has-tostringtag": "^1.0.2"
6381 + },
6382 + "engines": {
6383 + "node": ">= 0.4"
6384 + },
6385 + "funding": {
6386 + "url": "https://github.com/sponsors/ljharb"
6387 + }
6388 + },
6389 + "node_modules/is-symbol": {
6390 + "version": "1.1.1",
6391 + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
6392 + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
6393 + "dev": true,
6394 + "license": "MIT",
6395 + "dependencies": {
6396 + "call-bound": "^1.0.2",
6397 + "has-symbols": "^1.1.0",
6398 + "safe-regex-test": "^1.1.0"
6399 + },
6400 + "engines": {
6401 + "node": ">= 0.4"
6402 + },
6403 + "funding": {
6404 + "url": "https://github.com/sponsors/ljharb"
6405 + }
6406 + },
6407 + "node_modules/is-typed-array": {
6408 + "version": "1.1.15",
6409 + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
6410 + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
6411 + "dev": true,
6412 + "license": "MIT",
6413 + "dependencies": {
6414 + "which-typed-array": "^1.1.16"
6415 + },
6416 + "engines": {
6417 + "node": ">= 0.4"
6418 + },
6419 + "funding": {
6420 + "url": "https://github.com/sponsors/ljharb"
6421 + }
6422 + },
6423 + "node_modules/is-weakmap": {
6424 + "version": "2.0.2",
6425 + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
6426 + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
6427 + "dev": true,
6428 + "license": "MIT",
6429 + "engines": {
6430 + "node": ">= 0.4"
6431 + },
6432 + "funding": {
6433 + "url": "https://github.com/sponsors/ljharb"
6434 + }
6435 + },
6436 + "node_modules/is-weakref": {
6437 + "version": "1.1.1",
6438 + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
6439 + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
6440 + "dev": true,
6441 + "license": "MIT",
6442 + "dependencies": {
6443 + "call-bound": "^1.0.3"
6444 + },
6445 + "engines": {
6446 + "node": ">= 0.4"
6447 + },
6448 + "funding": {
6449 + "url": "https://github.com/sponsors/ljharb"
6450 + }
6451 + },
6452 + "node_modules/is-weakset": {
6453 + "version": "2.0.4",
6454 + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
6455 + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
6456 + "dev": true,
6457 + "license": "MIT",
6458 + "dependencies": {
6459 + "call-bound": "^1.0.3",
6460 + "get-intrinsic": "^1.2.6"
6461 + },
6462 + "engines": {
6463 + "node": ">= 0.4"
6464 + },
6465 + "funding": {
6466 + "url": "https://github.com/sponsors/ljharb"
6467 + }
6468 + },
6469 + "node_modules/isarray": {
6470 + "version": "2.0.5",
6471 + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
6472 + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
6473 + "dev": true,
6474 + "license": "MIT"
6475 + },
6476 + "node_modules/isexe": {
6477 + "version": "2.0.0",
6478 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
6479 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
6480 + "license": "ISC"
6481 + },
6482 + "node_modules/iterator.prototype": {
6483 + "version": "1.1.5",
6484 + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
6485 + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
6486 + "dev": true,
6487 + "license": "MIT",
6488 + "dependencies": {
6489 + "define-data-property": "^1.1.4",
6490 + "es-object-atoms": "^1.0.0",
6491 + "get-intrinsic": "^1.2.6",
6492 + "get-proto": "^1.0.0",
6493 + "has-symbols": "^1.1.0",
6494 + "set-function-name": "^2.0.2"
6495 + },
6496 + "engines": {
6497 + "node": ">= 0.4"
6498 + }
6499 + },
6500 + "node_modules/jackspeak": {
6501 + "version": "2.3.6",
6502 + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
6503 + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
6504 + "license": "BlueOak-1.0.0",
6505 + "dependencies": {
6506 + "@isaacs/cliui": "^8.0.2"
6507 + },
6508 + "engines": {
6509 + "node": ">=14"
6510 + },
6511 + "funding": {
6512 + "url": "https://github.com/sponsors/isaacs"
6513 + },
6514 + "optionalDependencies": {
6515 + "@pkgjs/parseargs": "^0.11.0"
6516 + }
6517 + },
6518 + "node_modules/jiti": {
6519 + "version": "1.21.7",
6520 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
6521 + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
6522 + "dev": true,
6523 + "license": "MIT",
6524 + "bin": {
6525 + "jiti": "bin/jiti.js"
6526 + }
6527 + },
6528 + "node_modules/js-tokens": {
6529 + "version": "4.0.0",
6530 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
6531 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
6532 + "license": "MIT"
6533 + },
6534 + "node_modules/js-yaml": {
6535 + "version": "4.1.1",
6536 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
6537 + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
6538 + "dev": true,
6539 + "license": "MIT",
6540 + "dependencies": {
6541 + "argparse": "^2.0.1"
6542 + },
6543 + "bin": {
6544 + "js-yaml": "bin/js-yaml.js"
6545 + }
6546 + },
6547 + "node_modules/json-buffer": {
6548 + "version": "3.0.1",
6549 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
6550 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
6551 + "dev": true,
6552 + "license": "MIT"
6553 + },
6554 + "node_modules/json-schema-to-ts": {
6555 + "version": "3.1.1",
6556 + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
6557 + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
6558 + "license": "MIT",
6559 + "dependencies": {
6560 + "@babel/runtime": "^7.18.3",
6561 + "ts-algebra": "^2.0.0"
6562 + },
6563 + "engines": {
6564 + "node": ">=16"
6565 + }
6566 + },
6567 + "node_modules/json-schema-traverse": {
6568 + "version": "0.4.1",
6569 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
6570 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
6571 + "dev": true,
6572 + "license": "MIT"
6573 + },
6574 + "node_modules/json-stable-stringify-without-jsonify": {
6575 + "version": "1.0.1",
6576 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
6577 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
6578 + "dev": true,
6579 + "license": "MIT"
6580 + },
6581 + "node_modules/json5": {
6582 + "version": "1.0.2",
6583 + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
6584 + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
6585 + "dev": true,
6586 + "license": "MIT",
6587 + "dependencies": {
6588 + "minimist": "^1.2.0"
6589 + },
6590 + "bin": {
6591 + "json5": "lib/cli.js"
6592 + }
6593 + },
6594 + "node_modules/jsx-ast-utils": {
6595 + "version": "3.3.5",
6596 + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
6597 + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
6598 + "dev": true,
6599 + "license": "MIT",
6600 + "dependencies": {
6601 + "array-includes": "^3.1.6",
6602 + "array.prototype.flat": "^1.3.1",
6603 + "object.assign": "^4.1.4",
6604 + "object.values": "^1.1.6"
6605 + },
6606 + "engines": {
6607 + "node": ">=4.0"
6608 + }
6609 + },
6610 + "node_modules/jszip": {
6611 + "version": "3.10.1",
6612 + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
6613 + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
6614 + "license": "(MIT OR GPL-3.0-or-later)",
6615 + "dependencies": {
6616 + "lie": "~3.3.0",
6617 + "pako": "~1.0.2",
6618 + "readable-stream": "~2.3.6",
6619 + "setimmediate": "^1.0.5"
6620 + }
6621 + },
6622 + "node_modules/keyv": {
6623 + "version": "4.5.4",
6624 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
6625 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
6626 + "dev": true,
6627 + "license": "MIT",
6628 + "dependencies": {
6629 + "json-buffer": "3.0.1"
6630 + }
6631 + },
6632 + "node_modules/language-subtag-registry": {
6633 + "version": "0.3.23",
6634 + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
6635 + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
6636 + "dev": true,
6637 + "license": "CC0-1.0"
6638 + },
6639 + "node_modules/language-tags": {
6640 + "version": "1.0.9",
6641 + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
6642 + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
6643 + "dev": true,
6644 + "license": "MIT",
6645 + "dependencies": {
6646 + "language-subtag-registry": "^0.3.20"
6647 + },
6648 + "engines": {
6649 + "node": ">=0.10"
6650 + }
6651 + },
6652 + "node_modules/lazystream": {
6653 + "version": "1.0.1",
6654 + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
6655 + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
6656 + "license": "MIT",
6657 + "dependencies": {
6658 + "readable-stream": "^2.0.5"
6659 + },
6660 + "engines": {
6661 + "node": ">= 0.6.3"
6662 + }
6663 + },
6664 + "node_modules/levn": {
6665 + "version": "0.4.1",
6666 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
6667 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
6668 + "dev": true,
6669 + "license": "MIT",
6670 + "dependencies": {
6671 + "prelude-ls": "^1.2.1",
6672 + "type-check": "~0.4.0"
6673 + },
6674 + "engines": {
6675 + "node": ">= 0.8.0"
6676 + }
6677 + },
6678 + "node_modules/lie": {
6679 + "version": "3.3.0",
6680 + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
6681 + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
6682 + "license": "MIT",
6683 + "dependencies": {
6684 + "immediate": "~3.0.5"
6685 + }
6686 + },
6687 + "node_modules/lilconfig": {
6688 + "version": "3.1.3",
6689 + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
6690 + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
6691 + "dev": true,
6692 + "license": "MIT",
6693 + "engines": {
6694 + "node": ">=14"
6695 + },
6696 + "funding": {
6697 + "url": "https://github.com/sponsors/antonk52"
6698 + }
6699 + },
6700 + "node_modules/lines-and-columns": {
6701 + "version": "1.2.4",
6702 + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
6703 + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
6704 + "dev": true,
6705 + "license": "MIT"
6706 + },
6707 + "node_modules/locate-path": {
6708 + "version": "6.0.0",
6709 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
6710 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
6711 + "dev": true,
6712 + "license": "MIT",
6713 + "dependencies": {
6714 + "p-locate": "^5.0.0"
6715 + },
6716 + "engines": {
6717 + "node": ">=10"
6718 + },
6719 + "funding": {
6720 + "url": "https://github.com/sponsors/sindresorhus"
6721 + }
6722 + },
6723 + "node_modules/lodash": {
6724 + "version": "4.17.23",
6725 + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
6726 + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
6727 + "license": "MIT"
6728 + },
6729 + "node_modules/lodash.merge": {
6730 + "version": "4.6.2",
6731 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
6732 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
6733 + "dev": true,
6734 + "license": "MIT"
6735 + },
6736 + "node_modules/longest-streak": {
6737 + "version": "3.1.0",
6738 + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
6739 + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
6740 + "license": "MIT",
6741 + "funding": {
6742 + "type": "github",
6743 + "url": "https://github.com/sponsors/wooorm"
6744 + }
6745 + },
6746 + "node_modules/loose-envify": {
6747 + "version": "1.4.0",
6748 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
6749 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
6750 + "license": "MIT",
6751 + "dependencies": {
6752 + "js-tokens": "^3.0.0 || ^4.0.0"
6753 + },
6754 + "bin": {
6755 + "loose-envify": "cli.js"
6756 + }
6757 + },
6758 + "node_modules/lowlight": {
6759 + "version": "1.20.0",
6760 + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
6761 + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
6762 + "license": "MIT",
6763 + "dependencies": {
6764 + "fault": "^1.0.0",
6765 + "highlight.js": "~10.7.0"
6766 + },
6767 + "funding": {
6768 + "type": "github",
6769 + "url": "https://github.com/sponsors/wooorm"
6770 + }
6771 + },
6772 + "node_modules/lru-cache": {
6773 + "version": "10.4.3",
6774 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
6775 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
6776 + "license": "ISC"
6777 + },
6778 + "node_modules/lucide-react": {
6779 + "version": "0.564.0",
6780 + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.564.0.tgz",
6781 + "integrity": "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==",
6782 + "license": "ISC",
6783 + "peerDependencies": {
6784 + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
6785 + }
6786 + },
6787 + "node_modules/math-intrinsics": {
6788 + "version": "1.1.0",
6789 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
6790 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
6791 + "dev": true,
6792 + "license": "MIT",
6793 + "engines": {
6794 + "node": ">= 0.4"
6795 + }
6796 + },
6797 + "node_modules/mdast-util-from-markdown": {
6798 + "version": "2.0.2",
6799 + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
6800 + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
6801 + "license": "MIT",
6802 + "dependencies": {
6803 + "@types/mdast": "^4.0.0",
6804 + "@types/unist": "^3.0.0",
6805 + "decode-named-character-reference": "^1.0.0",
6806 + "devlop": "^1.0.0",
6807 + "mdast-util-to-string": "^4.0.0",
6808 + "micromark": "^4.0.0",
6809 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
6810 + "micromark-util-decode-string": "^2.0.0",
6811 + "micromark-util-normalize-identifier": "^2.0.0",
6812 + "micromark-util-symbol": "^2.0.0",
6813 + "micromark-util-types": "^2.0.0",
6814 + "unist-util-stringify-position": "^4.0.0"
6815 + },
6816 + "funding": {
6817 + "type": "opencollective",
6818 + "url": "https://opencollective.com/unified"
6819 + }
6820 + },
6821 + "node_modules/mdast-util-mdx-expression": {
6822 + "version": "2.0.1",
6823 + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
6824 + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
6825 + "license": "MIT",
6826 + "dependencies": {
6827 + "@types/estree-jsx": "^1.0.0",
6828 + "@types/hast": "^3.0.0",
6829 + "@types/mdast": "^4.0.0",
6830 + "devlop": "^1.0.0",
6831 + "mdast-util-from-markdown": "^2.0.0",
6832 + "mdast-util-to-markdown": "^2.0.0"
6833 + },
6834 + "funding": {
6835 + "type": "opencollective",
6836 + "url": "https://opencollective.com/unified"
6837 + }
6838 + },
6839 + "node_modules/mdast-util-mdx-jsx": {
6840 + "version": "3.2.0",
6841 + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
6842 + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
6843 + "license": "MIT",
6844 + "dependencies": {
6845 + "@types/estree-jsx": "^1.0.0",
6846 + "@types/hast": "^3.0.0",
6847 + "@types/mdast": "^4.0.0",
6848 + "@types/unist": "^3.0.0",
6849 + "ccount": "^2.0.0",
6850 + "devlop": "^1.1.0",
6851 + "mdast-util-from-markdown": "^2.0.0",
6852 + "mdast-util-to-markdown": "^2.0.0",
6853 + "parse-entities": "^4.0.0",
6854 + "stringify-entities": "^4.0.0",
6855 + "unist-util-stringify-position": "^4.0.0",
6856 + "vfile-message": "^4.0.0"
6857 + },
6858 + "funding": {
6859 + "type": "opencollective",
6860 + "url": "https://opencollective.com/unified"
6861 + }
6862 + },
6863 + "node_modules/mdast-util-mdxjs-esm": {
6864 + "version": "2.0.1",
6865 + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
6866 + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
6867 + "license": "MIT",
6868 + "dependencies": {
6869 + "@types/estree-jsx": "^1.0.0",
6870 + "@types/hast": "^3.0.0",
6871 + "@types/mdast": "^4.0.0",
6872 + "devlop": "^1.0.0",
6873 + "mdast-util-from-markdown": "^2.0.0",
6874 + "mdast-util-to-markdown": "^2.0.0"
6875 + },
6876 + "funding": {
6877 + "type": "opencollective",
6878 + "url": "https://opencollective.com/unified"
6879 + }
6880 + },
6881 + "node_modules/mdast-util-phrasing": {
6882 + "version": "4.1.0",
6883 + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
6884 + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
6885 + "license": "MIT",
6886 + "dependencies": {
6887 + "@types/mdast": "^4.0.0",
6888 + "unist-util-is": "^6.0.0"
6889 + },
6890 + "funding": {
6891 + "type": "opencollective",
6892 + "url": "https://opencollective.com/unified"
6893 + }
6894 + },
6895 + "node_modules/mdast-util-to-hast": {
6896 + "version": "13.2.1",
6897 + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
6898 + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
6899 + "license": "MIT",
6900 + "dependencies": {
6901 + "@types/hast": "^3.0.0",
6902 + "@types/mdast": "^4.0.0",
6903 + "@ungap/structured-clone": "^1.0.0",
6904 + "devlop": "^1.0.0",
6905 + "micromark-util-sanitize-uri": "^2.0.0",
6906 + "trim-lines": "^3.0.0",
6907 + "unist-util-position": "^5.0.0",
6908 + "unist-util-visit": "^5.0.0",
6909 + "vfile": "^6.0.0"
6910 + },
6911 + "funding": {
6912 + "type": "opencollective",
6913 + "url": "https://opencollective.com/unified"
6914 + }
6915 + },
6916 + "node_modules/mdast-util-to-markdown": {
6917 + "version": "2.1.2",
6918 + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
6919 + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
6920 + "license": "MIT",
6921 + "dependencies": {
6922 + "@types/mdast": "^4.0.0",
6923 + "@types/unist": "^3.0.0",
6924 + "longest-streak": "^3.0.0",
6925 + "mdast-util-phrasing": "^4.0.0",
6926 + "mdast-util-to-string": "^4.0.0",
6927 + "micromark-util-classify-character": "^2.0.0",
6928 + "micromark-util-decode-string": "^2.0.0",
6929 + "unist-util-visit": "^5.0.0",
6930 + "zwitch": "^2.0.0"
6931 + },
6932 + "funding": {
6933 + "type": "opencollective",
6934 + "url": "https://opencollective.com/unified"
6935 + }
6936 + },
6937 + "node_modules/mdast-util-to-string": {
6938 + "version": "4.0.0",
6939 + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
6940 + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
6941 + "license": "MIT",
6942 + "dependencies": {
6943 + "@types/mdast": "^4.0.0"
6944 + },
6945 + "funding": {
6946 + "type": "opencollective",
6947 + "url": "https://opencollective.com/unified"
6948 + }
6949 + },
6950 + "node_modules/merge2": {
6951 + "version": "1.4.1",
6952 + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
6953 + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
6954 + "dev": true,
6955 + "license": "MIT",
6956 + "engines": {
6957 + "node": ">= 8"
6958 + }
6959 + },
6960 + "node_modules/micromark": {
6961 + "version": "4.0.2",
6962 + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
6963 + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
6964 + "funding": [
6965 + {
6966 + "type": "GitHub Sponsors",
6967 + "url": "https://github.com/sponsors/unifiedjs"
6968 + },
6969 + {
6970 + "type": "OpenCollective",
6971 + "url": "https://opencollective.com/unified"
6972 + }
6973 + ],
6974 + "license": "MIT",
6975 + "dependencies": {
6976 + "@types/debug": "^4.0.0",
6977 + "debug": "^4.0.0",
6978 + "decode-named-character-reference": "^1.0.0",
6979 + "devlop": "^1.0.0",
6980 + "micromark-core-commonmark": "^2.0.0",
6981 + "micromark-factory-space": "^2.0.0",
6982 + "micromark-util-character": "^2.0.0",
6983 + "micromark-util-chunked": "^2.0.0",
6984 + "micromark-util-combine-extensions": "^2.0.0",
6985 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
6986 + "micromark-util-encode": "^2.0.0",
6987 + "micromark-util-normalize-identifier": "^2.0.0",
6988 + "micromark-util-resolve-all": "^2.0.0",
6989 + "micromark-util-sanitize-uri": "^2.0.0",
6990 + "micromark-util-subtokenize": "^2.0.0",
6991 + "micromark-util-symbol": "^2.0.0",
6992 + "micromark-util-types": "^2.0.0"
6993 + }
6994 + },
6995 + "node_modules/micromark-core-commonmark": {
6996 + "version": "2.0.3",
6997 + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
6998 + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
6999 + "funding": [
7000 + {
7001 + "type": "GitHub Sponsors",
7002 + "url": "https://github.com/sponsors/unifiedjs"
7003 + },
7004 + {
7005 + "type": "OpenCollective",
7006 + "url": "https://opencollective.com/unified"
7007 + }
7008 + ],
7009 + "license": "MIT",
7010 + "dependencies": {
7011 + "decode-named-character-reference": "^1.0.0",
7012 + "devlop": "^1.0.0",
7013 + "micromark-factory-destination": "^2.0.0",
7014 + "micromark-factory-label": "^2.0.0",
7015 + "micromark-factory-space": "^2.0.0",
7016 + "micromark-factory-title": "^2.0.0",
7017 + "micromark-factory-whitespace": "^2.0.0",
7018 + "micromark-util-character": "^2.0.0",
7019 + "micromark-util-chunked": "^2.0.0",
7020 + "micromark-util-classify-character": "^2.0.0",
7021 + "micromark-util-html-tag-name": "^2.0.0",
7022 + "micromark-util-normalize-identifier": "^2.0.0",
7023 + "micromark-util-resolve-all": "^2.0.0",
7024 + "micromark-util-subtokenize": "^2.0.0",
7025 + "micromark-util-symbol": "^2.0.0",
7026 + "micromark-util-types": "^2.0.0"
7027 + }
7028 + },
7029 + "node_modules/micromark-factory-destination": {
7030 + "version": "2.0.1",
7031 + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
7032 + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
7033 + "funding": [
7034 + {
7035 + "type": "GitHub Sponsors",
7036 + "url": "https://github.com/sponsors/unifiedjs"
7037 + },
7038 + {
7039 + "type": "OpenCollective",
7040 + "url": "https://opencollective.com/unified"
7041 + }
7042 + ],
7043 + "license": "MIT",
7044 + "dependencies": {
7045 + "micromark-util-character": "^2.0.0",
7046 + "micromark-util-symbol": "^2.0.0",
7047 + "micromark-util-types": "^2.0.0"
7048 + }
7049 + },
7050 + "node_modules/micromark-factory-label": {
7051 + "version": "2.0.1",
7052 + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
7053 + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
7054 + "funding": [
7055 + {
7056 + "type": "GitHub Sponsors",
7057 + "url": "https://github.com/sponsors/unifiedjs"
7058 + },
7059 + {
7060 + "type": "OpenCollective",
7061 + "url": "https://opencollective.com/unified"
7062 + }
7063 + ],
7064 + "license": "MIT",
7065 + "dependencies": {
7066 + "devlop": "^1.0.0",
7067 + "micromark-util-character": "^2.0.0",
7068 + "micromark-util-symbol": "^2.0.0",
7069 + "micromark-util-types": "^2.0.0"
7070 + }
7071 + },
7072 + "node_modules/micromark-factory-space": {
7073 + "version": "2.0.1",
7074 + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
7075 + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
7076 + "funding": [
7077 + {
7078 + "type": "GitHub Sponsors",
7079 + "url": "https://github.com/sponsors/unifiedjs"
7080 + },
7081 + {
7082 + "type": "OpenCollective",
7083 + "url": "https://opencollective.com/unified"
7084 + }
7085 + ],
7086 + "license": "MIT",
7087 + "dependencies": {
7088 + "micromark-util-character": "^2.0.0",
7089 + "micromark-util-types": "^2.0.0"
7090 + }
7091 + },
7092 + "node_modules/micromark-factory-title": {
7093 + "version": "2.0.1",
7094 + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
7095 + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
7096 + "funding": [
7097 + {
7098 + "type": "GitHub Sponsors",
7099 + "url": "https://github.com/sponsors/unifiedjs"
7100 + },
7101 + {
7102 + "type": "OpenCollective",
7103 + "url": "https://opencollective.com/unified"
7104 + }
7105 + ],
7106 + "license": "MIT",
7107 + "dependencies": {
7108 + "micromark-factory-space": "^2.0.0",
7109 + "micromark-util-character": "^2.0.0",
7110 + "micromark-util-symbol": "^2.0.0",
7111 + "micromark-util-types": "^2.0.0"
7112 + }
7113 + },
7114 + "node_modules/micromark-factory-whitespace": {
7115 + "version": "2.0.1",
7116 + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
7117 + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
7118 + "funding": [
7119 + {
7120 + "type": "GitHub Sponsors",
7121 + "url": "https://github.com/sponsors/unifiedjs"
7122 + },
7123 + {
7124 + "type": "OpenCollective",
7125 + "url": "https://opencollective.com/unified"
7126 + }
7127 + ],
7128 + "license": "MIT",
7129 + "dependencies": {
7130 + "micromark-factory-space": "^2.0.0",
7131 + "micromark-util-character": "^2.0.0",
7132 + "micromark-util-symbol": "^2.0.0",
7133 + "micromark-util-types": "^2.0.0"
7134 + }
7135 + },
7136 + "node_modules/micromark-util-character": {
7137 + "version": "2.1.1",
7138 + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
7139 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
7140 + "funding": [
7141 + {
7142 + "type": "GitHub Sponsors",
7143 + "url": "https://github.com/sponsors/unifiedjs"
7144 + },
7145 + {
7146 + "type": "OpenCollective",
7147 + "url": "https://opencollective.com/unified"
7148 + }
7149 + ],
7150 + "license": "MIT",
7151 + "dependencies": {
7152 + "micromark-util-symbol": "^2.0.0",
7153 + "micromark-util-types": "^2.0.0"
7154 + }
7155 + },
7156 + "node_modules/micromark-util-chunked": {
7157 + "version": "2.0.1",
7158 + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
7159 + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
7160 + "funding": [
7161 + {
7162 + "type": "GitHub Sponsors",
7163 + "url": "https://github.com/sponsors/unifiedjs"
7164 + },
7165 + {
7166 + "type": "OpenCollective",
7167 + "url": "https://opencollective.com/unified"
7168 + }
7169 + ],
7170 + "license": "MIT",
7171 + "dependencies": {
7172 + "micromark-util-symbol": "^2.0.0"
7173 + }
7174 + },
7175 + "node_modules/micromark-util-classify-character": {
7176 + "version": "2.0.1",
7177 + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
7178 + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
7179 + "funding": [
7180 + {
7181 + "type": "GitHub Sponsors",
7182 + "url": "https://github.com/sponsors/unifiedjs"
7183 + },
7184 + {
7185 + "type": "OpenCollective",
7186 + "url": "https://opencollective.com/unified"
7187 + }
7188 + ],
7189 + "license": "MIT",
7190 + "dependencies": {
7191 + "micromark-util-character": "^2.0.0",
7192 + "micromark-util-symbol": "^2.0.0",
7193 + "micromark-util-types": "^2.0.0"
7194 + }
7195 + },
7196 + "node_modules/micromark-util-combine-extensions": {
7197 + "version": "2.0.1",
7198 + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
7199 + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
7200 + "funding": [
7201 + {
7202 + "type": "GitHub Sponsors",
7203 + "url": "https://github.com/sponsors/unifiedjs"
7204 + },
7205 + {
7206 + "type": "OpenCollective",
7207 + "url": "https://opencollective.com/unified"
7208 + }
7209 + ],
7210 + "license": "MIT",
7211 + "dependencies": {
7212 + "micromark-util-chunked": "^2.0.0",
7213 + "micromark-util-types": "^2.0.0"
7214 + }
7215 + },
7216 + "node_modules/micromark-util-decode-numeric-character-reference": {
7217 + "version": "2.0.2",
7218 + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
7219 + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
7220 + "funding": [
7221 + {
7222 + "type": "GitHub Sponsors",
7223 + "url": "https://github.com/sponsors/unifiedjs"
7224 + },
7225 + {
7226 + "type": "OpenCollective",
7227 + "url": "https://opencollective.com/unified"
7228 + }
7229 + ],
7230 + "license": "MIT",
7231 + "dependencies": {
7232 + "micromark-util-symbol": "^2.0.0"
7233 + }
7234 + },
7235 + "node_modules/micromark-util-decode-string": {
7236 + "version": "2.0.1",
7237 + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
7238 + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
7239 + "funding": [
7240 + {
7241 + "type": "GitHub Sponsors",
7242 + "url": "https://github.com/sponsors/unifiedjs"
7243 + },
7244 + {
7245 + "type": "OpenCollective",
7246 + "url": "https://opencollective.com/unified"
7247 + }
7248 + ],
7249 + "license": "MIT",
7250 + "dependencies": {
7251 + "decode-named-character-reference": "^1.0.0",
7252 + "micromark-util-character": "^2.0.0",
7253 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
7254 + "micromark-util-symbol": "^2.0.0"
7255 + }
7256 + },
7257 + "node_modules/micromark-util-encode": {
7258 + "version": "2.0.1",
7259 + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
7260 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
7261 + "funding": [
7262 + {
7263 + "type": "GitHub Sponsors",
7264 + "url": "https://github.com/sponsors/unifiedjs"
7265 + },
7266 + {
7267 + "type": "OpenCollective",
7268 + "url": "https://opencollective.com/unified"
7269 + }
7270 + ],
7271 + "license": "MIT"
7272 + },
7273 + "node_modules/micromark-util-html-tag-name": {
7274 + "version": "2.0.1",
7275 + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
7276 + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
7277 + "funding": [
7278 + {
7279 + "type": "GitHub Sponsors",
7280 + "url": "https://github.com/sponsors/unifiedjs"
7281 + },
7282 + {
7283 + "type": "OpenCollective",
7284 + "url": "https://opencollective.com/unified"
7285 + }
7286 + ],
7287 + "license": "MIT"
7288 + },
7289 + "node_modules/micromark-util-normalize-identifier": {
7290 + "version": "2.0.1",
7291 + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
7292 + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
7293 + "funding": [
7294 + {
7295 + "type": "GitHub Sponsors",
7296 + "url": "https://github.com/sponsors/unifiedjs"
7297 + },
7298 + {
7299 + "type": "OpenCollective",
7300 + "url": "https://opencollective.com/unified"
7301 + }
7302 + ],
7303 + "license": "MIT",
7304 + "dependencies": {
7305 + "micromark-util-symbol": "^2.0.0"
7306 + }
7307 + },
7308 + "node_modules/micromark-util-resolve-all": {
7309 + "version": "2.0.1",
7310 + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
7311 + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
7312 + "funding": [
7313 + {
7314 + "type": "GitHub Sponsors",
7315 + "url": "https://github.com/sponsors/unifiedjs"
7316 + },
7317 + {
7318 + "type": "OpenCollective",
7319 + "url": "https://opencollective.com/unified"
7320 + }
7321 + ],
7322 + "license": "MIT",
7323 + "dependencies": {
7324 + "micromark-util-types": "^2.0.0"
7325 + }
7326 + },
7327 + "node_modules/micromark-util-sanitize-uri": {
7328 + "version": "2.0.1",
7329 + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
7330 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
7331 + "funding": [
7332 + {
7333 + "type": "GitHub Sponsors",
7334 + "url": "https://github.com/sponsors/unifiedjs"
7335 + },
7336 + {
7337 + "type": "OpenCollective",
7338 + "url": "https://opencollective.com/unified"
7339 + }
7340 + ],
7341 + "license": "MIT",
7342 + "dependencies": {
7343 + "micromark-util-character": "^2.0.0",
7344 + "micromark-util-encode": "^2.0.0",
7345 + "micromark-util-symbol": "^2.0.0"
7346 + }
7347 + },
7348 + "node_modules/micromark-util-subtokenize": {
7349 + "version": "2.1.0",
7350 + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
7351 + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
7352 + "funding": [
7353 + {
7354 + "type": "GitHub Sponsors",
7355 + "url": "https://github.com/sponsors/unifiedjs"
7356 + },
7357 + {
7358 + "type": "OpenCollective",
7359 + "url": "https://opencollective.com/unified"
7360 + }
7361 + ],
7362 + "license": "MIT",
7363 + "dependencies": {
7364 + "devlop": "^1.0.0",
7365 + "micromark-util-chunked": "^2.0.0",
7366 + "micromark-util-symbol": "^2.0.0",
7367 + "micromark-util-types": "^2.0.0"
7368 + }
7369 + },
7370 + "node_modules/micromark-util-symbol": {
7371 + "version": "2.0.1",
7372 + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
7373 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
7374 + "funding": [
7375 + {
7376 + "type": "GitHub Sponsors",
7377 + "url": "https://github.com/sponsors/unifiedjs"
7378 + },
7379 + {
7380 + "type": "OpenCollective",
7381 + "url": "https://opencollective.com/unified"
7382 + }
7383 + ],
7384 + "license": "MIT"
7385 + },
7386 + "node_modules/micromark-util-types": {
7387 + "version": "2.0.2",
7388 + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
7389 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
7390 + "funding": [
7391 + {
7392 + "type": "GitHub Sponsors",
7393 + "url": "https://github.com/sponsors/unifiedjs"
7394 + },
7395 + {
7396 + "type": "OpenCollective",
7397 + "url": "https://opencollective.com/unified"
7398 + }
7399 + ],
7400 + "license": "MIT"
7401 + },
7402 + "node_modules/micromatch": {
7403 + "version": "4.0.8",
7404 + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
7405 + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
7406 + "dev": true,
7407 + "license": "MIT",
7408 + "dependencies": {
7409 + "braces": "^3.0.3",
7410 + "picomatch": "^2.3.1"
7411 + },
7412 + "engines": {
7413 + "node": ">=8.6"
7414 + }
7415 + },
7416 + "node_modules/mime-db": {
7417 + "version": "1.54.0",
7418 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
7419 + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
7420 + "license": "MIT",
7421 + "engines": {
7422 + "node": ">= 0.6"
7423 + }
7424 + },
7425 + "node_modules/mime-types": {
7426 + "version": "3.0.2",
7427 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
7428 + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
7429 + "license": "MIT",
7430 + "dependencies": {
7431 + "mime-db": "^1.54.0"
7432 + },
7433 + "engines": {
7434 + "node": ">=18"
7435 + },
7436 + "funding": {
7437 + "type": "opencollective",
7438 + "url": "https://opencollective.com/express"
7439 + }
7440 + },
7441 + "node_modules/minimatch": {
7442 + "version": "3.1.2",
7443 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
7444 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
7445 + "dev": true,
7446 + "license": "ISC",
7447 + "dependencies": {
7448 + "brace-expansion": "^1.1.7"
7449 + },
7450 + "engines": {
7451 + "node": "*"
7452 + }
7453 + },
7454 + "node_modules/minimist": {
7455 + "version": "1.2.8",
7456 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
7457 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
7458 + "dev": true,
7459 + "license": "MIT",
7460 + "funding": {
7461 + "url": "https://github.com/sponsors/ljharb"
7462 + }
7463 + },
7464 + "node_modules/minipass": {
7465 + "version": "7.1.2",
7466 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
7467 + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
7468 + "license": "ISC",
7469 + "engines": {
7470 + "node": ">=16 || 14 >=14.17"
7471 + }
7472 + },
7473 + "node_modules/motion-dom": {
7474 + "version": "12.34.0",
7475 + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz",
7476 + "integrity": "sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==",
7477 + "license": "MIT",
7478 + "dependencies": {
7479 + "motion-utils": "^12.29.2"
7480 + }
7481 + },
7482 + "node_modules/motion-utils": {
7483 + "version": "12.29.2",
7484 + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
7485 + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
7486 + "license": "MIT"
7487 + },
7488 + "node_modules/ms": {
7489 + "version": "2.1.3",
7490 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
7491 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
7492 + "license": "MIT"
7493 + },
7494 + "node_modules/mz": {
7495 + "version": "2.7.0",
7496 + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
7497 + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
7498 + "dev": true,
7499 + "license": "MIT",
7500 + "dependencies": {
7501 + "any-promise": "^1.0.0",
7502 + "object-assign": "^4.0.1",
7503 + "thenify-all": "^1.0.0"
7504 + }
7505 + },
7506 + "node_modules/nanoid": {
7507 + "version": "3.3.11",
7508 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
7509 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
7510 + "funding": [
7511 + {
7512 + "type": "github",
7513 + "url": "https://github.com/sponsors/ai"
7514 + }
7515 + ],
7516 + "license": "MIT",
7517 + "bin": {
7518 + "nanoid": "bin/nanoid.cjs"
7519 + },
7520 + "engines": {
7521 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
7522 + }
7523 + },
7524 + "node_modules/napi-postinstall": {
7525 + "version": "0.3.4",
7526 + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
7527 + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
7528 + "dev": true,
7529 + "license": "MIT",
7530 + "bin": {
7531 + "napi-postinstall": "lib/cli.js"
7532 + },
7533 + "engines": {
7534 + "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
7535 + },
7536 + "funding": {
7537 + "url": "https://opencollective.com/napi-postinstall"
7538 + }
7539 + },
7540 + "node_modules/natural-compare": {
7541 + "version": "1.4.0",
7542 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
7543 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
7544 + "dev": true,
7545 + "license": "MIT"
7546 + },
7547 + "node_modules/next": {
7548 + "version": "14.2.35",
7549 + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
7550 + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
7551 + "license": "MIT",
7552 + "dependencies": {
7553 + "@next/env": "14.2.35",
7554 + "@swc/helpers": "0.5.5",
7555 + "busboy": "1.6.0",
7556 + "caniuse-lite": "^1.0.30001579",
7557 + "graceful-fs": "^4.2.11",
7558 + "postcss": "8.4.31",
7559 + "styled-jsx": "5.1.1"
7560 + },
7561 + "bin": {
7562 + "next": "dist/bin/next"
7563 + },
7564 + "engines": {
7565 + "node": ">=18.17.0"
7566 + },
7567 + "optionalDependencies": {
7568 + "@next/swc-darwin-arm64": "14.2.33",
7569 + "@next/swc-darwin-x64": "14.2.33",
7570 + "@next/swc-linux-arm64-gnu": "14.2.33",
7571 + "@next/swc-linux-arm64-musl": "14.2.33",
7572 + "@next/swc-linux-x64-gnu": "14.2.33",
7573 + "@next/swc-linux-x64-musl": "14.2.33",
7574 + "@next/swc-win32-arm64-msvc": "14.2.33",
7575 + "@next/swc-win32-ia32-msvc": "14.2.33",
7576 + "@next/swc-win32-x64-msvc": "14.2.33"
7577 + },
7578 + "peerDependencies": {
7579 + "@opentelemetry/api": "^1.1.0",
7580 + "@playwright/test": "^1.41.2",
7581 + "react": "^18.2.0",
7582 + "react-dom": "^18.2.0",
7583 + "sass": "^1.3.0"
7584 + },
7585 + "peerDependenciesMeta": {
7586 + "@opentelemetry/api": {
7587 + "optional": true
7588 + },
7589 + "@playwright/test": {
7590 + "optional": true
7591 + },
7592 + "sass": {
7593 + "optional": true
7594 + }
7595 + }
7596 + },
7597 + "node_modules/next/node_modules/postcss": {
7598 + "version": "8.4.31",
7599 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
7600 + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
7601 + "funding": [
7602 + {
7603 + "type": "opencollective",
7604 + "url": "https://opencollective.com/postcss/"
7605 + },
7606 + {
7607 + "type": "tidelift",
7608 + "url": "https://tidelift.com/funding/github/npm/postcss"
7609 + },
7610 + {
7611 + "type": "github",
7612 + "url": "https://github.com/sponsors/ai"
7613 + }
7614 + ],
7615 + "license": "MIT",
7616 + "dependencies": {
7617 + "nanoid": "^3.3.6",
7618 + "picocolors": "^1.0.0",
7619 + "source-map-js": "^1.0.2"
7620 + },
7621 + "engines": {
7622 + "node": "^10 || ^12 || >=14"
7623 + }
7624 + },
7625 + "node_modules/node-readable-to-web-readable-stream": {
7626 + "version": "0.4.2",
7627 + "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
7628 + "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==",
7629 + "license": "MIT",
7630 + "optional": true
7631 + },
7632 + "node_modules/normalize-path": {
7633 + "version": "3.0.0",
7634 + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
7635 + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
7636 + "license": "MIT",
7637 + "engines": {
7638 + "node": ">=0.10.0"
7639 + }
7640 + },
7641 + "node_modules/object-assign": {
7642 + "version": "4.1.1",
7643 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
7644 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
7645 + "license": "MIT",
7646 + "engines": {
7647 + "node": ">=0.10.0"
7648 + }
7649 + },
7650 + "node_modules/object-hash": {
7651 + "version": "3.0.0",
7652 + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
7653 + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
7654 + "dev": true,
7655 + "license": "MIT",
7656 + "engines": {
7657 + "node": ">= 6"
7658 + }
7659 + },
7660 + "node_modules/object-inspect": {
7661 + "version": "1.13.4",
7662 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
7663 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
7664 + "dev": true,
7665 + "license": "MIT",
7666 + "engines": {
7667 + "node": ">= 0.4"
7668 + },
7669 + "funding": {
7670 + "url": "https://github.com/sponsors/ljharb"
7671 + }
7672 + },
7673 + "node_modules/object-keys": {
7674 + "version": "1.1.1",
7675 + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
7676 + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
7677 + "dev": true,
7678 + "license": "MIT",
7679 + "engines": {
7680 + "node": ">= 0.4"
7681 + }
7682 + },
7683 + "node_modules/object.assign": {
7684 + "version": "4.1.7",
7685 + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
7686 + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
7687 + "dev": true,
7688 + "license": "MIT",
7689 + "dependencies": {
7690 + "call-bind": "^1.0.8",
7691 + "call-bound": "^1.0.3",
7692 + "define-properties": "^1.2.1",
7693 + "es-object-atoms": "^1.0.0",
7694 + "has-symbols": "^1.1.0",
7695 + "object-keys": "^1.1.1"
7696 + },
7697 + "engines": {
7698 + "node": ">= 0.4"
7699 + },
7700 + "funding": {
7701 + "url": "https://github.com/sponsors/ljharb"
7702 + }
7703 + },
7704 + "node_modules/object.entries": {
7705 + "version": "1.1.9",
7706 + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
7707 + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
7708 + "dev": true,
7709 + "license": "MIT",
7710 + "dependencies": {
7711 + "call-bind": "^1.0.8",
7712 + "call-bound": "^1.0.4",
7713 + "define-properties": "^1.2.1",
7714 + "es-object-atoms": "^1.1.1"
7715 + },
7716 + "engines": {
7717 + "node": ">= 0.4"
7718 + }
7719 + },
7720 + "node_modules/object.fromentries": {
7721 + "version": "2.0.8",
7722 + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
7723 + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
7724 + "dev": true,
7725 + "license": "MIT",
7726 + "dependencies": {
7727 + "call-bind": "^1.0.7",
7728 + "define-properties": "^1.2.1",
7729 + "es-abstract": "^1.23.2",
7730 + "es-object-atoms": "^1.0.0"
7731 + },
7732 + "engines": {
7733 + "node": ">= 0.4"
7734 + },
7735 + "funding": {
7736 + "url": "https://github.com/sponsors/ljharb"
7737 + }
7738 + },
7739 + "node_modules/object.groupby": {
7740 + "version": "1.0.3",
7741 + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
7742 + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
7743 + "dev": true,
7744 + "license": "MIT",
7745 + "dependencies": {
7746 + "call-bind": "^1.0.7",
7747 + "define-properties": "^1.2.1",
7748 + "es-abstract": "^1.23.2"
7749 + },
7750 + "engines": {
7751 + "node": ">= 0.4"
7752 + }
7753 + },
7754 + "node_modules/object.values": {
7755 + "version": "1.2.1",
7756 + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
7757 + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
7758 + "dev": true,
7759 + "license": "MIT",
7760 + "dependencies": {
7761 + "call-bind": "^1.0.8",
7762 + "call-bound": "^1.0.3",
7763 + "define-properties": "^1.2.1",
7764 + "es-object-atoms": "^1.0.0"
7765 + },
7766 + "engines": {
7767 + "node": ">= 0.4"
7768 + },
7769 + "funding": {
7770 + "url": "https://github.com/sponsors/ljharb"
7771 + }
7772 + },
7773 + "node_modules/once": {
7774 + "version": "1.4.0",
7775 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
7776 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
7777 + "dev": true,
7778 + "license": "ISC",
7779 + "dependencies": {
7780 + "wrappy": "1"
7781 + }
7782 + },
7783 + "node_modules/optionator": {
7784 + "version": "0.9.4",
7785 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
7786 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
7787 + "dev": true,
7788 + "license": "MIT",
7789 + "dependencies": {
7790 + "deep-is": "^0.1.3",
7791 + "fast-levenshtein": "^2.0.6",
7792 + "levn": "^0.4.1",
7793 + "prelude-ls": "^1.2.1",
7794 + "type-check": "^0.4.0",
7795 + "word-wrap": "^1.2.5"
7796 + },
7797 + "engines": {
7798 + "node": ">= 0.8.0"
7799 + }
7800 + },
7801 + "node_modules/otplib": {
7802 + "version": "13.3.0",
7803 + "resolved": "https://registry.npmjs.org/otplib/-/otplib-13.3.0.tgz",
7804 + "integrity": "sha512-VYMKyyDG8yt2q+z58sz54/EIyTh7+tyMrjeemR44iVh5+dkKtIs57irTqxjH+IkAL1uMmG1JIFhG5CxTpqdU5g==",
7805 + "license": "MIT",
7806 + "dependencies": {
7807 + "@otplib/core": "13.3.0",
7808 + "@otplib/hotp": "13.3.0",
7809 + "@otplib/plugin-base32-scure": "13.3.0",
7810 + "@otplib/plugin-crypto-noble": "13.3.0",
7811 + "@otplib/totp": "13.3.0",
7812 + "@otplib/uri": "13.3.0"
7813 + }
7814 + },
7815 + "node_modules/own-keys": {
7816 + "version": "1.0.1",
7817 + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
7818 + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
7819 + "dev": true,
7820 + "license": "MIT",
7821 + "dependencies": {
7822 + "get-intrinsic": "^1.2.6",
7823 + "object-keys": "^1.1.1",
7824 + "safe-push-apply": "^1.0.0"
7825 + },
7826 + "engines": {
7827 + "node": ">= 0.4"
7828 + },
7829 + "funding": {
7830 + "url": "https://github.com/sponsors/ljharb"
7831 + }
7832 + },
7833 + "node_modules/p-limit": {
7834 + "version": "3.1.0",
7835 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
7836 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
7837 + "dev": true,
7838 + "license": "MIT",
7839 + "dependencies": {
7840 + "yocto-queue": "^0.1.0"
7841 + },
7842 + "engines": {
7843 + "node": ">=10"
7844 + },
7845 + "funding": {
7846 + "url": "https://github.com/sponsors/sindresorhus"
7847 + }
7848 + },
7849 + "node_modules/p-locate": {
7850 + "version": "5.0.0",
7851 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
7852 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
7853 + "dev": true,
7854 + "license": "MIT",
7855 + "dependencies": {
7856 + "p-limit": "^3.0.2"
7857 + },
7858 + "engines": {
7859 + "node": ">=10"
7860 + },
7861 + "funding": {
7862 + "url": "https://github.com/sponsors/sindresorhus"
7863 + }
7864 + },
7865 + "node_modules/p-try": {
7866 + "version": "2.2.0",
7867 + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
7868 + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
7869 + "license": "MIT",
7870 + "engines": {
7871 + "node": ">=6"
7872 + }
7873 + },
7874 + "node_modules/pako": {
7875 + "version": "1.0.11",
7876 + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
7877 + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
7878 + "license": "(MIT AND Zlib)"
7879 + },
7880 + "node_modules/papaparse": {
7881 + "version": "5.5.3",
7882 + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
7883 + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
7884 + "license": "MIT"
7885 + },
7886 + "node_modules/parent-module": {
7887 + "version": "1.0.1",
7888 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
7889 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
7890 + "dev": true,
7891 + "license": "MIT",
7892 + "dependencies": {
7893 + "callsites": "^3.0.0"
7894 + },
7895 + "engines": {
7896 + "node": ">=6"
7897 + }
7898 + },
7899 + "node_modules/parse-entities": {
7900 + "version": "4.0.2",
7901 + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
7902 + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
7903 + "license": "MIT",
7904 + "dependencies": {
7905 + "@types/unist": "^2.0.0",
7906 + "character-entities-legacy": "^3.0.0",
7907 + "character-reference-invalid": "^2.0.0",
7908 + "decode-named-character-reference": "^1.0.0",
7909 + "is-alphanumerical": "^2.0.0",
7910 + "is-decimal": "^2.0.0",
7911 + "is-hexadecimal": "^2.0.0"
7912 + },
7913 + "funding": {
7914 + "type": "github",
7915 + "url": "https://github.com/sponsors/wooorm"
7916 + }
7917 + },
7918 + "node_modules/parse-entities/node_modules/@types/unist": {
7919 + "version": "2.0.11",
7920 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
7921 + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
7922 + "license": "MIT"
7923 + },
7924 + "node_modules/path-exists": {
7925 + "version": "4.0.0",
7926 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
7927 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
7928 + "license": "MIT",
7929 + "engines": {
7930 + "node": ">=8"
7931 + }
7932 + },
7933 + "node_modules/path-is-absolute": {
7934 + "version": "1.0.1",
7935 + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
7936 + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
7937 + "dev": true,
7938 + "license": "MIT",
7939 + "engines": {
7940 + "node": ">=0.10.0"
7941 + }
7942 + },
7943 + "node_modules/path-key": {
7944 + "version": "3.1.1",
7945 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
7946 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
7947 + "license": "MIT",
7948 + "engines": {
7949 + "node": ">=8"
7950 + }
7951 + },
7952 + "node_modules/path-parse": {
7953 + "version": "1.0.7",
7954 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
7955 + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
7956 + "dev": true,
7957 + "license": "MIT"
7958 + },
7959 + "node_modules/path-scurry": {
7960 + "version": "1.11.1",
7961 + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
7962 + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
7963 + "license": "BlueOak-1.0.0",
7964 + "dependencies": {
7965 + "lru-cache": "^10.2.0",
7966 + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
7967 + },
7968 + "engines": {
7969 + "node": ">=16 || 14 >=14.18"
7970 + },
7971 + "funding": {
7972 + "url": "https://github.com/sponsors/isaacs"
7973 + }
7974 + },
7975 + "node_modules/pdf-lib": {
7976 + "version": "1.17.1",
7977 + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
7978 + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
7979 + "license": "MIT",
7980 + "dependencies": {
7981 + "@pdf-lib/standard-fonts": "^1.0.0",
7982 + "@pdf-lib/upng": "^1.0.1",
7983 + "pako": "^1.0.11",
7984 + "tslib": "^1.11.1"
7985 + }
7986 + },
7987 + "node_modules/pdf-lib/node_modules/tslib": {
7988 + "version": "1.14.1",
7989 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
7990 + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
7991 + "license": "0BSD"
7992 + },
7993 + "node_modules/pdfjs-dist": {
7994 + "version": "5.4.624",
7995 + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.624.tgz",
7996 + "integrity": "sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==",
7997 + "license": "Apache-2.0",
7998 + "engines": {
7999 + "node": ">=20.16.0 || >=22.3.0"
8000 + },
8001 + "optionalDependencies": {
8002 + "@napi-rs/canvas": "^0.1.88",
8003 + "node-readable-to-web-readable-stream": "^0.4.2"
8004 + }
8005 + },
8006 + "node_modules/picocolors": {
8007 + "version": "1.1.1",
8008 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
8009 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
8010 + "license": "ISC"
8011 + },
8012 + "node_modules/picomatch": {
8013 + "version": "2.3.1",
8014 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
8015 + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
8016 + "dev": true,
8017 + "license": "MIT",
8018 + "engines": {
8019 + "node": ">=8.6"
8020 + },
8021 + "funding": {
8022 + "url": "https://github.com/sponsors/jonschlinkert"
8023 + }
8024 + },
8025 + "node_modules/pify": {
8026 + "version": "2.3.0",
8027 + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
8028 + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
8029 + "dev": true,
8030 + "license": "MIT",
8031 + "engines": {
8032 + "node": ">=0.10.0"
8033 + }
8034 + },
8035 + "node_modules/pirates": {
8036 + "version": "4.0.7",
8037 + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
8038 + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
8039 + "dev": true,
8040 + "license": "MIT",
8041 + "engines": {
8042 + "node": ">= 6"
8043 + }
8044 + },
8045 + "node_modules/pngjs": {
8046 + "version": "5.0.0",
8047 + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
8048 + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
8049 + "license": "MIT",
8050 + "engines": {
8051 + "node": ">=10.13.0"
8052 + }
8053 + },
8054 + "node_modules/possible-typed-array-names": {
8055 + "version": "1.1.0",
8056 + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
8057 + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
8058 + "dev": true,
8059 + "license": "MIT",
8060 + "engines": {
8061 + "node": ">= 0.4"
8062 + }
8063 + },
8064 + "node_modules/postcss": {
8065 + "version": "8.5.6",
8066 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
8067 + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
8068 + "dev": true,
8069 + "funding": [
8070 + {
8071 + "type": "opencollective",
8072 + "url": "https://opencollective.com/postcss/"
8073 + },
8074 + {
8075 + "type": "tidelift",
8076 + "url": "https://tidelift.com/funding/github/npm/postcss"
8077 + },
8078 + {
8079 + "type": "github",
8080 + "url": "https://github.com/sponsors/ai"
8081 + }
8082 + ],
8083 + "license": "MIT",
8084 + "dependencies": {
8085 + "nanoid": "^3.3.11",
8086 + "picocolors": "^1.1.1",
8087 + "source-map-js": "^1.2.1"
8088 + },
8089 + "engines": {
8090 + "node": "^10 || ^12 || >=14"
8091 + }
8092 + },
8093 + "node_modules/postcss-import": {
8094 + "version": "15.1.0",
8095 + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
8096 + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
8097 + "dev": true,
8098 + "license": "MIT",
8099 + "dependencies": {
8100 + "postcss-value-parser": "^4.0.0",
8101 + "read-cache": "^1.0.0",
8102 + "resolve": "^1.1.7"
8103 + },
8104 + "engines": {
8105 + "node": ">=14.0.0"
8106 + },
8107 + "peerDependencies": {
8108 + "postcss": "^8.0.0"
8109 + }
8110 + },
8111 + "node_modules/postcss-js": {
8112 + "version": "4.1.0",
8113 + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
8114 + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
8115 + "dev": true,
8116 + "funding": [
8117 + {
8118 + "type": "opencollective",
8119 + "url": "https://opencollective.com/postcss/"
8120 + },
8121 + {
8122 + "type": "github",
8123 + "url": "https://github.com/sponsors/ai"
8124 + }
8125 + ],
8126 + "license": "MIT",
8127 + "dependencies": {
8128 + "camelcase-css": "^2.0.1"
8129 + },
8130 + "engines": {
8131 + "node": "^12 || ^14 || >= 16"
8132 + },
8133 + "peerDependencies": {
8134 + "postcss": "^8.4.21"
8135 + }
8136 + },
8137 + "node_modules/postcss-load-config": {
8138 + "version": "6.0.1",
8139 + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
8140 + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
8141 + "dev": true,
8142 + "funding": [
8143 + {
8144 + "type": "opencollective",
8145 + "url": "https://opencollective.com/postcss/"
8146 + },
8147 + {
8148 + "type": "github",
8149 + "url": "https://github.com/sponsors/ai"
8150 + }
8151 + ],
8152 + "license": "MIT",
8153 + "dependencies": {
8154 + "lilconfig": "^3.1.1"
8155 + },
8156 + "engines": {
8157 + "node": ">= 18"
8158 + },
8159 + "peerDependencies": {
8160 + "jiti": ">=1.21.0",
8161 + "postcss": ">=8.0.9",
8162 + "tsx": "^4.8.1",
8163 + "yaml": "^2.4.2"
8164 + },
8165 + "peerDependenciesMeta": {
8166 + "jiti": {
8167 + "optional": true
8168 + },
8169 + "postcss": {
8170 + "optional": true
8171 + },
8172 + "tsx": {
8173 + "optional": true
8174 + },
8175 + "yaml": {
8176 + "optional": true
8177 + }
8178 + }
8179 + },
8180 + "node_modules/postcss-nested": {
8181 + "version": "6.2.0",
8182 + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
8183 + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
8184 + "dev": true,
8185 + "funding": [
8186 + {
8187 + "type": "opencollective",
8188 + "url": "https://opencollective.com/postcss/"
8189 + },
8190 + {
8191 + "type": "github",
8192 + "url": "https://github.com/sponsors/ai"
8193 + }
8194 + ],
8195 + "license": "MIT",
8196 + "dependencies": {
8197 + "postcss-selector-parser": "^6.1.1"
8198 + },
8199 + "engines": {
8200 + "node": ">=12.0"
8201 + },
8202 + "peerDependencies": {
8203 + "postcss": "^8.2.14"
8204 + }
8205 + },
8206 + "node_modules/postcss-selector-parser": {
8207 + "version": "6.1.2",
8208 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
8209 + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
8210 + "dev": true,
8211 + "license": "MIT",
8212 + "dependencies": {
8213 + "cssesc": "^3.0.0",
8214 + "util-deprecate": "^1.0.2"
8215 + },
8216 + "engines": {
8217 + "node": ">=4"
8218 + }
8219 + },
8220 + "node_modules/postcss-value-parser": {
8221 + "version": "4.2.0",
8222 + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
8223 + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
8224 + "dev": true,
8225 + "license": "MIT"
8226 + },
8227 + "node_modules/prelude-ls": {
8228 + "version": "1.2.1",
8229 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
8230 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
8231 + "dev": true,
8232 + "license": "MIT",
8233 + "engines": {
8234 + "node": ">= 0.8.0"
8235 + }
8236 + },
8237 + "node_modules/prisma": {
8238 + "version": "5.22.0",
8239 + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
8240 + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
8241 + "hasInstallScript": true,
8242 + "license": "Apache-2.0",
8243 + "dependencies": {
8244 + "@prisma/engines": "5.22.0"
8245 + },
8246 + "bin": {
8247 + "prisma": "build/index.js"
8248 + },
8249 + "engines": {
8250 + "node": ">=16.13"
8251 + },
8252 + "optionalDependencies": {
8253 + "fsevents": "2.3.3"
8254 + }
8255 + },
8256 + "node_modules/prismjs": {
8257 + "version": "1.30.0",
8258 + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
8259 + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
8260 + "license": "MIT",
8261 + "engines": {
8262 + "node": ">=6"
8263 + }
8264 + },
8265 + "node_modules/process": {
8266 + "version": "0.11.10",
8267 + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
8268 + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
8269 + "license": "MIT",
8270 + "engines": {
8271 + "node": ">= 0.6.0"
8272 + }
8273 + },
8274 + "node_modules/process-nextick-args": {
8275 + "version": "2.0.1",
8276 + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
8277 + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
8278 + "license": "MIT"
8279 + },
8280 + "node_modules/prop-types": {
8281 + "version": "15.8.1",
8282 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
8283 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
8284 + "license": "MIT",
8285 + "dependencies": {
8286 + "loose-envify": "^1.4.0",
8287 + "object-assign": "^4.1.1",
8288 + "react-is": "^16.13.1"
8289 + }
8290 + },
8291 + "node_modules/property-information": {
8292 + "version": "7.1.0",
8293 + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
8294 + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
8295 + "license": "MIT",
8296 + "funding": {
8297 + "type": "github",
8298 + "url": "https://github.com/sponsors/wooorm"
8299 + }
8300 + },
8301 + "node_modules/punycode": {
8302 + "version": "2.3.1",
8303 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
8304 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
8305 + "dev": true,
8306 + "license": "MIT",
8307 + "engines": {
8308 + "node": ">=6"
8309 + }
8310 + },
8311 + "node_modules/qrcode": {
8312 + "version": "1.5.4",
8313 + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
8314 + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
8315 + "license": "MIT",
8316 + "dependencies": {
8317 + "dijkstrajs": "^1.0.1",
8318 + "pngjs": "^5.0.0",
8319 + "yargs": "^15.3.1"
8320 + },
8321 + "bin": {
8322 + "qrcode": "bin/qrcode"
8323 + },
8324 + "engines": {
8325 + "node": ">=10.13.0"
8326 + }
8327 + },
8328 + "node_modules/queue-microtask": {
8329 + "version": "1.2.3",
8330 + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
8331 + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
8332 + "dev": true,
8333 + "funding": [
8334 + {
8335 + "type": "github",
8336 + "url": "https://github.com/sponsors/feross"
8337 + },
8338 + {
8339 + "type": "patreon",
8340 + "url": "https://www.patreon.com/feross"
8341 + },
8342 + {
8343 + "type": "consulting",
8344 + "url": "https://feross.org/support"
8345 + }
8346 + ],
8347 + "license": "MIT"
8348 + },
8349 + "node_modules/rate-limiter-flexible": {
8350 + "version": "9.1.1",
8351 + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-9.1.1.tgz",
8352 + "integrity": "sha512-imxFjzPCmvDLMe7d2tsgiSQvs5EI2fI9SNymmslAfOqznZhsZ+PqbIjIYKpuSbd3pKovR1aMG47qfCLIO/adVg==",
8353 + "license": "ISC"
8354 + },
8355 + "node_modules/react": {
8356 + "version": "18.3.1",
8357 + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
8358 + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
8359 + "license": "MIT",
8360 + "dependencies": {
8361 + "loose-envify": "^1.1.0"
8362 + },
8363 + "engines": {
8364 + "node": ">=0.10.0"
8365 + }
8366 + },
8367 + "node_modules/react-dom": {
8368 + "version": "18.3.1",
8369 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
8370 + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
8371 + "license": "MIT",
8372 + "dependencies": {
8373 + "loose-envify": "^1.1.0",
8374 + "scheduler": "^0.23.2"
8375 + },
8376 + "peerDependencies": {
8377 + "react": "^18.3.1"
8378 + }
8379 + },
8380 + "node_modules/react-dropzone": {
8381 + "version": "15.0.0",
8382 + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-15.0.0.tgz",
8383 + "integrity": "sha512-lGjYV/EoqEjEWPnmiSvH4v5IoIAwQM2W4Z1C0Q/Pw2xD0eVzKPS359BQTUMum+1fa0kH2nrKjuavmTPOGhpLPg==",
8384 + "license": "MIT",
8385 + "dependencies": {
8386 + "attr-accept": "^2.2.4",
8387 + "file-selector": "^2.1.0",
8388 + "prop-types": "^15.8.1"
8389 + },
8390 + "engines": {
8391 + "node": ">= 10.13"
8392 + },
8393 + "peerDependencies": {
8394 + "react": ">= 16.8 || 18.0.0"
8395 + }
8396 + },
8397 + "node_modules/react-hot-toast": {
8398 + "version": "2.6.0",
8399 + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
8400 + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
8401 + "license": "MIT",
8402 + "dependencies": {
8403 + "csstype": "^3.1.3",
8404 + "goober": "^2.1.16"
8405 + },
8406 + "engines": {
8407 + "node": ">=10"
8408 + },
8409 + "peerDependencies": {
8410 + "react": ">=16",
8411 + "react-dom": ">=16"
8412 + }
8413 + },
8414 + "node_modules/react-is": {
8415 + "version": "16.13.1",
8416 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
8417 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
8418 + "license": "MIT"
8419 + },
8420 + "node_modules/react-markdown": {
8421 + "version": "10.1.0",
8422 + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
8423 + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
8424 + "license": "MIT",
8425 + "dependencies": {
8426 + "@types/hast": "^3.0.0",
8427 + "@types/mdast": "^4.0.0",
8428 + "devlop": "^1.0.0",
8429 + "hast-util-to-jsx-runtime": "^2.0.0",
8430 + "html-url-attributes": "^3.0.0",
8431 + "mdast-util-to-hast": "^13.0.0",
8432 + "remark-parse": "^11.0.0",
8433 + "remark-rehype": "^11.0.0",
8434 + "unified": "^11.0.0",
8435 + "unist-util-visit": "^5.0.0",
8436 + "vfile": "^6.0.0"
8437 + },
8438 + "funding": {
8439 + "type": "opencollective",
8440 + "url": "https://opencollective.com/unified"
8441 + },
8442 + "peerDependencies": {
8443 + "@types/react": ">=18",
8444 + "react": ">=18"
8445 + }
8446 + },
8447 + "node_modules/react-syntax-highlighter": {
8448 + "version": "16.1.0",
8449 + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz",
8450 + "integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==",
8451 + "license": "MIT",
8452 + "dependencies": {
8453 + "@babel/runtime": "^7.28.4",
8454 + "highlight.js": "^10.4.1",
8455 + "highlightjs-vue": "^1.0.0",
8456 + "lowlight": "^1.17.0",
8457 + "prismjs": "^1.30.0",
8458 + "refractor": "^5.0.0"
8459 + },
8460 + "engines": {
8461 + "node": ">= 16.20.2"
8462 + },
8463 + "peerDependencies": {
8464 + "react": ">= 0.14.0"
8465 + }
8466 + },
8467 + "node_modules/read-cache": {
8468 + "version": "1.0.0",
8469 + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
8470 + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
8471 + "dev": true,
8472 + "license": "MIT",
8473 + "dependencies": {
8474 + "pify": "^2.3.0"
8475 + }
8476 + },
8477 + "node_modules/readable-stream": {
8478 + "version": "2.3.8",
8479 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
8480 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
8481 + "license": "MIT",
8482 + "dependencies": {
8483 + "core-util-is": "~1.0.0",
8484 + "inherits": "~2.0.3",
8485 + "isarray": "~1.0.0",
8486 + "process-nextick-args": "~2.0.0",
8487 + "safe-buffer": "~5.1.1",
8488 + "string_decoder": "~1.1.1",
8489 + "util-deprecate": "~1.0.1"
8490 + }
8491 + },
8492 + "node_modules/readable-stream/node_modules/isarray": {
8493 + "version": "1.0.0",
8494 + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
8495 + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
8496 + "license": "MIT"
8497 + },
8498 + "node_modules/readdir-glob": {
8499 + "version": "1.1.3",
8500 + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
8501 + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
8502 + "license": "Apache-2.0",
8503 + "dependencies": {
8504 + "minimatch": "^5.1.0"
8505 + }
8506 + },
8507 + "node_modules/readdir-glob/node_modules/brace-expansion": {
8508 + "version": "2.0.2",
8509 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
8510 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
8511 + "license": "MIT",
8512 + "dependencies": {
8513 + "balanced-match": "^1.0.0"
8514 + }
8515 + },
8516 + "node_modules/readdir-glob/node_modules/minimatch": {
8517 + "version": "5.1.6",
8518 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
8519 + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
8520 + "license": "ISC",
8521 + "dependencies": {
8522 + "brace-expansion": "^2.0.1"
8523 + },
8524 + "engines": {
8525 + "node": ">=10"
8526 + }
8527 + },
8528 + "node_modules/readdirp": {
8529 + "version": "3.6.0",
8530 + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
8531 + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
8532 + "dev": true,
8533 + "license": "MIT",
8534 + "dependencies": {
8535 + "picomatch": "^2.2.1"
8536 + },
8537 + "engines": {
8538 + "node": ">=8.10.0"
8539 + }
8540 + },
8541 + "node_modules/reflect.getprototypeof": {
8542 + "version": "1.0.10",
8543 + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
8544 + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
8545 + "dev": true,
8546 + "license": "MIT",
8547 + "dependencies": {
8548 + "call-bind": "^1.0.8",
8549 + "define-properties": "^1.2.1",
8550 + "es-abstract": "^1.23.9",
8551 + "es-errors": "^1.3.0",
8552 + "es-object-atoms": "^1.0.0",
8553 + "get-intrinsic": "^1.2.7",
8554 + "get-proto": "^1.0.1",
8555 + "which-builtin-type": "^1.2.1"
8556 + },
8557 + "engines": {
8558 + "node": ">= 0.4"
8559 + },
8560 + "funding": {
8561 + "url": "https://github.com/sponsors/ljharb"
8562 + }
8563 + },
8564 + "node_modules/refractor": {
8565 + "version": "5.0.0",
8566 + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
8567 + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
8568 + "license": "MIT",
8569 + "dependencies": {
8570 + "@types/hast": "^3.0.0",
8571 + "@types/prismjs": "^1.0.0",
8572 + "hastscript": "^9.0.0",
8573 + "parse-entities": "^4.0.0"
8574 + },
8575 + "funding": {
8576 + "type": "github",
8577 + "url": "https://github.com/sponsors/wooorm"
8578 + }
8579 + },
8580 + "node_modules/regexp.prototype.flags": {
8581 + "version": "1.5.4",
8582 + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
8583 + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
8584 + "dev": true,
8585 + "license": "MIT",
8586 + "dependencies": {
8587 + "call-bind": "^1.0.8",
8588 + "define-properties": "^1.2.1",
8589 + "es-errors": "^1.3.0",
8590 + "get-proto": "^1.0.1",
8591 + "gopd": "^1.2.0",
8592 + "set-function-name": "^2.0.2"
8593 + },
8594 + "engines": {
8595 + "node": ">= 0.4"
8596 + },
8597 + "funding": {
8598 + "url": "https://github.com/sponsors/ljharb"
8599 + }
8600 + },
8601 + "node_modules/remark-parse": {
8602 + "version": "11.0.0",
8603 + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
8604 + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
8605 + "license": "MIT",
8606 + "dependencies": {
8607 + "@types/mdast": "^4.0.0",
8608 + "mdast-util-from-markdown": "^2.0.0",
8609 + "micromark-util-types": "^2.0.0",
8610 + "unified": "^11.0.0"
8611 + },
8612 + "funding": {
8613 + "type": "opencollective",
8614 + "url": "https://opencollective.com/unified"
8615 + }
8616 + },
8617 + "node_modules/remark-rehype": {
8618 + "version": "11.1.2",
8619 + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
8620 + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
8621 + "license": "MIT",
8622 + "dependencies": {
8623 + "@types/hast": "^3.0.0",
8624 + "@types/mdast": "^4.0.0",
8625 + "mdast-util-to-hast": "^13.0.0",
8626 + "unified": "^11.0.0",
8627 + "vfile": "^6.0.0"
8628 + },
8629 + "funding": {
8630 + "type": "opencollective",
8631 + "url": "https://opencollective.com/unified"
8632 + }
8633 + },
8634 + "node_modules/require-directory": {
8635 + "version": "2.1.1",
8636 + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
8637 + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
8638 + "license": "MIT",
8639 + "engines": {
8640 + "node": ">=0.10.0"
8641 + }
8642 + },
8643 + "node_modules/require-main-filename": {
8644 + "version": "2.0.0",
8645 + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
8646 + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
8647 + "license": "ISC"
8648 + },
8649 + "node_modules/resolve": {
8650 + "version": "1.22.11",
8651 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
8652 + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
8653 + "dev": true,
8654 + "license": "MIT",
8655 + "dependencies": {
8656 + "is-core-module": "^2.16.1",
8657 + "path-parse": "^1.0.7",
8658 + "supports-preserve-symlinks-flag": "^1.0.0"
8659 + },
8660 + "bin": {
8661 + "resolve": "bin/resolve"
8662 + },
8663 + "engines": {
8664 + "node": ">= 0.4"
8665 + },
8666 + "funding": {
8667 + "url": "https://github.com/sponsors/ljharb"
8668 + }
8669 + },
8670 + "node_modules/resolve-from": {
8671 + "version": "4.0.0",
8672 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
8673 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
8674 + "dev": true,
8675 + "license": "MIT",
8676 + "engines": {
8677 + "node": ">=4"
8678 + }
8679 + },
8680 + "node_modules/resolve-pkg-maps": {
8681 + "version": "1.0.0",
8682 + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
8683 + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
8684 + "dev": true,
8685 + "license": "MIT",
8686 + "funding": {
8687 + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
8688 + }
8689 + },
8690 + "node_modules/reusify": {
8691 + "version": "1.1.0",
8692 + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
8693 + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
8694 + "dev": true,
8695 + "license": "MIT",
8696 + "engines": {
8697 + "iojs": ">=1.0.0",
8698 + "node": ">=0.10.0"
8699 + }
8700 + },
8701 + "node_modules/rimraf": {
8702 + "version": "3.0.2",
8703 + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
8704 + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
8705 + "deprecated": "Rimraf versions prior to v4 are no longer supported",
8706 + "dev": true,
8707 + "license": "ISC",
8708 + "dependencies": {
8709 + "glob": "^7.1.3"
8710 + },
8711 + "bin": {
8712 + "rimraf": "bin.js"
8713 + },
8714 + "funding": {
8715 + "url": "https://github.com/sponsors/isaacs"
8716 + }
8717 + },
8718 + "node_modules/rimraf/node_modules/glob": {
8719 + "version": "7.2.3",
8720 + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
8721 + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
8722 + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
8723 + "dev": true,
8724 + "license": "ISC",
8725 + "dependencies": {
8726 + "fs.realpath": "^1.0.0",
8727 + "inflight": "^1.0.4",
8728 + "inherits": "2",
8729 + "minimatch": "^3.1.1",
8730 + "once": "^1.3.0",
8731 + "path-is-absolute": "^1.0.0"
8732 + },
8733 + "engines": {
8734 + "node": "*"
8735 + },
8736 + "funding": {
8737 + "url": "https://github.com/sponsors/isaacs"
8738 + }
8739 + },
8740 + "node_modules/run-parallel": {
8741 + "version": "1.2.0",
8742 + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
8743 + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
8744 + "dev": true,
8745 + "funding": [
8746 + {
8747 + "type": "github",
8748 + "url": "https://github.com/sponsors/feross"
8749 + },
8750 + {
8751 + "type": "patreon",
8752 + "url": "https://www.patreon.com/feross"
8753 + },
8754 + {
8755 + "type": "consulting",
8756 + "url": "https://feross.org/support"
8757 + }
8758 + ],
8759 + "license": "MIT",
8760 + "dependencies": {
8761 + "queue-microtask": "^1.2.2"
8762 + }
8763 + },
8764 + "node_modules/safe-array-concat": {
8765 + "version": "1.1.3",
8766 + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
8767 + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
8768 + "dev": true,
8769 + "license": "MIT",
8770 + "dependencies": {
8771 + "call-bind": "^1.0.8",
8772 + "call-bound": "^1.0.2",
8773 + "get-intrinsic": "^1.2.6",
8774 + "has-symbols": "^1.1.0",
8775 + "isarray": "^2.0.5"
8776 + },
8777 + "engines": {
8778 + "node": ">=0.4"
8779 + },
8780 + "funding": {
8781 + "url": "https://github.com/sponsors/ljharb"
8782 + }
8783 + },
8784 + "node_modules/safe-buffer": {
8785 + "version": "5.1.2",
8786 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
8787 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
8788 + "license": "MIT"
8789 + },
8790 + "node_modules/safe-push-apply": {
8791 + "version": "1.0.0",
8792 + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
8793 + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
8794 + "dev": true,
8795 + "license": "MIT",
8796 + "dependencies": {
8797 + "es-errors": "^1.3.0",
8798 + "isarray": "^2.0.5"
8799 + },
8800 + "engines": {
8801 + "node": ">= 0.4"
8802 + },
8803 + "funding": {
8804 + "url": "https://github.com/sponsors/ljharb"
8805 + }
8806 + },
8807 + "node_modules/safe-regex-test": {
8808 + "version": "1.1.0",
8809 + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
8810 + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
8811 + "dev": true,
8812 + "license": "MIT",
8813 + "dependencies": {
8814 + "call-bound": "^1.0.2",
8815 + "es-errors": "^1.3.0",
8816 + "is-regex": "^1.2.1"
8817 + },
8818 + "engines": {
8819 + "node": ">= 0.4"
8820 + },
8821 + "funding": {
8822 + "url": "https://github.com/sponsors/ljharb"
8823 + }
8824 + },
8825 + "node_modules/scheduler": {
8826 + "version": "0.23.2",
8827 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
8828 + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
8829 + "license": "MIT",
8830 + "dependencies": {
8831 + "loose-envify": "^1.1.0"
8832 + }
8833 + },
8834 + "node_modules/semver": {
8835 + "version": "7.7.4",
8836 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
8837 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
8838 + "license": "ISC",
8839 + "bin": {
8840 + "semver": "bin/semver.js"
8841 + },
8842 + "engines": {
8843 + "node": ">=10"
8844 + }
8845 + },
8846 + "node_modules/set-blocking": {
8847 + "version": "2.0.0",
8848 + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
8849 + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
8850 + "license": "ISC"
8851 + },
8852 + "node_modules/set-function-length": {
8853 + "version": "1.2.2",
8854 + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
8855 + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
8856 + "dev": true,
8857 + "license": "MIT",
8858 + "dependencies": {
8859 + "define-data-property": "^1.1.4",
8860 + "es-errors": "^1.3.0",
8861 + "function-bind": "^1.1.2",
8862 + "get-intrinsic": "^1.2.4",
8863 + "gopd": "^1.0.1",
8864 + "has-property-descriptors": "^1.0.2"
8865 + },
8866 + "engines": {
8867 + "node": ">= 0.4"
8868 + }
8869 + },
8870 + "node_modules/set-function-name": {
8871 + "version": "2.0.2",
8872 + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
8873 + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
8874 + "dev": true,
8875 + "license": "MIT",
8876 + "dependencies": {
8877 + "define-data-property": "^1.1.4",
8878 + "es-errors": "^1.3.0",
8879 + "functions-have-names": "^1.2.3",
8880 + "has-property-descriptors": "^1.0.2"
8881 + },
8882 + "engines": {
8883 + "node": ">= 0.4"
8884 + }
8885 + },
8886 + "node_modules/set-proto": {
8887 + "version": "1.0.0",
8888 + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
8889 + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
8890 + "dev": true,
8891 + "license": "MIT",
8892 + "dependencies": {
8893 + "dunder-proto": "^1.0.1",
8894 + "es-errors": "^1.3.0",
8895 + "es-object-atoms": "^1.0.0"
8896 + },
8897 + "engines": {
8898 + "node": ">= 0.4"
8899 + }
8900 + },
8901 + "node_modules/setimmediate": {
8902 + "version": "1.0.5",
8903 + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
8904 + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
8905 + "license": "MIT"
8906 + },
8907 + "node_modules/sharp": {
8908 + "version": "0.34.5",
8909 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
8910 + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
8911 + "hasInstallScript": true,
8912 + "license": "Apache-2.0",
8913 + "dependencies": {
8914 + "@img/colour": "^1.0.0",
8915 + "detect-libc": "^2.1.2",
8916 + "semver": "^7.7.3"
8917 + },
8918 + "engines": {
8919 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
8920 + },
8921 + "funding": {
8922 + "url": "https://opencollective.com/libvips"
8923 + },
8924 + "optionalDependencies": {
8925 + "@img/sharp-darwin-arm64": "0.34.5",
8926 + "@img/sharp-darwin-x64": "0.34.5",
8927 + "@img/sharp-libvips-darwin-arm64": "1.2.4",
8928 + "@img/sharp-libvips-darwin-x64": "1.2.4",
8929 + "@img/sharp-libvips-linux-arm": "1.2.4",
8930 + "@img/sharp-libvips-linux-arm64": "1.2.4",
8931 + "@img/sharp-libvips-linux-ppc64": "1.2.4",
8932 + "@img/sharp-libvips-linux-riscv64": "1.2.4",
8933 + "@img/sharp-libvips-linux-s390x": "1.2.4",
8934 + "@img/sharp-libvips-linux-x64": "1.2.4",
8935 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
8936 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
8937 + "@img/sharp-linux-arm": "0.34.5",
8938 + "@img/sharp-linux-arm64": "0.34.5",
8939 + "@img/sharp-linux-ppc64": "0.34.5",
8940 + "@img/sharp-linux-riscv64": "0.34.5",
8941 + "@img/sharp-linux-s390x": "0.34.5",
8942 + "@img/sharp-linux-x64": "0.34.5",
8943 + "@img/sharp-linuxmusl-arm64": "0.34.5",
8944 + "@img/sharp-linuxmusl-x64": "0.34.5",
8945 + "@img/sharp-wasm32": "0.34.5",
8946 + "@img/sharp-win32-arm64": "0.34.5",
8947 + "@img/sharp-win32-ia32": "0.34.5",
8948 + "@img/sharp-win32-x64": "0.34.5"
8949 + }
8950 + },
8951 + "node_modules/shebang-command": {
8952 + "version": "2.0.0",
8953 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
8954 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
8955 + "license": "MIT",
8956 + "dependencies": {
8957 + "shebang-regex": "^3.0.0"
8958 + },
8959 + "engines": {
8960 + "node": ">=8"
8961 + }
8962 + },
8963 + "node_modules/shebang-regex": {
8964 + "version": "3.0.0",
8965 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
8966 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
8967 + "license": "MIT",
8968 + "engines": {
8969 + "node": ">=8"
8970 + }
8971 + },
8972 + "node_modules/side-channel": {
8973 + "version": "1.1.0",
8974 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
8975 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
8976 + "dev": true,
8977 + "license": "MIT",
8978 + "dependencies": {
8979 + "es-errors": "^1.3.0",
8980 + "object-inspect": "^1.13.3",
8981 + "side-channel-list": "^1.0.0",
8982 + "side-channel-map": "^1.0.1",
8983 + "side-channel-weakmap": "^1.0.2"
8984 + },
8985 + "engines": {
8986 + "node": ">= 0.4"
8987 + },
8988 + "funding": {
8989 + "url": "https://github.com/sponsors/ljharb"
8990 + }
8991 + },
8992 + "node_modules/side-channel-list": {
8993 + "version": "1.0.0",
8994 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
8995 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
8996 + "dev": true,
8997 + "license": "MIT",
8998 + "dependencies": {
8999 + "es-errors": "^1.3.0",
9000 + "object-inspect": "^1.13.3"
9001 + },
9002 + "engines": {
9003 + "node": ">= 0.4"
9004 + },
9005 + "funding": {
9006 + "url": "https://github.com/sponsors/ljharb"
9007 + }
9008 + },
9009 + "node_modules/side-channel-map": {
9010 + "version": "1.0.1",
9011 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
9012 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
9013 + "dev": true,
9014 + "license": "MIT",
9015 + "dependencies": {
9016 + "call-bound": "^1.0.2",
9017 + "es-errors": "^1.3.0",
9018 + "get-intrinsic": "^1.2.5",
9019 + "object-inspect": "^1.13.3"
9020 + },
9021 + "engines": {
9022 + "node": ">= 0.4"
9023 + },
9024 + "funding": {
9025 + "url": "https://github.com/sponsors/ljharb"
9026 + }
9027 + },
9028 + "node_modules/side-channel-weakmap": {
9029 + "version": "1.0.2",
9030 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
9031 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
9032 + "dev": true,
9033 + "license": "MIT",
9034 + "dependencies": {
9035 + "call-bound": "^1.0.2",
9036 + "es-errors": "^1.3.0",
9037 + "get-intrinsic": "^1.2.5",
9038 + "object-inspect": "^1.13.3",
9039 + "side-channel-map": "^1.0.1"
9040 + },
9041 + "engines": {
9042 + "node": ">= 0.4"
9043 + },
9044 + "funding": {
9045 + "url": "https://github.com/sponsors/ljharb"
9046 + }
9047 + },
9048 + "node_modules/signal-exit": {
9049 + "version": "4.1.0",
9050 + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
9051 + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
9052 + "license": "ISC",
9053 + "engines": {
9054 + "node": ">=14"
9055 + },
9056 + "funding": {
9057 + "url": "https://github.com/sponsors/isaacs"
9058 + }
9059 + },
9060 + "node_modules/source-map-js": {
9061 + "version": "1.2.1",
9062 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
9063 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
9064 + "license": "BSD-3-Clause",
9065 + "engines": {
9066 + "node": ">=0.10.0"
9067 + }
9068 + },
9069 + "node_modules/space-separated-tokens": {
9070 + "version": "2.0.2",
9071 + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
9072 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
9073 + "license": "MIT",
9074 + "funding": {
9075 + "type": "github",
9076 + "url": "https://github.com/sponsors/wooorm"
9077 + }
9078 + },
9079 + "node_modules/stable-hash": {
9080 + "version": "0.0.5",
9081 + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
9082 + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
9083 + "dev": true,
9084 + "license": "MIT"
9085 + },
9086 + "node_modules/stop-iteration-iterator": {
9087 + "version": "1.1.0",
9088 + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
9089 + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
9090 + "dev": true,
9091 + "license": "MIT",
9092 + "dependencies": {
9093 + "es-errors": "^1.3.0",
9094 + "internal-slot": "^1.1.0"
9095 + },
9096 + "engines": {
9097 + "node": ">= 0.4"
9098 + }
9099 + },
9100 + "node_modules/streamsearch": {
9101 + "version": "1.1.0",
9102 + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
9103 + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
9104 + "engines": {
9105 + "node": ">=10.0.0"
9106 + }
9107 + },
9108 + "node_modules/streamx": {
9109 + "version": "2.23.0",
9110 + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
9111 + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
9112 + "license": "MIT",
9113 + "dependencies": {
9114 + "events-universal": "^1.0.0",
9115 + "fast-fifo": "^1.3.2",
9116 + "text-decoder": "^1.1.0"
9117 + }
9118 + },
9119 + "node_modules/string_decoder": {
9120 + "version": "1.1.1",
9121 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
9122 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
9123 + "license": "MIT",
9124 + "dependencies": {
9125 + "safe-buffer": "~5.1.0"
9126 + }
9127 + },
9128 + "node_modules/string-width": {
9129 + "version": "5.1.2",
9130 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
9131 + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
9132 + "license": "MIT",
9133 + "dependencies": {
9134 + "eastasianwidth": "^0.2.0",
9135 + "emoji-regex": "^9.2.2",
9136 + "strip-ansi": "^7.0.1"
9137 + },
9138 + "engines": {
9139 + "node": ">=12"
9140 + },
9141 + "funding": {
9142 + "url": "https://github.com/sponsors/sindresorhus"
9143 + }
9144 + },
9145 + "node_modules/string-width-cjs": {
9146 + "name": "string-width",
9147 + "version": "4.2.3",
9148 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
9149 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
9150 + "license": "MIT",
9151 + "dependencies": {
9152 + "emoji-regex": "^8.0.0",
9153 + "is-fullwidth-code-point": "^3.0.0",
9154 + "strip-ansi": "^6.0.1"
9155 + },
9156 + "engines": {
9157 + "node": ">=8"
9158 + }
9159 + },
9160 + "node_modules/string-width-cjs/node_modules/emoji-regex": {
9161 + "version": "8.0.0",
9162 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
9163 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
9164 + "license": "MIT"
9165 + },
9166 + "node_modules/string-width/node_modules/ansi-regex": {
9167 + "version": "6.2.2",
9168 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
9169 + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
9170 + "license": "MIT",
9171 + "engines": {
9172 + "node": ">=12"
9173 + },
9174 + "funding": {
9175 + "url": "https://github.com/chalk/ansi-regex?sponsor=1"
9176 + }
9177 + },
9178 + "node_modules/string-width/node_modules/strip-ansi": {
9179 + "version": "7.1.2",
9180 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
9181 + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
9182 + "license": "MIT",
9183 + "dependencies": {
9184 + "ansi-regex": "^6.0.1"
9185 + },
9186 + "engines": {
9187 + "node": ">=12"
9188 + },
9189 + "funding": {
9190 + "url": "https://github.com/chalk/strip-ansi?sponsor=1"
9191 + }
9192 + },
9193 + "node_modules/string.prototype.includes": {
9194 + "version": "2.0.1",
9195 + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
9196 + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
9197 + "dev": true,
9198 + "license": "MIT",
9199 + "dependencies": {
9200 + "call-bind": "^1.0.7",
9201 + "define-properties": "^1.2.1",
9202 + "es-abstract": "^1.23.3"
9203 + },
9204 + "engines": {
9205 + "node": ">= 0.4"
9206 + }
9207 + },
9208 + "node_modules/string.prototype.matchall": {
9209 + "version": "4.0.12",
9210 + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
9211 + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
9212 + "dev": true,
9213 + "license": "MIT",
9214 + "dependencies": {
9215 + "call-bind": "^1.0.8",
9216 + "call-bound": "^1.0.3",
9217 + "define-properties": "^1.2.1",
9218 + "es-abstract": "^1.23.6",
9219 + "es-errors": "^1.3.0",
9220 + "es-object-atoms": "^1.0.0",
9221 + "get-intrinsic": "^1.2.6",
9222 + "gopd": "^1.2.0",
9223 + "has-symbols": "^1.1.0",
9224 + "internal-slot": "^1.1.0",
9225 + "regexp.prototype.flags": "^1.5.3",
9226 + "set-function-name": "^2.0.2",
9227 + "side-channel": "^1.1.0"
9228 + },
9229 + "engines": {
9230 + "node": ">= 0.4"
9231 + },
9232 + "funding": {
9233 + "url": "https://github.com/sponsors/ljharb"
9234 + }
9235 + },
9236 + "node_modules/string.prototype.repeat": {
9237 + "version": "1.0.0",
9238 + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
9239 + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
9240 + "dev": true,
9241 + "license": "MIT",
9242 + "dependencies": {
9243 + "define-properties": "^1.1.3",
9244 + "es-abstract": "^1.17.5"
9245 + }
9246 + },
9247 + "node_modules/string.prototype.trim": {
9248 + "version": "1.2.10",
9249 + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
9250 + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
9251 + "dev": true,
9252 + "license": "MIT",
9253 + "dependencies": {
9254 + "call-bind": "^1.0.8",
9255 + "call-bound": "^1.0.2",
9256 + "define-data-property": "^1.1.4",
9257 + "define-properties": "^1.2.1",
9258 + "es-abstract": "^1.23.5",
9259 + "es-object-atoms": "^1.0.0",
9260 + "has-property-descriptors": "^1.0.2"
9261 + },
9262 + "engines": {
9263 + "node": ">= 0.4"
9264 + },
9265 + "funding": {
9266 + "url": "https://github.com/sponsors/ljharb"
9267 + }
9268 + },
9269 + "node_modules/string.prototype.trimend": {
9270 + "version": "1.0.9",
9271 + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
9272 + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
9273 + "dev": true,
9274 + "license": "MIT",
9275 + "dependencies": {
9276 + "call-bind": "^1.0.8",
9277 + "call-bound": "^1.0.2",
9278 + "define-properties": "^1.2.1",
9279 + "es-object-atoms": "^1.0.0"
9280 + },
9281 + "engines": {
9282 + "node": ">= 0.4"
9283 + },
9284 + "funding": {
9285 + "url": "https://github.com/sponsors/ljharb"
9286 + }
9287 + },
9288 + "node_modules/string.prototype.trimstart": {
9289 + "version": "1.0.8",
9290 + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
9291 + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
9292 + "dev": true,
9293 + "license": "MIT",
9294 + "dependencies": {
9295 + "call-bind": "^1.0.7",
9296 + "define-properties": "^1.2.1",
9297 + "es-object-atoms": "^1.0.0"
9298 + },
9299 + "engines": {
9300 + "node": ">= 0.4"
9301 + },
9302 + "funding": {
9303 + "url": "https://github.com/sponsors/ljharb"
9304 + }
9305 + },
9306 + "node_modules/stringify-entities": {
9307 + "version": "4.0.4",
9308 + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
9309 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
9310 + "license": "MIT",
9311 + "dependencies": {
9312 + "character-entities-html4": "^2.0.0",
9313 + "character-entities-legacy": "^3.0.0"
9314 + },
9315 + "funding": {
9316 + "type": "github",
9317 + "url": "https://github.com/sponsors/wooorm"
9318 + }
9319 + },
9320 + "node_modules/strip-ansi": {
9321 + "version": "6.0.1",
9322 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
9323 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
9324 + "license": "MIT",
9325 + "dependencies": {
9326 + "ansi-regex": "^5.0.1"
9327 + },
9328 + "engines": {
9329 + "node": ">=8"
9330 + }
9331 + },
9332 + "node_modules/strip-ansi-cjs": {
9333 + "name": "strip-ansi",
9334 + "version": "6.0.1",
9335 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
9336 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
9337 + "license": "MIT",
9338 + "dependencies": {
9339 + "ansi-regex": "^5.0.1"
9340 + },
9341 + "engines": {
9342 + "node": ">=8"
9343 + }
9344 + },
9345 + "node_modules/strip-bom": {
9346 + "version": "3.0.0",
9347 + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
9348 + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
9349 + "dev": true,
9350 + "license": "MIT",
9351 + "engines": {
9352 + "node": ">=4"
9353 + }
9354 + },
9355 + "node_modules/strip-json-comments": {
9356 + "version": "3.1.1",
9357 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
9358 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
9359 + "dev": true,
9360 + "license": "MIT",
9361 + "engines": {
9362 + "node": ">=8"
9363 + },
9364 + "funding": {
9365 + "url": "https://github.com/sponsors/sindresorhus"
9366 + }
9367 + },
9368 + "node_modules/style-mod": {
9369 + "version": "4.1.3",
9370 + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
9371 + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
9372 + "license": "MIT"
9373 + },
9374 + "node_modules/style-to-js": {
9375 + "version": "1.1.21",
9376 + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
9377 + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
9378 + "license": "MIT",
9379 + "dependencies": {
9380 + "style-to-object": "1.0.14"
9381 + }
9382 + },
9383 + "node_modules/style-to-object": {
9384 + "version": "1.0.14",
9385 + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
9386 + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
9387 + "license": "MIT",
9388 + "dependencies": {
9389 + "inline-style-parser": "0.2.7"
9390 + }
9391 + },
9392 + "node_modules/styled-jsx": {
9393 + "version": "5.1.1",
9394 + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
9395 + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
9396 + "license": "MIT",
9397 + "dependencies": {
9398 + "client-only": "0.0.1"
9399 + },
9400 + "engines": {
9401 + "node": ">= 12.0.0"
9402 + },
9403 + "peerDependencies": {
9404 + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
9405 + },
9406 + "peerDependenciesMeta": {
9407 + "@babel/core": {
9408 + "optional": true
9409 + },
9410 + "babel-plugin-macros": {
9411 + "optional": true
9412 + }
9413 + }
9414 + },
9415 + "node_modules/sucrase": {
9416 + "version": "3.35.1",
9417 + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
9418 + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
9419 + "dev": true,
9420 + "license": "MIT",
9421 + "dependencies": {
9422 + "@jridgewell/gen-mapping": "^0.3.2",
9423 + "commander": "^4.0.0",
9424 + "lines-and-columns": "^1.1.6",
9425 + "mz": "^2.7.0",
9426 + "pirates": "^4.0.1",
9427 + "tinyglobby": "^0.2.11",
9428 + "ts-interface-checker": "^0.1.9"
9429 + },
9430 + "bin": {
9431 + "sucrase": "bin/sucrase",
9432 + "sucrase-node": "bin/sucrase-node"
9433 + },
9434 + "engines": {
9435 + "node": ">=16 || 14 >=14.17"
9436 + }
9437 + },
9438 + "node_modules/supports-color": {
9439 + "version": "7.2.0",
9440 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
9441 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
9442 + "dev": true,
9443 + "license": "MIT",
9444 + "dependencies": {
9445 + "has-flag": "^4.0.0"
9446 + },
9447 + "engines": {
9448 + "node": ">=8"
9449 + }
9450 + },
9451 + "node_modules/supports-preserve-symlinks-flag": {
9452 + "version": "1.0.0",
9453 + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
9454 + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
9455 + "dev": true,
9456 + "license": "MIT",
9457 + "engines": {
9458 + "node": ">= 0.4"
9459 + },
9460 + "funding": {
9461 + "url": "https://github.com/sponsors/ljharb"
9462 + }
9463 + },
9464 + "node_modules/tailwindcss": {
9465 + "version": "3.4.19",
9466 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
9467 + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
9468 + "dev": true,
9469 + "license": "MIT",
9470 + "dependencies": {
9471 + "@alloc/quick-lru": "^5.2.0",
9472 + "arg": "^5.0.2",
9473 + "chokidar": "^3.6.0",
9474 + "didyoumean": "^1.2.2",
9475 + "dlv": "^1.1.3",
9476 + "fast-glob": "^3.3.2",
9477 + "glob-parent": "^6.0.2",
9478 + "is-glob": "^4.0.3",
9479 + "jiti": "^1.21.7",
9480 + "lilconfig": "^3.1.3",
9481 + "micromatch": "^4.0.8",
9482 + "normalize-path": "^3.0.0",
9483 + "object-hash": "^3.0.0",
9484 + "picocolors": "^1.1.1",
9485 + "postcss": "^8.4.47",
9486 + "postcss-import": "^15.1.0",
9487 + "postcss-js": "^4.0.1",
9488 + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
9489 + "postcss-nested": "^6.2.0",
9490 + "postcss-selector-parser": "^6.1.2",
9491 + "resolve": "^1.22.8",
9492 + "sucrase": "^3.35.0"
9493 + },
9494 + "bin": {
9495 + "tailwind": "lib/cli.js",
9496 + "tailwindcss": "lib/cli.js"
9497 + },
9498 + "engines": {
9499 + "node": ">=14.0.0"
9500 + }
9501 + },
9502 + "node_modules/tar-stream": {
9503 + "version": "3.1.7",
9504 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
9505 + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
9506 + "license": "MIT",
9507 + "dependencies": {
9508 + "b4a": "^1.6.4",
9509 + "fast-fifo": "^1.2.0",
9510 + "streamx": "^2.15.0"
9511 + }
9512 + },
9513 + "node_modules/tar-stream/node_modules/b4a": {
9514 + "version": "1.7.5",
9515 + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.5.tgz",
9516 + "integrity": "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA==",
9517 + "license": "Apache-2.0",
9518 + "peerDependencies": {
9519 + "react-native-b4a": "*"
9520 + },
9521 + "peerDependenciesMeta": {
9522 + "react-native-b4a": {
9523 + "optional": true
9524 + }
9525 + }
9526 + },
9527 + "node_modules/text-decoder": {
9528 + "version": "1.2.7",
9529 + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
9530 + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
9531 + "license": "Apache-2.0",
9532 + "dependencies": {
9533 + "b4a": "^1.6.4"
9534 + }
9535 + },
9536 + "node_modules/text-decoder/node_modules/b4a": {
9537 + "version": "1.7.5",
9538 + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.5.tgz",
9539 + "integrity": "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA==",
9540 + "license": "Apache-2.0",
9541 + "peerDependencies": {
9542 + "react-native-b4a": "*"
9543 + },
9544 + "peerDependenciesMeta": {

Diff truncated — file too large.