feat: dashboard, Convex auth, UI components, and LemonSpace branding
- Add dashboard shell with auth integration - Wire Better Auth / Convex (client, server, HTTP routes) - Add shadcn-style UI primitives and logo assets - Update global styles and landing page - Add internal docs (.docs) Made-with: Cursor
237
.docs/LemonSpace_Manifest_v1_2.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# 🍋 LemonSpace — Produkt-Manifest
|
||||||
|
|
||||||
|
**v1.2 — März 2026**
|
||||||
|
|
||||||
|
*Self-Hosted, Source-Available Creative Workspace*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vision
|
||||||
|
|
||||||
|
LemonSpace ist eine self-hosted, source-available Alternative zu Freepik Spaces. Eine visuelle Arbeitsfläche, auf der kreative Teams aus wenigen Input-Assets schnell kampagnenfähige Bildvarianten erzeugen — mit KI-gestützter Generierung, durchdachter Latenz-UX und voller Kontrolle über ihre Daten.
|
||||||
|
|
||||||
|
Das Produkt positioniert sich nicht als generisches „AI Creative Workspace", sondern löst ein spezifisches Problem: **Vom Rohbild zur fertigen Kampagnenvariante in Minuten statt Stunden** — auf eigener Infrastruktur oder als gehosteter Service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problemstellung
|
||||||
|
|
||||||
|
Freepik Spaces zeigt, dass KI-gestützte Canvas-Workflows funktionieren. Aber:
|
||||||
|
|
||||||
|
- Proprietäres SaaS ohne Self-Hosting-Option
|
||||||
|
- Abhängig von Freepiks Pricing, Modellauswahl und Verfügbarkeit
|
||||||
|
- Keine Anpassbarkeit oder Erweiterbarkeit
|
||||||
|
- Datenschutzbedenken bei kreativen Assets auf fremden Servern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Primärer ICP
|
||||||
|
|
||||||
|
**Kleine Design- und Marketing-Teams (2–10 Personen)**, die aus wenigen Input-Assets schnell kampagnenfähige Bildvarianten erzeugen wollen, ohne ihre Daten in generischen SaaS-Tools zu verstreuen.
|
||||||
|
|
||||||
|
### Typisches Profil
|
||||||
|
|
||||||
|
- In-House-Designteam oder kleine Agentur
|
||||||
|
- Regelmäßig Social-Media-, Kampagnen- oder Produktbilder
|
||||||
|
- Entscheider oder Budget-Owner für Creative-Tools
|
||||||
|
- Datenschutz und Kontrolle über Assets sind kaufentscheidend
|
||||||
|
|
||||||
|
> **Sekundäre Segmente (nicht Phase 1):** Entwickler/Tech-Teams (Self-Hosted-Anpassung), Compliance-sensible Unternehmen (Regulatorik), Open-Source-Community (Contributions). Diese Segmente werden adressiert, sobald der Kern-Job für den primären ICP validiert ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Der eine MVP-Job
|
||||||
|
|
||||||
|
Phase 1 löst genau einen End-to-End-Job so gut, dass Nutzer wiederkommen oder zahlen:
|
||||||
|
|
||||||
|
> **Upload Bilder → Prompt / Brief → Bildvarianten generieren → Vergleichen → Export**
|
||||||
|
>
|
||||||
|
> *Alles, was diesen Flow nicht direkt besser macht, ist erstmal verdächtig.*
|
||||||
|
|
||||||
|
### Konkret bedeutet das für Phase 1
|
||||||
|
|
||||||
|
1. Nutzer lädt 1–5 Produktbilder auf den Canvas (Bild-Node)
|
||||||
|
2. Schreibt einen Prompt oder Brief direkt am Canvas (Prompt-Node — jetzt Phase 1)
|
||||||
|
3. Generiert 4–8 Bildvarianten per KI (KI-Bild-Nodes)
|
||||||
|
4. Vergleicht Ergebnisse nebeneinander (Compare-Node — jetzt Phase 1)
|
||||||
|
5. Exportiert fertige Varianten als PNG oder ZIP (Export — jetzt Phase 1)
|
||||||
|
|
||||||
|
> **Inkonsistenzen aus v1.0 behoben:** Prompt-Node und Compare-Node sind in Phase 1 vorgezogen, weil sie für den Kern-Job essenziell sind. Export (PNG/ZIP) ist ebenfalls Phase 1 — ohne Export gibt es kein „Job done".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Node-System — Phase 1
|
||||||
|
|
||||||
|
Nur die Nodes, die den MVP-Job ermöglichen. Die vollständige Node-Taxonomie (5 Kategorien, 25+ Node-Typen) wird in einem separaten **Node Spec Doc** dokumentiert.
|
||||||
|
|
||||||
|
| Node | Kategorie | Rolle im MVP-Job |
|
||||||
|
|------|-----------|------------------|
|
||||||
|
| Bild | Quelle | Upload eigener Bilder (PNG, JPG, WebP) oder Einbindung per URL |
|
||||||
|
| Text | Quelle | Freitextfeld für Copy, Brief, Beschreibungen — semantisch kein Prompt |
|
||||||
|
| Prompt | Quelle | Dedizierter Node für Modellinstruktionen. Verbindet sich mit KI-Nodes |
|
||||||
|
| KI-Bild | KI-Ausgabe | Output eines Bildgenerierungs-Calls. Speichert Prompt, Modell, Parameter |
|
||||||
|
| Gruppe | Layout | Container für Nodes. Collapse/Expand, benannte Scopes |
|
||||||
|
| Frame | Layout | Artboard mit definierter Auflösung. Export-Boundary für PNG/ZIP |
|
||||||
|
| Notiz | Layout | Annotation auf dem Canvas. Markdown, kein Datenanschluss |
|
||||||
|
| Compare | Layout | Zwei Bilder nebeneinander mit interaktivem Slider |
|
||||||
|
|
||||||
|
### Ausblick: Spätere Phasen
|
||||||
|
|
||||||
|
Transformation (BG entfernen, Upscale, Crop), Steuerung & Flow (Splitter, Loop, Agent, Mixer, Weiche), erweiterte Layout-Nodes (Text-Overlay, Kommentar, Präsentation) und weitere Quell-Nodes (Video, Asset, Farbe/Palette). Details im Node Spec Doc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. UX-Prinzipien
|
||||||
|
|
||||||
|
Die UX-Strategie für Latenzen ist **Kern-DNA des Produkts**, nicht Nice-to-have. Sie unterscheidet LemonSpace von Tools, die KI-Wartezeiten als globale Spinner behandeln.
|
||||||
|
|
||||||
|
### Node-Status-Modell
|
||||||
|
|
||||||
|
Jeder ausführende Node zeigt seinen Zustand visuell direkt auf dem Canvas:
|
||||||
|
|
||||||
|
```
|
||||||
|
idle → analyzing → clarifying → executing (Step X/N) → done | error
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei Fehler: Error-State direkt am Node mit kurzem Hinweis („Timeout — Credits wurden nicht abgebucht"). Kein globales Loading-Banner, kein blockierendes Modal.
|
||||||
|
|
||||||
|
### Skeleton Nodes
|
||||||
|
|
||||||
|
Sobald ein Agent seinen Execution-Plan erstellt hat, erscheinen Skeleton-Nodes auf dem Canvas — noch bevor API-Calls laufen. Skeletons sind verschiebbar, zeigen Node-Typ-Icons und füllen sich sequenziell mit echten Outputs. Visuell: gedimmter Rahmen mit Shimmer-Effekt.
|
||||||
|
|
||||||
|
### Browser Notifications
|
||||||
|
|
||||||
|
Opt-in via Browser Notifications API: Bei Tab-Wechsel und fertigem Job erhält der Nutzer eine native Benachrichtigung. Nicht erzwungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Bewusste Entscheidungen
|
||||||
|
|
||||||
|
Kompakt statt erschöpfend. Details wandern in eigene Architecture Decision Records (ADRs).
|
||||||
|
|
||||||
|
| Thema | Entscheidung | Status |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| Backend | Convex (self-hosted). Bewusster Lock-in für Realtime, Storage, Jobs. Migrations-Pfad: Convex Cloud EU. | ✅ |
|
||||||
|
| Auth | Better Auth (self-hosted, open-source) | ✅ |
|
||||||
|
| AI Layer | OpenRouter als primäre AI-Schicht. 9 Image-Modelle, Text/Reasoning via Claude / GPT. | ✅ |
|
||||||
|
| Self-hosted KI | rembg, Real-ESRGAN, GFPGAN — kostenlos, separate Repos | ✅ |
|
||||||
|
| Payment | Lemon Squeezy (Merchant of Record, VAT-Handling) | ✅ |
|
||||||
|
| Credits | Reservation + Commit. Gecachte Preise (Redis, TTL ~10min). Kein nachträglicher Ausgleich. | ✅ |
|
||||||
|
| Pricing | 4 Tiers: Free / Starter €9 / Pro €49 / Business €99. 30% Marge, 70% → Credits. | ✅ |
|
||||||
|
| Lizenz | BSL 1.1, 3J Change Date → Apache 2.0. Private Nutzung frei. | ✅ |
|
||||||
|
| Repo-Strategie | Zwei unabhängige Repos (lemonspace-web + lemonspace-landing). Auth-Cookie-Sharing via `.lemonspace.io`. | ✅ |
|
||||||
|
| Frontend | Next.js 16 + Tailwind v4 + ShadCN | ✅ |
|
||||||
|
| Canvas | @xyflow/react + dnd-kit | ✅ |
|
||||||
|
| E-Mail | useSend + Stalwart (Self-Hosted). Für lemonspace.app pragmatisch externer SMTP möglich. | ✅ |
|
||||||
|
| Kollaboration | Phase 3. Phase 1 fokussiert auf Solo-/Kleinteam-Workflows. | ⏳ |
|
||||||
|
| Abuse Prevention | Daily Caps, Concurrency Limits, Freemium-Guardrails — Design TBD. | ⏳ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Was wir bewusst nicht bauen (Phase 1)
|
||||||
|
|
||||||
|
Fokus heißt Nein sagen. Diese Features sind bewusst ausgeklammert, nicht vergessen:
|
||||||
|
|
||||||
|
| Feature | Warum nicht jetzt |
|
||||||
|
|---------|-------------------|
|
||||||
|
| Echtzeit-Kollaboration | Kein Kernbedürfnis des primären ICP in Phase 1. Solo-/Kleinteam reicht. |
|
||||||
|
| Agent Nodes | Zu komplex für MVP. Erst bauen, wenn der Basis-Job validiert ist. |
|
||||||
|
| Video-Generierung | Anderer Job, andere Kosten, anderer ICP. |
|
||||||
|
| Freepik Asset Browser | Nice-to-have, nicht Kern-Job. |
|
||||||
|
| Style Transfer / GFPGAN | Transformation-Nodes kommen in Phase 2–3. |
|
||||||
|
| Team-Features | Workspaces, Rollen, Rechte, Seat-Management — erst wenn Business-Tier validiert. |
|
||||||
|
| docker-compose.yml | Self-Hosting dokumentieren, aber nicht den Hosted-MVP verzögern. |
|
||||||
|
| E2E-Testing | Neubewertung bei Skalierung. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Erfolgskriterien
|
||||||
|
|
||||||
|
Ohne messbare Ziele ist jedes PRD Wünsch-dir-was. Diese Metriken entscheiden, ob Phase 1 funktioniert:
|
||||||
|
|
||||||
|
### Produkt-Metriken
|
||||||
|
|
||||||
|
| Metrik | Ziel (Phase 1) | Messung |
|
||||||
|
|--------|----------------|---------|
|
||||||
|
| Time to first output | < 3 Minuten | Onboarding-Flow-Tracking |
|
||||||
|
| Erfolgsquote pro Generierung | > 90% | API-Success-Rate |
|
||||||
|
| Median-Latenz Bildgenerierung | < 10 Sekunden | Node-Status-Events |
|
||||||
|
| Export-Rate | > 40% der Sessions | Export-Events |
|
||||||
|
| D7-Retention | > 25% | Rybbit Analytics |
|
||||||
|
| W4-Retention | > 15% | Rybbit Analytics |
|
||||||
|
|
||||||
|
### Business-Metriken
|
||||||
|
|
||||||
|
| Metrik | Ziel (6 Monate) | Messung |
|
||||||
|
|--------|-----------------|---------|
|
||||||
|
| Conversion Free → Paid | > 5% | Lemon Squeezy Events |
|
||||||
|
| COGS pro aktivem Workspace | < 70% des Abo-Preises | OpenRouter-Kosten / aktive User |
|
||||||
|
| MRR | €2.000+ | Lemon Squeezy Dashboard |
|
||||||
|
| Churn (monatlich) | < 8% | Subscription-Events |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Abuse Prevention & Guardrails
|
||||||
|
|
||||||
|
Ein AI-Kreativtool mit Free-Tier und Premium-Modellen braucht von Tag 1 Schutzmaßnahmen. Kein Randthema.
|
||||||
|
|
||||||
|
### Geplante Maßnahmen
|
||||||
|
|
||||||
|
- Daily Generation Caps pro Tier (Free: 10/Tag, Starter: 50, Pro: 200, Business: 500)
|
||||||
|
- Concurrency Limits: max. 2 parallele Generierungen (Free: 1)
|
||||||
|
- Rate Limiting auf allen API-Endpunkten (Redis-backed)
|
||||||
|
- Premium-Modelle erst ab Starter-Tier (Free nur Budget-Modelle)
|
||||||
|
- Top-Up-Limit pro Monat (verhindert Missbrauch des Selbstkostenpreises)
|
||||||
|
- Account-Verifizierung per E-Mail, optional Telefon bei Abuse-Verdacht
|
||||||
|
|
||||||
|
> **Offene Entscheidung:** Konkretes Design der Caps und Limits wird nach ersten Nutzungsdaten kalibriert. Initiale Werte sind konservativ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Lizenz-Klarstellung
|
||||||
|
|
||||||
|
LemonSpace ist **Source Available**, nicht Open Source.
|
||||||
|
|
||||||
|
| | Community Use | Commercial Self-Host | Hosted SaaS |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Zugang | Quellcode öffentlich | Separate Lizenz | lemonspace.app |
|
||||||
|
| Kosten | Kostenlos | Lizenzgebühr (TBD) | Abo-Tiers |
|
||||||
|
| Nutzung | Privat / persönlich | Unternehmen, produktiv | Jeder |
|
||||||
|
| Self-hosted KI | Kostenlos | Kostenlos | Kostenlos |
|
||||||
|
| Support | Community | E-Mail | In-App |
|
||||||
|
|
||||||
|
BSL 1.1 mit 3-Jahres-Change-Date zu Apache 2.0. Nach Change Date ist jedes Release vollständig Apache-2.0-lizenziert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Nächste Schritte
|
||||||
|
|
||||||
|
Priorisiert nach Abhängigkeiten. Jeder Schritt hat ein klares Artefakt.
|
||||||
|
|
||||||
|
| # | Schritt | Artefakt |
|
||||||
|
|---|---------|----------|
|
||||||
|
| 1 | Repos scaffolden | `lemonspace-web` (Next.js + Convex + BetterAuth) + `lemonspace-landing` (Next.js) |
|
||||||
|
| 2 | Convex Schema entwerfen | Schema-Datei mit Node-Typen + Credit-System |
|
||||||
|
| 3 | Basis-Canvas mit @xyflow/react | Funktionierender Canvas mit Bild- und Prompt-Nodes |
|
||||||
|
| 4 | OpenRouter-Prototyp | Image Gen (Gemini 2.5 Flash) funktioniert im Canvas |
|
||||||
|
| 5 | Compare + Export | PNG/ZIP-Export aus Frame-Nodes |
|
||||||
|
| 6 | Better Auth + Credit-System | Login, Balance-Tracking, Reservation+Commit |
|
||||||
|
| 7 | Lemon Squeezy Integration | Checkout, Webhooks, Credit-Zuweisung |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Ausgelagerte Dokumente
|
||||||
|
|
||||||
|
Folgende Themen werden in eigenen Dokumenten vertieft. Das Manifest bleibt schlank.
|
||||||
|
|
||||||
|
| Dokument | Inhalt |
|
||||||
|
|----------|--------|
|
||||||
|
| System Design Doc | Tech Stack mit Versionen, Zwei-Repo-Strategie, Infra-Details, Convex-Architektur, Redis, Cloudflare |
|
||||||
|
| Node Spec Doc | Vollständige Node-Taxonomie (5 Kategorien, 25+ Typen), Datenmodell pro Node-Typ |
|
||||||
|
| Credit & Pricing Doc | Detaillierte Pricing-Tabellen, Credit-Mechanik, Reservation+Commit-Flow, Agent Partial Failure |
|
||||||
|
| Self-Hosting Guide | docker-compose.yml, .env.example, Setup-README, Coolify-Anleitung |
|
||||||
|
| ADR-Sammlung | Architecture Decision Records für Convex, OpenRouter, BSL 1.1, useSend, etc. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*LemonSpace Manifest v1.2 — März 2026*
|
||||||
560
.docs/LemonSpace_PRD_v1_1.md
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
# 🍋 LemonSpace
|
||||||
|
## Product Requirements Document
|
||||||
|
*Self-Hosted, Source-Available Alternative to Freepik Spaces*
|
||||||
|
|
||||||
|
| Version | Status | Datum | Projekt |
|
||||||
|
|---------|--------|-------|---------|
|
||||||
|
| v1.1 | Draft | März 2026 | lemonspace.app |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
| Version | Änderung |
|
||||||
|
|---------|----------|
|
||||||
|
| v0.2 | KI-Integrationsstrategie: OpenRouter als primäre AI-Layer, Freepik API auf Stock-Assets reduziert |
|
||||||
|
| v0.3 | Agent Layer eingeführt: Agent Nodes als Canvas-native Smart Batch Processor |
|
||||||
|
| v0.4 | Vollständige Node-Taxonomie: fünf Kategorien, Semantik je Node-Typ |
|
||||||
|
| v0.5 | Auth: Better Auth. Pricing: 4-Tier-Abo, Credit-System 30% Marge. Tailwind v4 bestätigt |
|
||||||
|
| v0.6 | Lizenz: BSL 1.1 mit 3-Jahres-Change-Date zu Apache 2.0 |
|
||||||
|
| v0.7 | Tech Stack: Redis, Zod, Unsend + Stalwart, Rybbit, Sentry, Cloudflare |
|
||||||
|
| v0.8 | Text-Overlay Node eingeführt (Kategorie 5: Canvas & Layout) |
|
||||||
|
| v0.9 | Zwei-Repo-Strategie (Web-App + Landing Page), Auth-Cookie-Sharing |
|
||||||
|
| v1.0 | Self-Hosting-Strategie, Credit Reservation+Commit, UX-Latenzen/Skeleton-Nodes, Convex Lock-in dokumentiert |
|
||||||
|
| v1.1 | Monorepo verworfen → Zwei unabhängige Repos (lemonspace-web + lemonspace-landing), Auth-Cookie-Sharing via .lemonspace.io |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vision & Zielsetzung
|
||||||
|
|
||||||
|
LemonSpace ist eine self-hosted, source-available Alternative zu Freepik Spaces — ein kollaboratives, KI-gestütztes Creative-Workflow-Tool mit einer Infinite-Canvas-Oberfläche. Ziel ist ein freier und erweiterbarer Workspace für kreative Teams, der auf eigener Infrastruktur betrieben werden kann.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problemstellung
|
||||||
|
|
||||||
|
Freepik Spaces ist ein leistungsstarkes Tool für KI-gestützte kreative Workflows, aber:
|
||||||
|
|
||||||
|
- Proprietäres SaaS-Produkt ohne Self-Hosting-Option
|
||||||
|
- Nutzer abhängig von Freepiks Pricing und Verfügbarkeit
|
||||||
|
- Keine Anpassbarkeit oder Erweiterbarkeit
|
||||||
|
- Datenschutzbedenken bei der Speicherung kreativer Assets auf externen Servern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Zielgruppe
|
||||||
|
|
||||||
|
| Segment | Primärer Zugang | Beschreibung |
|
||||||
|
|---------|-----------------|--------------|
|
||||||
|
| Designer & kreative Teams | Gehostete Version (lemonspace.app) | Datensouveränität ohne technischen Aufwand — zahlende Kernkunden |
|
||||||
|
| Entwickler & Tech-Teams | Self-Hosted | Anpassbare KI-Canvas-Plattform auf eigener Infra |
|
||||||
|
| Compliance-sensible Unternehmen | Self-Hosted | Regulatorische Anforderungen, die Cloud-SaaS einschränken |
|
||||||
|
| Open-Source-Community | Self-Hosted / Contributing | Creative-Tools-Ökosystem, BSL-Lizenz bis Change Date |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Core Features
|
||||||
|
|
||||||
|
### 4.1 Infinite Canvas
|
||||||
|
|
||||||
|
- Zoom, Pan und Navigation auf einem unbegrenzten Canvas
|
||||||
|
- Nodes als wiederverwendbare kreative Bausteine
|
||||||
|
- Drag & Drop von Assets, KI-Outputs und Mediendateien
|
||||||
|
- Gruppierung und Layering von Canvas-Elementen
|
||||||
|
|
||||||
|
### 4.2 Node-System
|
||||||
|
|
||||||
|
Das Canvas-System basiert auf einem erweiterbaren Node-Modell. Nodes sind typisierte Bausteine, die untereinander verbunden werden und Daten weitergeben. Es gibt fünf Kategorien.
|
||||||
|
|
||||||
|
#### Kategorie 1: Quelle
|
||||||
|
|
||||||
|
Quelle-Nodes bringen Inhalte in den Canvas. Sie haben keine eingehenden Verbindungen, nur ausgehende.
|
||||||
|
|
||||||
|
| Node | Beschreibung | Phase |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| Bild | Upload eigener Bilder (PNG, JPG, WebP) oder Einbindung per URL. Basis-Asset für alle weiteren Operationen. | 1 |
|
||||||
|
| Text | Freitextfeld mit Markdown-Support. Enthält Inhalte (Copy, Brief, Beschreibung) — semantisch verschieden vom Prompt-Node. | 1 |
|
||||||
|
| Prompt | Dedizierter Node für Modellinstruktionen. Verbindet sich ausschließlich mit KI-Nodes. | 2 |
|
||||||
|
| Farbe / Palette | Definiert Farben oder Farbpaletten als Style-Referenz. Kann an KI-Nodes oder Style-Transfer übergeben werden. | 2 |
|
||||||
|
| Video | Upload von Videodateien oder Einbindung per Link. Darstellung als Thumbnail-Node, Playback im Panel. | 2 |
|
||||||
|
| Asset | Freepik Stock-Assets (Fotos, Vektoren, Icons), direkt aus dem Asset Browser auf den Canvas gezogen. | 2 |
|
||||||
|
|
||||||
|
#### Kategorie 2: KI-Ausgabe
|
||||||
|
|
||||||
|
KI-Ausgabe-Nodes sind das Ergebnis einer Modell-Operation. Sie werden vom System erzeugt, nicht vom Nutzer angelegt.
|
||||||
|
|
||||||
|
| Node | Beschreibung | Phase |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| KI-Bild | Output eines Bildgenerierungs-Calls. Speichert Prompt, verwendetes Modell und Generierungsparameter. | 1 |
|
||||||
|
| KI-Text | Output eines Text/Reasoning-Calls. Enthält generierten Copy, Captions, strukturierte Texte. | 2 |
|
||||||
|
| KI-Video | Output eines Videogenerierungs-Calls. Keyframe-basierte Generierung aus Bild-Input möglich. | 2 |
|
||||||
|
| Agent-Ausgabe | Bundle-Output eines Agent Nodes. Kann mehrere typisierte Sub-Outputs enthalten. | 3 |
|
||||||
|
|
||||||
|
#### Kategorie 3: Transformation
|
||||||
|
|
||||||
|
| Node | Beschreibung | Phase |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| Crop / Resize | Freie Bildausschnitt-Auswahl direkt auf dem Canvas, mit Aspect-Ratio-Lock. | 2 |
|
||||||
|
| BG entfernen | Hintergrundentfernung via rembg. Output ist ein freigestelltes Bild. Batch-Modus möglich. | 2 |
|
||||||
|
| Upscale | Hochskalierung via Real-ESRGAN. Unterstützt Faktoren 2×, 4×, 8×. | 2 |
|
||||||
|
| Style Transfer | Überträgt visuellen Stil eines Referenzbildes auf einen anderen Input. | 3 |
|
||||||
|
| Gesicht | Face Restoration via GFPGAN. Verbessert Gesichtsdetails in generierten oder degradierten Bildern. | 3 |
|
||||||
|
|
||||||
|
#### Kategorie 4: Steuerung & Flow
|
||||||
|
|
||||||
|
| Node | Semantik | Beschreibung | Phase |
|
||||||
|
|------|----------|--------------|-------|
|
||||||
|
| Splitter | 1 → N | Verteilt 1 Input auf N identische oder abgeleitete Outputs. Ohne Bedingung. | 2 |
|
||||||
|
| Loop | Liste → N | Iteriert über eine Liste von Inputs und führt dieselbe verknüpfte Operation für jeden Eintrag aus. | 2 |
|
||||||
|
| Agent | N → Plan → N | LLM-Orchestrator. Analysiert Inputs, plant strukturierten Ausführungsplan, delegiert Operationen. | 2 |
|
||||||
|
| Mixer / Merge | N → 1 | Kombiniert N Inputs zu 1 Output durch Überblendung, Komposition oder Selektion. | 3 |
|
||||||
|
| Weiche | 1 → Pfad A/B/... | Bedingter Router. Leitet den Input anhand einer definierbaren Bedingung auf einen von mehreren Ausgangspfaden. | 3 |
|
||||||
|
|
||||||
|
#### Kategorie 5: Canvas & Layout
|
||||||
|
|
||||||
|
| Node | Beschreibung | Phase |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| Gruppe | Container für andere Nodes. Unterstützt Collapse/Expand und benannte Scopes. | 1 |
|
||||||
|
| Frame | Artboard mit definierter Auflösung. Dient als Export-Boundary. | 1 |
|
||||||
|
| Notiz | Annotation auf dem Canvas. Markdown-Support, kein Datenanschluss. | 1 |
|
||||||
|
| Text-Overlay | Editierbarer Text-Layer über Bild- oder Video-Nodes innerhalb eines Frames. Verbraucht keine Credits. | 2 |
|
||||||
|
| Compare | Stellt zwei Bilder nebeneinander mit interaktivem Slider dar. | 2 |
|
||||||
|
| Kommentar | Kollaborations-Node für Reviews. Unterstützt Threads, @mentions und Resolve-Status. | 3 |
|
||||||
|
| Präsentation | Definiert Canvas-Bereiche als geordnete Slideshow. Export als PDF möglich. | 3 |
|
||||||
|
|
||||||
|
### 4.3 Agent Nodes
|
||||||
|
|
||||||
|
Agent Nodes sind ein spezieller Node-Typ auf dem Canvas. Sie fungieren als Smart Batch Processor: Sie nehmen mehrere Input-Nodes entgegen, orchestrieren komplexe Multi-Step-Workflows über ein Text/Reasoning LLM und produzieren mehrere Output-Nodes direkt auf dem Canvas.
|
||||||
|
|
||||||
|
**Ausführungsphasen:**
|
||||||
|
|
||||||
|
1. **Analyse:** Agent erhält alle verbundenen Inputs, LLM prüft ob alle nötigen Informationen vorhanden sind
|
||||||
|
2. **Clarification (optional):** Fehlen Angaben, stellt der Agent gezielt Rückfragen direkt am Node
|
||||||
|
3. **Execution:** LLM plant einen strukturierten Output-Plan (JSON), der dann als Batch abgearbeitet wird
|
||||||
|
4. **Output:** Ergebnisse landen als neue Nodes auf dem Canvas, verbunden mit dem Agent Node
|
||||||
|
|
||||||
|
**Vordefinierte Agent Templates:**
|
||||||
|
|
||||||
|
| Template | Typische Inputs | Typische Outputs |
|
||||||
|
|----------|-----------------|------------------|
|
||||||
|
| Instagram Curator | Produktfotos, Brand Brief, Zielgruppe | Feed Posts, Text-Overlays, Captions, Hashtag-Sets |
|
||||||
|
| (weitere folgen) | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Tech Stack
|
||||||
|
|
||||||
|
| Bereich | Technologie | Version / Hinweis |
|
||||||
|
|---------|-------------|-------------------|
|
||||||
|
| Frontend Framework | Next.js | 16.1.1 — App Router, Server Components |
|
||||||
|
| Styling | Tailwind CSS | v4 |
|
||||||
|
| UI Komponenten | ShadCN/UI | Aktuelle stabile Version |
|
||||||
|
| Backend / Realtime | Convex | Self-hosted via convex-backend |
|
||||||
|
| Authentifizierung | Better Auth | Self-hosted, open-source |
|
||||||
|
| Canvas / Flow | @xyflow/react | ehem. react-flow-renderer |
|
||||||
|
| Drag & Drop | dnd-kit | Empfohlen über react-dnd (bessere Performance) |
|
||||||
|
| Deployment | Coolify | VPS-Deployment für alle Self-hosted Services |
|
||||||
|
| Payment | Lemon Squeezy | Merchant of Record, VAT-Handling |
|
||||||
|
| Input Validation | Zod | Frontend + Backend, Convex Mutations |
|
||||||
|
| In-Memory Store | Redis | Self-hosted via Coolify |
|
||||||
|
| Rate Limiting | Redis-backed | Next.js Middleware / Route Handler |
|
||||||
|
| E-Mail | Unsend + Stalwart | Self-hosted via Coolify |
|
||||||
|
| Analytics | Rybbit | Self-hosted via Coolify |
|
||||||
|
| Error Tracking | Sentry Cloud | Free Tier (5.000 Errors/Monat) |
|
||||||
|
| DNS / DDoS / CDN | Cloudflare | Domain-Routing, DDoS-Schutz, Asset-Caching |
|
||||||
|
| Package Manager | pnpm | Je Repo |
|
||||||
|
|
||||||
|
### Zwei-Repo-Strategie
|
||||||
|
|
||||||
|
Statt eines Monorepos werden zwei unabhängige Repositories gepflegt. Zwischen den Repos gibt es keinen geteilten Code — die Landing Page hat keine Abhängigkeit auf Convex-Schemas, Node-Types oder andere App-Logik.
|
||||||
|
|
||||||
|
| Repo | Domain | Inhalt |
|
||||||
|
|------|--------|--------|
|
||||||
|
| `lemonspace-web` | app.lemonspace.io | Next.js App (Canvas, Dashboard, Auth, AI, Convex) |
|
||||||
|
| `lemonspace-landing` | lemonspace.io | Next.js Marketing Site |
|
||||||
|
|
||||||
|
**Auth-Cookie-Sharing:** BetterAuth setzt einen Session-Cookie auf `.lemonspace.io` (Dot-Prefix = gilt für alle Subdomains). Die Landing Page liest diesen Cookie, um den Login-State zu erkennen und zwischen "Get Started" und "Dashboard" Button zu wechseln. Die Landing Page führt keine Auth-Operationen durch — sie liest nur den Cookie.
|
||||||
|
|
||||||
|
### Self-Hosting-Strategie
|
||||||
|
|
||||||
|
Self-Hosting richtet sich primär an technisch versierte Nutzer und Entwickler. Die gehostete Version (lemonspace.app) ist der empfohlene Weg für alle anderen — insbesondere für Designer und kreative Teams ohne DevOps-Erfahrung.
|
||||||
|
|
||||||
|
Das Self-Hosting-Paket umfasst:
|
||||||
|
|
||||||
|
- **`docker-compose.yml`** — fasst alle Services zusammen: Next.js, Convex, Redis, Stalwart, Rybbit, rembg, Real-ESRGAN, GFPGAN
|
||||||
|
- **`.env.example`** — alle Umgebungsvariablen mit Kommentaren und Standardwerten
|
||||||
|
- **Setup-README** — Schritt-für-Schritt-Anleitung (Voraussetzungen: Docker + Coolify oder plain Docker)
|
||||||
|
|
||||||
|
> **Hinweis:** Self-hosted KI-Services (rembg, Real-ESRGAN, GFPGAN) bleiben in separaten Repositories mit eigenem Docker/Infra-Lifecycle und werden über Coolify unabhängig deployt.
|
||||||
|
|
||||||
|
### Convex: Architektonische Entscheidung & Lock-in
|
||||||
|
|
||||||
|
Convex liefert Realtime-Sync, File Storage und Background Jobs out-of-the-box, ohne dass eine eigene WebSocket-Infrastruktur, S3-Integration und Queue-Lösung zusammengestückelt werden muss. Dieser Geschwindigkeitsvorteil rechtfertigt den bewussten Vendor Lock-in.
|
||||||
|
|
||||||
|
> **Risiko (bewusst akzeptiert):** Der gesamte Realtime-, Storage- und Job-Stack ist an Convex gebunden. Eine spätere Migration ist aufwändig. Es wird keine künstliche Abstraktionsschicht eingebaut, da sie den Kernvorteil von Convex aufheben würde.
|
||||||
|
|
||||||
|
Dokumentierter Migrations-Pfad bei Skalierung: Convex Cloud mit EU-Standort. Convex bietet das eigene Migrations-Tooling und kennt das Ökosystem. Self-hosted Convex bleibt die Default-Strategie für Phase 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. KI-Integrationsstrategie
|
||||||
|
|
||||||
|
### Zwei LLM-Rollen im System
|
||||||
|
|
||||||
|
| Rolle | Zweck | Beispielmodelle | Aufgerufen von |
|
||||||
|
|-------|-------|-----------------|----------------|
|
||||||
|
| Text / Reasoning | Agent-Logik, Planung, Clarification, Copywriting | Claude 3.5 Sonnet, GPT-4o | Agent Node |
|
||||||
|
| Image Generation | Bildgenerierung auf dem Canvas | Gemini 2.5 Flash Image, Flux.1 Pro, GPT-5 Image | Canvas-Aktionen + Agent Node |
|
||||||
|
|
||||||
|
### OpenRouter — Image Generation
|
||||||
|
|
||||||
|
| Modell | OpenRouter ID | Stärke | ~Kosten/Bild |
|
||||||
|
|--------|---------------|--------|--------------|
|
||||||
|
| Gemini 2.5 Flash Image | google/gemini-2.5-flash-image | Multi-Turn Editing, günstig | ~€0,02–0,04 |
|
||||||
|
| FLUX.2 Klein 4B | black-forest-labs/flux.2-klein-4b | Photorealismus, schnellstes Flux | ~€0,01–0,03 |
|
||||||
|
| Seedream 4.5 | bytedance-seed/seedream-4.5 | Editing-Konsistenz, Portraits | ~€0,04 |
|
||||||
|
| Gemini 3.1 Flash Image | google/gemini-3.1-flash-image-preview | Pro-Qualität bei Flash-Speed | ~€0,04–0,08 |
|
||||||
|
| GPT-5 Image Mini | openai/gpt-5-image-mini | Gutes Preis-Leistungs-Verhältnis | ~€0,04–0,08 |
|
||||||
|
| Riverflow V2 Fast | sourceful/riverflow-v2-fast | Custom Font Rendering, schnell | ~€0,02 |
|
||||||
|
| Riverflow V2 Pro | sourceful/riverflow-v2-pro | Text-Rendering, 4K Output | ~€0,15–0,33 |
|
||||||
|
| Gemini 3 Pro Image | google/gemini-3-pro-image-preview | Multi-Image, 4K, bestes Text-Rendering | ~€0,08–0,15 |
|
||||||
|
| GPT-5 Image | openai/gpt-5-image | Instruction Following, Text in Bild | ~€0,10–0,20 |
|
||||||
|
|
||||||
|
### Self-hosted Services
|
||||||
|
|
||||||
|
| Service | Funktion | Credits |
|
||||||
|
|---------|----------|---------|
|
||||||
|
| rembg | Hintergrundentfernung | Kostenlos |
|
||||||
|
| Real-ESRGAN | Upscaling (2×, 4×, 8×) | Kostenlos |
|
||||||
|
| GFPGAN | Face Restoration | Kostenlos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. High-Level Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ Next.js Frontend │
|
||||||
|
│ Infinite Canvas (@xyflow/react + dnd-kit) │
|
||||||
|
│ │
|
||||||
|
│ Node-Kategorien: │
|
||||||
|
│ [Quelle] [KI-Ausgabe] [Transformation] │
|
||||||
|
│ [Steuerung] [Canvas & Layout] │
|
||||||
|
└───────────────────────┬──────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────▼─────────┐
|
||||||
|
│ Convex Backend │
|
||||||
|
│ (Self-hosted) │
|
||||||
|
│ - Realtime Sync │
|
||||||
|
│ - File Storage │
|
||||||
|
│ - Auth │
|
||||||
|
│ - Modell-Router │
|
||||||
|
│ - Agent Executor │
|
||||||
|
└──┬────────┬───┬───┘
|
||||||
|
│ │ │
|
||||||
|
┌────────────▼──┐ ┌───▼──────────────┐ ┌──▼──────────┐
|
||||||
|
│ OpenRouter │ │ Self-hosted KI │ │ Freepik API │
|
||||||
|
│ Image Gen + │ │ rembg / ESRGAN │ │ (Assets) │
|
||||||
|
│ Text/Reason │ │ GFPGAN │ └─────────────┘
|
||||||
|
└───────────────┘ └──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Datenmodell (High-Level)
|
||||||
|
|
||||||
|
### Canvas & Node
|
||||||
|
|
||||||
|
```
|
||||||
|
Canvas
|
||||||
|
├── id, name, ownerId, createdAt / updatedAt
|
||||||
|
└── nodes[]
|
||||||
|
|
||||||
|
Node (Basis)
|
||||||
|
├── id, canvasId
|
||||||
|
├── type (image | text | prompt | color | video | asset |
|
||||||
|
│ ai-image | ai-text | ai-video | agent-output |
|
||||||
|
│ crop | bg-remove | upscale | style-transfer | face-restore |
|
||||||
|
│ splitter | loop | agent | mixer | switch |
|
||||||
|
│ group | frame | note | text-overlay | compare | comment | presentation)
|
||||||
|
├── position { x, y }
|
||||||
|
├── size { width, height }
|
||||||
|
├── data (je nach Typ)
|
||||||
|
└── createdAt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Credit-System
|
||||||
|
|
||||||
|
```
|
||||||
|
CreditBalance
|
||||||
|
├── id, userId
|
||||||
|
├── balance // tatsächlich verfügbare Credits (Euro-Cent)
|
||||||
|
├── reserved // aktuell gesperrte Credits (laufende Jobs)
|
||||||
|
├── available // computed: balance - reserved
|
||||||
|
├── monthlyAllocation // Credits aus dem Abo
|
||||||
|
└── updatedAt
|
||||||
|
|
||||||
|
CreditTransaction
|
||||||
|
├── id, userId
|
||||||
|
├── amount // positiv = Gutschrift, negativ = Verbrauch
|
||||||
|
├── type // subscription | topup | usage | reservation | refund
|
||||||
|
├── status // committed | reserved | released | failed
|
||||||
|
├── description // z.B. "Bildgenerierung – Gemini 2.5 Flash Image"
|
||||||
|
├── nodeId? // Referenz auf den auslösenden Node
|
||||||
|
├── openRouterCost? // tatsächliche OpenRouter-Kosten (gecacht)
|
||||||
|
└── createdAt
|
||||||
|
|
||||||
|
Subscription
|
||||||
|
├── id, userId
|
||||||
|
├── tier // free | starter | pro | business
|
||||||
|
├── status // active | cancelled | past_due
|
||||||
|
├── currentPeriodStart / currentPeriodEnd
|
||||||
|
└── lemonSqueezySubscriptionId?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Pricing & Credit-System
|
||||||
|
|
||||||
|
### Abo-Stufen
|
||||||
|
|
||||||
|
| Tier | Preis/Monat | Marge (30%) | Credits (70%) | Credits gesamt | Zielgruppe |
|
||||||
|
|------|-------------|-------------|---------------|----------------|------------|
|
||||||
|
| Free | €0 | — | — | €0,50 (Geschenk) | Testen & Evaluieren |
|
||||||
|
| Starter | €9 | €2,70 | €6,30 | €6,30 | Einzelnutzer |
|
||||||
|
| Pro | €49 | €12,98 | €34,30 | €36,02 (+5%) | Aktive Creator |
|
||||||
|
| Business | €99 | €22,77 | €69,30 | €76,23 (+10%) | Teams, hoher Durchsatz |
|
||||||
|
|
||||||
|
### Credit Reservation + Commit (Option C)
|
||||||
|
|
||||||
|
Credits werden vor jedem KI-Call reserviert und erst nach erfolgreichem Abschluss committed. Bei Fehler werden reservierte Credits automatisch freigegeben — kein manueller Refund-Prozess nötig.
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. RESERVE → CreditTransaction (type: reservation, status: reserved)
|
||||||
|
CreditBalance.reserved += estimated_cost
|
||||||
|
CreditBalance.available = balance - reserved
|
||||||
|
|
||||||
|
2a. SUCCESS → Transaction status: committed
|
||||||
|
CreditBalance.balance -= actual_cost
|
||||||
|
CreditBalance.reserved -= estimated_cost
|
||||||
|
|
||||||
|
2b. FAILURE → Transaction status: released
|
||||||
|
CreditBalance.reserved -= estimated_cost
|
||||||
|
(balance bleibt unverändert — voller Refund)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Preisbasis:** Credits werden auf Basis der gecachten OpenRouter-Preise (Redis, TTL ~10 Minuten) reserviert und abgebucht. Minimale Abweichungen zum tatsächlichen API-Preis werden bewusst akzeptiert. Keine nachträgliche Korrektur.
|
||||||
|
|
||||||
|
### Agent Partial Failure
|
||||||
|
|
||||||
|
Bei Agent-Workflows läuft Reservation + Commit pro Suboperation. Schlägt Step 3 von 5 fehl: Steps 1+2 sind committed, Step 3 wird released, Steps 4+5 werden nicht mehr reserviert. Nur tatsächlich verbrauchte Credits werden berechnet.
|
||||||
|
|
||||||
|
### Credit-Verbrauch
|
||||||
|
|
||||||
|
| Operation | Modell / Service | Ungefähre Kosten |
|
||||||
|
|-----------|------------------|------------------|
|
||||||
|
| Bildgenerierung (Budget) | FLUX.2 Klein 4B | ~€0,01–0,03 |
|
||||||
|
| Bildgenerierung (Standard) | Gemini 2.5 Flash Image | ~€0,02–0,04 |
|
||||||
|
| Bildgenerierung (Premium) | GPT-5 Image / Nano Banana Pro | ~€0,10–0,20 |
|
||||||
|
| Agent Reasoning Call | Claude 3.5 Sonnet | ~€0,01–0,05 |
|
||||||
|
| BG-Entfernung | rembg (self-hosted) | Kostenlos |
|
||||||
|
| Upscaling | Real-ESRGAN (self-hosted) | Kostenlos |
|
||||||
|
| Face Restoration | GFPGAN (self-hosted) | Kostenlos |
|
||||||
|
| Canvas-Operationen | — | Kostenlos |
|
||||||
|
| Text-Overlay | — | Kostenlos |
|
||||||
|
| Export-Funktionen | — | Kostenlos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. UX-Strategie für Latenzen
|
||||||
|
|
||||||
|
KI-Operationen haben inhärente Wartezeiten. Einzelne Bildgenerierungen dauern 3–15 Sekunden, Agent-Workflows 20–60+ Sekunden. Die UI überbrückt diese Wartezeiten durch optimistische Darstellung direkt am Node — kein globales Loading-Banner, kein blockierendes Modal.
|
||||||
|
|
||||||
|
### Node-Status-Modell
|
||||||
|
|
||||||
|
Jeder ausführende Node zeigt seinen Zustand visuell direkt auf dem Canvas:
|
||||||
|
|
||||||
|
```
|
||||||
|
idle → analyzing → clarifying → executing (Step X/N) → done | error
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent Nodes zeigen zusätzlich den Step-Progress während der Execution ("Generating Feed Post 2/3"). Bei Fehler wechselt der Node in einen Error-State mit kurzem Hinweis direkt am Node ("Timeout — Credits wurden nicht abgebucht").
|
||||||
|
|
||||||
|
### Skeleton Nodes
|
||||||
|
|
||||||
|
Sobald der Agent seinen Execution-Plan (JSON) erstellt hat, kennt das System Anzahl und Typ aller Output-Nodes. Ab diesem Moment werden Skeleton-Nodes auf dem Canvas platziert — noch bevor ein einziger API-Call für die Generierung läuft.
|
||||||
|
|
||||||
|
```
|
||||||
|
Agent Status: analyzing
|
||||||
|
→ Plan fertig: 3x KI-Bild, 2x KI-Text, 1x Text-Overlay
|
||||||
|
→ 6 Skeleton-Nodes erscheinen auf dem Canvas, korrekt positioniert
|
||||||
|
→ Agent Status: executing (1/6)
|
||||||
|
→ Skeletons füllen sich der Reihe nach mit echten Outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
- Skeleton-Nodes sind bereits verschiebbar und arrangierbar bevor der Output fertig ist
|
||||||
|
- Sobald der Output fertig ist, ersetzt er den Skeleton in-place — Position bleibt erhalten
|
||||||
|
- Visuell: gedimmter Node-Rahmen mit Shimmer-Effekt, Node-Typ-Icon sichtbar (Bild vs. Text)
|
||||||
|
|
||||||
|
### Browser Notifications (Tab-Wechsel)
|
||||||
|
|
||||||
|
- Opt-in Browser Notifications API: wenn der Nutzer den Tab verlässt und der Job fertig wird, native Browser-Benachrichtigung
|
||||||
|
- Nicht erzwungen — Nutzer die im Tab bleiben sehen den Node-Status direkt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Entwicklungsphasen
|
||||||
|
|
||||||
|
### Phase 1 — Foundation (MVP)
|
||||||
|
|
||||||
|
**Nodes:**
|
||||||
|
- Quelle: Bild, Text
|
||||||
|
- KI-Ausgabe: KI-Bild
|
||||||
|
- Canvas & Layout: Gruppe, Frame, Notiz
|
||||||
|
|
||||||
|
**Infrastruktur & Features:**
|
||||||
|
|
||||||
|
| Task | Status |
|
||||||
|
|------|--------|
|
||||||
|
| Projektsetup: Next.js 16 + Tailwind v4 + ShadCN | ☐ Offen |
|
||||||
|
| Zwei Repos aufsetzen (`lemonspace-web` für app.lemonspace.io, `lemonspace-landing` für lemonspace.io) | ☐ Offen |
|
||||||
|
| Convex Self-hosted Backend aufsetzen | ☐ Offen |
|
||||||
|
| Basis-Canvas mit @xyflow/react | ☐ Offen |
|
||||||
|
| Drag & Drop von Bildern via dnd-kit | ☐ Offen |
|
||||||
|
| Authentifizierung via Better Auth | ☐ Offen |
|
||||||
|
| OpenRouter Integration (Image Gen, Gemini 2.5 Flash Image) | ☐ Offen |
|
||||||
|
| Credit-System: Balance-Tracking, Reservation+Commit, Kosten-Voranzeige | ☐ Offen |
|
||||||
|
| Abo-Verwaltung: Free/Starter/Pro/Business Tiers, monatliche Credit-Zuweisung | ☐ Offen |
|
||||||
|
| Lemon Squeezy Integration: Checkout, Webhooks, Credit-Zuweisung | ☐ Offen |
|
||||||
|
| Credit-Nachkauf (Top-Up) zum Selbstkostenpreis | ☐ Offen |
|
||||||
|
| Node-Status-Modell (idle/executing/done/error) direkt am Node | ☐ Offen |
|
||||||
|
| docker-compose.yml + .env.example + Setup-README | ☐ Offen |
|
||||||
|
|
||||||
|
### Phase 2 — KI-Features
|
||||||
|
|
||||||
|
**Nodes:**
|
||||||
|
- Quelle: Prompt, Farbe / Palette, Video, Asset
|
||||||
|
- KI-Ausgabe: KI-Text, KI-Video
|
||||||
|
- Transformation: Crop / Resize, BG entfernen, Upscale
|
||||||
|
- Steuerung: Splitter, Loop, Agent
|
||||||
|
- Canvas & Layout: Text-Overlay, Compare
|
||||||
|
|
||||||
|
**Infrastruktur & Features:**
|
||||||
|
|
||||||
|
| Task | Status |
|
||||||
|
|------|--------|
|
||||||
|
| Vollständige OpenRouter Image Gen Integration (alle 9 Modelle) | ☐ Offen |
|
||||||
|
| Experten-Modus: Modellauswahl-UI im Canvas AI Panel | ☐ Offen |
|
||||||
|
| OpenRouter Text/Reasoning Integration (Claude 3.5 Sonnet) | ☐ Offen |
|
||||||
|
| Agent Node: Analyse, Clarification, Execution, Output | ☐ Offen |
|
||||||
|
| Skeleton-Nodes: Platzierung nach Plan-Erstellung, sequenzielle Befüllung | ☐ Offen |
|
||||||
|
| Browser Notifications API (opt-in, Tab-Wechsel) | ☐ Offen |
|
||||||
|
| Erster Agent Template: Instagram Curator | ☐ Offen |
|
||||||
|
| Self-hosted KI-Services (rembg, Real-ESRGAN) | ☐ Offen |
|
||||||
|
| Freepik Asset Browser (Stock-Fotos, Vektoren) | ☐ Offen |
|
||||||
|
| Prompt-History und Re-Generation | ☐ Offen |
|
||||||
|
|
||||||
|
### Phase 3 — Kollaboration & Polish
|
||||||
|
|
||||||
|
**Nodes:**
|
||||||
|
- Transformation: Style Transfer, Gesicht (GFPGAN)
|
||||||
|
- Steuerung: Mixer, Weiche
|
||||||
|
- Canvas & Layout: Kommentar, Präsentation
|
||||||
|
|
||||||
|
**Infrastruktur & Features:**
|
||||||
|
|
||||||
|
| Task | Status |
|
||||||
|
|------|--------|
|
||||||
|
| Echtzeit-Kollaboration via Convex Subscriptions | ☐ Offen |
|
||||||
|
| Kommentar- und Annotations-System | ☐ Offen |
|
||||||
|
| Versions-History | ☐ Offen |
|
||||||
|
| Weitere Agent Templates | ☐ Offen |
|
||||||
|
| Export-Funktionen (PNG, PDF, ZIP) | ☐ Offen |
|
||||||
|
| Performance-Optimierung für große Canvases | ☐ Offen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Offene Entscheidungen
|
||||||
|
|
||||||
|
| Thema | Entscheidung / Status |
|
||||||
|
|-------|----------------------|
|
||||||
|
| Authentifizierung | ✅ Better Auth (self-hosted, open-source) |
|
||||||
|
| Tailwind v4 | ✅ v4 ist Standard, keine Migration nötig |
|
||||||
|
| Pricing / Credit-System | ✅ 4-Tier Abo + Credit-System, 30% Marge, Reservation+Commit |
|
||||||
|
| Payment Provider | ✅ Lemon Squeezy (Merchant of Record, VAT-Handling) |
|
||||||
|
| Self-Hosting-Strategie | ✅ docker-compose.yml + .env.example + README, für technisch versierte Nutzer |
|
||||||
|
| Convex Lock-in | ✅ Bewusst akzeptiert; Migrations-Pfad: Convex Cloud EU |
|
||||||
|
| OpenRouter Image-Modelle | ✅ 9 Modelle definiert, alle Tiers haben Zugriff |
|
||||||
|
| Lizenz | ✅ BSL 1.1, 3 Jahre Change Date, Apache 2.0, nur private Nutzung frei |
|
||||||
|
| Repo-Strategie | ✅ Zwei unabhängige Repos (lemonspace-web + lemonspace-landing), Auth-Cookie-Sharing via .lemonspace.io |
|
||||||
|
| Job Queue | ✅ Convex native (Phase 1), externe Lösung bei Bedarf |
|
||||||
|
| E-Mail | ✅ Unsend + Stalwart, self-hosted |
|
||||||
|
| Analytics | ✅ Rybbit, self-hosted |
|
||||||
|
| Error Tracking | ✅ Sentry Cloud (Free Tier) |
|
||||||
|
| Cache-Strategie | ✅ Cloudflare (Edge) + Redis (Application, TTL ~10min für OpenRouter-Preise) |
|
||||||
|
| E2E-Testing | ✅ Kein E2E in Phase 1, Neubewertung bei Skalierung |
|
||||||
|
| UX-Latenzen | ✅ Node-Status-Modell, Skeleton-Nodes, Browser Notifications (opt-in) |
|
||||||
|
| Credit Fehlerbehandlung | ✅ Reservation + Commit, gecachte Preise, kein nachträglicher Ausgleich |
|
||||||
|
| Kollaborationstiefe | ⏳ Cursor-Sync, gleichzeitige Edits, Kommentare |
|
||||||
|
| Agent Clarification UX | ⏳ Inline am Node vs. Modal vs. Chat-Sidebar |
|
||||||
|
| Agent Template Format | ⏳ Markdown-Datei vs. strukturiertes JSON-Schema |
|
||||||
|
| Weiche: Bedingungslogik | ⏳ Visueller Rule-Builder vs. Ausdruckssprache |
|
||||||
|
| Mixer: Blend Modes | ⏳ min. Normal, Multiply, Screen, Overlay |
|
||||||
|
| Canvas-Export | ⏳ PNG, PDF, ZIP (Phase 3, Library TBD) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Nicht-funktionale Anforderungen
|
||||||
|
|
||||||
|
| Anforderung | Beschreibung |
|
||||||
|
|-------------|--------------|
|
||||||
|
| Self-hostable | Vollständiger Betrieb auf eigenem VPS möglich; docker-compose.yml als primäres Deployment-Artefakt |
|
||||||
|
| Source Available | BSL 1.1 — Quellcode öffentlich, kommerzielle Nutzung lizenzpflichtig (siehe Abschnitt 15) |
|
||||||
|
| Performance | Canvas mit 100+ Nodes ohne spürbare Verzögerung |
|
||||||
|
| Datenschutz | Keine externen Tracking-Dienste; Ausnahme: Sentry Cloud für Error Tracking |
|
||||||
|
| Skalierbarkeit | Convex-Backend skaliert mit wachsender Nutzerzahl; Migrations-Pfad: Convex Cloud EU |
|
||||||
|
| Sicherheit | Rate Limiting auf allen API-Endpunkten via Redis, DDoS-Schutz via Cloudflare |
|
||||||
|
| UX-Resilienz | Alle KI-Operationen zeigen Status direkt am Node; Skeleton-Nodes bei Agent-Workflows |
|
||||||
|
| Credit-Integrität | Reservation+Commit-Mechanismus verhindert Credit-Verlust bei fehlgeschlagenen API-Calls |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Nächste Schritte
|
||||||
|
|
||||||
|
1. Zwei Repos aufsetzen (`lemonspace-web` mit Next.js 16 + Tailwind v4 + ShadCN + Better Auth + Convex, `lemonspace-landing` mit Next.js 16 + Tailwind v4 + ShadCN)
|
||||||
|
2. Convex Schema: Detailliertes Datenbankschema entwerfen (Node-Taxonomie + Credit-System inkl. CreditBalance.reserved/available)
|
||||||
|
3. UI/UX Wireframes: Canvas-Interface, Node-Status-Modell, Skeleton-Nodes, Agent Clarification-UX skizzieren
|
||||||
|
4. API-Prototyp: OpenRouter Anbindung testen — Image Gen (Gemini 2.5 Flash Image) und Text/Reasoning (Claude 3.5 Sonnet)
|
||||||
|
5. Lemon Squeezy Integration: Abo-Tiers anlegen, Webhook-Handling für Subscription-Events und Credit-Zuweisung
|
||||||
|
6. docker-compose.yml + .env.example + Setup-README ausarbeiten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Lizenzmodell
|
||||||
|
|
||||||
|
Die Software wird unter der Business Source License 1.1 (BSL 1.1) veröffentlicht. Der vollständige Quellcode ist öffentlich einsehbar, auditierbar und für private/persönliche Nutzung kostenlos. Kommerzielle Nutzung erfordert eine separate Lizenzvereinbarung.
|
||||||
|
|
||||||
|
### Parameter
|
||||||
|
|
||||||
|
| Parameter | Wert |
|
||||||
|
|-----------|------|
|
||||||
|
| Lizenz | Business Source License 1.1 |
|
||||||
|
| Change Date | 3 Jahre nach Veröffentlichung jedes Releases |
|
||||||
|
| Change License | Apache License 2.0 |
|
||||||
|
| Additional Use Grant | Nutzung ausschließlich für private und persönliche, nicht-kommerzielle Zwecke |
|
||||||
|
|
||||||
|
### Kommerzielle Lizenzen (geplant)
|
||||||
|
|
||||||
|
| Lizenz | Zielgruppe | Details |
|
||||||
|
|--------|------------|---------|
|
||||||
|
| Small Business | Unternehmen ≤ 10 Mitarbeiter | Preis und Konditionen TBD |
|
||||||
|
| Enterprise | Unternehmen > 10 Mitarbeiter | Preis und Konditionen TBD |
|
||||||
|
| OEM / Reseller | Einbettung in Drittprodukte | Individuelle Vereinbarung |
|
||||||
|
|
||||||
|
> **Positionierung:** LemonSpace wird als „Source Available" bzw. „Fair Source" positioniert — nicht als „Open Source" im Sinne der OSI-Definition. Der Quellcode ist vollständig öffentlich und transparent; Nutzungsrechte sind eingeschränkt bis zum Erreichen des Change Date.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*LemonSpace PRD v1.1 — März 2026*
|
||||||
111
.docs/LemonSpace_Phase1_TODO.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# 🍋 LemonSpace — Phase 1 MVP TODO
|
||||||
|
|
||||||
|
## 1. Projekt-Setup & Infrastruktur
|
||||||
|
|
||||||
|
- [ ] `lemonspace-web` Repo scaffolden (Next.js 16 + Tailwind v4 + ShadCN + Convex + BetterAuth)
|
||||||
|
- [ ] `lemonspace-landing` Repo scaffolden (Next.js 16 + Tailwind v4 + ShadCN)
|
||||||
|
- [ ] Auth-Cookie-Sharing: BetterAuth Cookie auf `.lemonspace.io` setzen, Landing Page liest Login-State
|
||||||
|
- [ ] Convex Self-hosted Backend aufsetzen (via Coolify)
|
||||||
|
- [ ] Redis aufsetzen (via Coolify)
|
||||||
|
- [ ] Sentry Cloud anbinden (Free Tier)
|
||||||
|
- [ ] Cloudflare DNS + DDoS-Schutz konfigurieren
|
||||||
|
- [ ] Rybbit Analytics deployen (via Coolify)
|
||||||
|
- [ ] useSend + Stalwart E-Mail-Stack deployen (via Coolify)
|
||||||
|
|
||||||
|
## 2. Authentifizierung
|
||||||
|
|
||||||
|
- [ ] Better Auth integrieren (Self-hosted)
|
||||||
|
- [ ] Login / Signup Flow
|
||||||
|
- [ ] E-Mail-Verifizierung (via useSend)
|
||||||
|
- [ ] Session-Management
|
||||||
|
|
||||||
|
## 3. Canvas — Kernfunktion
|
||||||
|
|
||||||
|
- [ ] Basis-Canvas mit @xyflow/react
|
||||||
|
- [ ] Zoom, Pan, Navigation
|
||||||
|
- [ ] Drag & Drop von Bildern via dnd-kit
|
||||||
|
- [ ] Node-Rendering-System (typisierte Bausteine)
|
||||||
|
- [ ] Node-Verbindungen (Edges) zwischen kompatiblen Nodes
|
||||||
|
- [ ] Gruppierung und Layering von Canvas-Elementen
|
||||||
|
|
||||||
|
## 4. Phase-1-Nodes
|
||||||
|
|
||||||
|
### Quelle
|
||||||
|
- [ ] **Bild-Node** — Upload (PNG, JPG, WebP) + URL-Einbindung
|
||||||
|
- [ ] **Text-Node** — Freitextfeld mit Markdown-Support
|
||||||
|
- [ ] **Prompt-Node** — Dedizierter Node für Modellinstruktionen, verbindet sich mit KI-Nodes
|
||||||
|
|
||||||
|
### KI-Ausgabe
|
||||||
|
- [ ] **KI-Bild-Node** — Output eines Bildgenerierungs-Calls, speichert Prompt, Modell, Parameter
|
||||||
|
|
||||||
|
### Canvas & Layout
|
||||||
|
- [ ] **Gruppe-Node** — Container, Collapse/Expand, benannte Scopes
|
||||||
|
- [ ] **Frame-Node** — Artboard mit definierter Auflösung, Export-Boundary
|
||||||
|
- [ ] **Notiz-Node** — Annotation, Markdown-Support, kein Datenanschluss
|
||||||
|
- [ ] **Compare-Node** — Zwei Bilder nebeneinander mit interaktivem Slider
|
||||||
|
|
||||||
|
## 5. KI-Integration
|
||||||
|
|
||||||
|
- [ ] OpenRouter-Anbindung (Image Generation)
|
||||||
|
- [ ] Initiales Modell: Gemini 2.5 Flash Image
|
||||||
|
- [ ] Modellauswahl-UI (mindestens Budget/Standard/Premium)
|
||||||
|
- [ ] Prompt → KI-Bild-Generierung End-to-End im Canvas
|
||||||
|
- [ ] Node-Status-Modell implementieren (`idle → executing → done | error`)
|
||||||
|
- [ ] Error-State direkt am Node mit Hinweistext
|
||||||
|
|
||||||
|
## 6. Credit-System
|
||||||
|
|
||||||
|
- [ ] Convex Schema: `CreditBalance` (balance, reserved, available, monthlyAllocation)
|
||||||
|
- [ ] Convex Schema: `CreditTransaction` (amount, type, status, nodeId, openRouterCost)
|
||||||
|
- [ ] Convex Schema: `Subscription` (tier, status, periodStart/End, lemonSqueezyId)
|
||||||
|
- [ ] Reservation + Commit Flow implementieren
|
||||||
|
- [ ] Kosten-Voranzeige vor Generierung
|
||||||
|
- [ ] OpenRouter-Preise cachen (Redis, TTL ~10min)
|
||||||
|
- [ ] Credit-Balance-Anzeige in der UI
|
||||||
|
|
||||||
|
## 7. Pricing & Payment
|
||||||
|
|
||||||
|
- [ ] Lemon Squeezy Integration: Checkout-Flow
|
||||||
|
- [ ] Webhook-Handling für Subscription-Events
|
||||||
|
- [ ] Automatische Credit-Zuweisung bei Abo-Start / Abo-Verlängerung
|
||||||
|
- [ ] 4 Tiers anlegen: Free (€0,50) / Starter €9 / Pro €49 / Business €99
|
||||||
|
- [ ] Credit-Nachkauf (Top-Up) zum Selbstkostenpreis
|
||||||
|
|
||||||
|
## 8. Abuse Prevention
|
||||||
|
|
||||||
|
- [ ] Daily Generation Caps (Free: 10, Starter: 50, Pro: 200, Business: 500)
|
||||||
|
- [ ] Concurrency Limits (Free: 1, Paid: 2 parallele Generierungen)
|
||||||
|
- [ ] Rate Limiting auf API-Endpunkten (Redis-backed)
|
||||||
|
- [ ] Premium-Modelle erst ab Starter-Tier
|
||||||
|
- [ ] Top-Up-Limit pro Monat
|
||||||
|
|
||||||
|
## 9. Export
|
||||||
|
|
||||||
|
- [ ] PNG-Export aus Frame-Nodes
|
||||||
|
- [ ] ZIP-Export (mehrere Frames / Varianten)
|
||||||
|
|
||||||
|
## 10. Convex Schema (Gesamtübersicht)
|
||||||
|
|
||||||
|
- [ ] `Canvas` — id, name, ownerId, createdAt, updatedAt
|
||||||
|
- [ ] `Node` — id, canvasId, type, position, size, data, createdAt
|
||||||
|
- [ ] `Edge` — id, canvasId, sourceNodeId, targetNodeId
|
||||||
|
- [ ] `CreditBalance` — siehe Credit-System
|
||||||
|
- [ ] `CreditTransaction` — siehe Credit-System
|
||||||
|
- [ ] `Subscription` — siehe Credit-System
|
||||||
|
- [ ] `User` — id, email, name, avatarUrl, createdAt
|
||||||
|
|
||||||
|
## 11. Nicht Phase 1 (bewusst ausgeklammert)
|
||||||
|
|
||||||
|
- Echtzeit-Kollaboration
|
||||||
|
- Agent Nodes
|
||||||
|
- Video-Generierung
|
||||||
|
- Freepik Asset Browser
|
||||||
|
- Style Transfer / GFPGAN / rembg / Real-ESRGAN
|
||||||
|
- Team-Features (Workspaces, Rollen, Rechte)
|
||||||
|
- docker-compose.yml für Self-Hosting
|
||||||
|
- E2E-Testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Reihenfolge orientiert sich an den Abhängigkeiten aus dem Manifest v1.2:*
|
||||||
|
*Repos scaffolden → Convex Schema → Canvas → OpenRouter → Compare + Export → Auth + Credits → Lemon Squeezy*
|
||||||
22
CLAUDE.md
@@ -1 +1,23 @@
|
|||||||
@AGENTS.md
|
@AGENTS.md
|
||||||
|
|
||||||
|
## Design Context
|
||||||
|
|
||||||
|
### Users
|
||||||
|
Kleine Design- und Marketing-Teams (2–10 Personen), die aus wenigen Input-Assets schnell kampagnenfähige Bildvarianten erzeugen. Sie arbeiten unter Zeitdruck, brauchen Überblick über Credits und laufende Generierungen, und wollen sich auf die kreative Arbeit konzentrieren — nicht auf das Tool.
|
||||||
|
|
||||||
|
### Brand Personality
|
||||||
|
Frisch, klar, einladend. Wie eine Zitrone: hell, sauber, natürlich. Vertrauen durch Klarheit, nicht durch Komplexität. Drei Worte: **frisch · klar · menschlich**.
|
||||||
|
|
||||||
|
### Aesthetic Direction
|
||||||
|
- **Tone**: Warm & Organic — weiche Formen, warme Töne, einladend und menschlich
|
||||||
|
- **Light/Dark**: Beides (system auto), aber Light als primäre Designrichtung
|
||||||
|
- **Colors**: Teal-Primary mit warmen Neutraltönen (beige/sand statt kaltem Grau). Zitronengelb als Akzent.
|
||||||
|
- **Anti-references**: Kalte Dashboard-Ästhetik, Glassmorphism, Neon-auf-Dunkel, generische SaaS-Templates
|
||||||
|
- **References**: Notion (Klarheit), Linear (Präzision), Craft (Wärme), Things (Menschlichkeit)
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
1. **Klarheit vor Dekoration** — Jedes Element muss einen Zweck erfüllen. Keine schmückenden Karten oder Sparklines.
|
||||||
|
2. **Warme Neutralität** — Neutraltöne immer Richtung Warm (Sand/Beige) tönen, niemals kaltes Grau.
|
||||||
|
3. **Asymmetrische Balance** — Nicht alles zentrieren. Linksbündiger Text, variierte Abstände, bewusste visuelle Hierarchie.
|
||||||
|
4. **Progressive Disclosure** — Wichtigstes zuerst, Details bei Bedarf. Nicht alles gleichzeitig zeigen.
|
||||||
|
5. **Natürliches Tempo** — Sanfte Übergänge mit exponentiellem Easing. Bewegung soll sich organisch anfühlen.
|
||||||
|
|||||||
3
app/api/auth/[...all]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { handler } from "@/lib/auth-server";
|
||||||
|
|
||||||
|
export const { GET, POST } = handler;
|
||||||
349
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import Image from "next/image"
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
ArrowUpRight,
|
||||||
|
ChevronDown,
|
||||||
|
Coins,
|
||||||
|
LayoutTemplate,
|
||||||
|
Search,
|
||||||
|
Sparkles,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const formatEurFromCents = (cents: number) =>
|
||||||
|
new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(cents / 100)
|
||||||
|
|
||||||
|
const mockRuns = [
|
||||||
|
{
|
||||||
|
id: "run-8841",
|
||||||
|
workspace: "Sommer-Kampagne",
|
||||||
|
node: "KI-Bild",
|
||||||
|
model: "flux-pro",
|
||||||
|
status: "done" as const,
|
||||||
|
credits: 42,
|
||||||
|
updated: "vor 12 Min.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "run-8839",
|
||||||
|
workspace: "Produktfotos",
|
||||||
|
node: "KI-Bild",
|
||||||
|
model: "flux-schnell",
|
||||||
|
status: "executing" as const,
|
||||||
|
credits: 18,
|
||||||
|
updated: "gerade eben",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "run-8832",
|
||||||
|
workspace: "Social Variants",
|
||||||
|
node: "Compare",
|
||||||
|
model: "—",
|
||||||
|
status: "idle" as const,
|
||||||
|
credits: 0,
|
||||||
|
updated: "vor 1 Std.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "run-8828",
|
||||||
|
workspace: "Sommer-Kampagne",
|
||||||
|
node: "KI-Bild",
|
||||||
|
model: "flux-pro",
|
||||||
|
status: "error" as const,
|
||||||
|
credits: 0,
|
||||||
|
updated: "vor 2 Std.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockWorkspaces = [
|
||||||
|
{ name: "Sommer-Kampagne", nodes: 24, frames: 3, initial: "S" },
|
||||||
|
{ name: "Produktfotos", nodes: 11, frames: 2, initial: "P" },
|
||||||
|
{ name: "Social Variants", nodes: 8, frames: 1, initial: "V" },
|
||||||
|
]
|
||||||
|
|
||||||
|
function StatusDot({ status }: { status: (typeof mockRuns)[0]["status"] }) {
|
||||||
|
const base = "inline-block size-2 rounded-full"
|
||||||
|
switch (status) {
|
||||||
|
case "done":
|
||||||
|
return <span className={cn(base, "bg-primary")} />
|
||||||
|
case "executing":
|
||||||
|
return (
|
||||||
|
<span className="relative inline-flex size-2">
|
||||||
|
<span className="absolute inline-flex size-full animate-ping rounded-full bg-primary/60" />
|
||||||
|
<span className={cn(base, "relative bg-primary")} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case "idle":
|
||||||
|
return <span className={cn(base, "bg-border")} />
|
||||||
|
case "error":
|
||||||
|
return <span className={cn(base, "bg-destructive")} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: (typeof mockRuns)[0]["status"]) {
|
||||||
|
switch (status) {
|
||||||
|
case "done":
|
||||||
|
return "Fertig"
|
||||||
|
case "executing":
|
||||||
|
return "Läuft"
|
||||||
|
case "idle":
|
||||||
|
return "Bereit"
|
||||||
|
case "error":
|
||||||
|
return "Fehler"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
const balanceCents = 4320
|
||||||
|
const reservedCents = 180
|
||||||
|
const monthlyPoolCents = 5000
|
||||||
|
const usagePercent = Math.round(
|
||||||
|
((monthlyPoolCents - balanceCents) / monthlyPoolCents) * 100,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-full bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="sticky top-0 z-10 border-b bg-background/90 backdrop-blur-sm">
|
||||||
|
<div className="mx-auto flex h-14 max-w-5xl items-center gap-4 px-6">
|
||||||
|
<div className="flex items-center gap-2.5 text-base font-semibold tracking-tight">
|
||||||
|
<Image
|
||||||
|
src="/logos/lemonspace-logo-v2-primary-rgb.svg"
|
||||||
|
alt=""
|
||||||
|
width={449}
|
||||||
|
height={86}
|
||||||
|
unoptimized
|
||||||
|
className="h-5 w-auto shrink-0"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative ml-8 hidden max-w-xs flex-1 sm:block">
|
||||||
|
<Search className="pointer-events-none absolute top-1/2 left-3 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
className="h-8 rounded-lg bg-muted/60 pl-8 text-sm"
|
||||||
|
placeholder="Suchen…"
|
||||||
|
type="search"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-auto flex items-center gap-3">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="gap-2 px-1.5">
|
||||||
|
<Avatar className="size-7">
|
||||||
|
<AvatarFallback className="bg-primary/12 text-xs font-medium text-primary">
|
||||||
|
MK
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<span className="hidden text-sm font-medium md:inline">
|
||||||
|
Mock Nutzer
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="size-3.5 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
|
<DropdownMenuLabel>Account (Demo)</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem disabled>Einstellungen</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>Abrechnung</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem disabled>Abmelden</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="mx-auto max-w-5xl px-6 pt-10 pb-16">
|
||||||
|
{/* Greeting & Context */}
|
||||||
|
<div className="mb-10">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Guten Tag, Mock Nutzer
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1.5 text-muted-foreground">
|
||||||
|
Überblick über deine Credits und laufende Generierungen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Credits & Active Generation — asymmetric two-column */}
|
||||||
|
<div className="mb-12 grid gap-6 lg:grid-cols-[1fr_1.2fr]">
|
||||||
|
{/* Credits Section */}
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Coins className="size-3.5" />
|
||||||
|
<span>Credit-Guthaben</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-4xl font-semibold tabular-nums tracking-tight">
|
||||||
|
{formatEurFromCents(balanceCents)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 pt-1">
|
||||||
|
<div className="flex items-baseline justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Reserviert</span>
|
||||||
|
<span className="tabular-nums font-medium">
|
||||||
|
{formatEurFromCents(reservedCents)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-baseline justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Monatskontingent</span>
|
||||||
|
<span className="tabular-nums text-muted-foreground">
|
||||||
|
{usagePercent}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={usagePercent} className="h-1.5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs leading-relaxed text-muted-foreground/80">
|
||||||
|
Bei fehlgeschlagenen Jobs werden reservierte Credits automatisch freigegeben.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Generation */}
|
||||||
|
<div className="rounded-2xl border bg-card p-6 shadow-sm shadow-foreground/3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Sparkles className="size-3.5" />
|
||||||
|
<span>Aktive Generierung</span>
|
||||||
|
</div>
|
||||||
|
<Badge className="gap-1.5 font-normal">
|
||||||
|
<span className="relative inline-flex size-1.5">
|
||||||
|
<span className="absolute inline-flex size-full animate-ping rounded-full bg-primary-foreground/60 opacity-75" />
|
||||||
|
<span className="relative inline-flex size-1.5 rounded-full bg-primary-foreground" />
|
||||||
|
</span>
|
||||||
|
Läuft
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="mt-4 text-lg font-medium">
|
||||||
|
Produktfotos — Variante 3/4
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="mb-2 flex items-baseline justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Fortschritt</span>
|
||||||
|
<span className="font-medium tabular-nums">62%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={62} className="h-1.5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-4 text-xs text-muted-foreground leading-relaxed">
|
||||||
|
Step 2 von 4 —{" "}
|
||||||
|
<span className="font-mono text-[0.7rem]">
|
||||||
|
flux-schnell
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Workspaces */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<LayoutTemplate className="size-3.5 text-muted-foreground" />
|
||||||
|
Workspaces
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" className="text-muted-foreground" disabled>
|
||||||
|
Neuer Workspace
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
{mockWorkspaces.map((ws) => (
|
||||||
|
<button
|
||||||
|
key={ws.name}
|
||||||
|
className={cn(
|
||||||
|
"group flex items-center gap-4 rounded-xl border bg-card p-4 text-left shadow-sm shadow-foreground/3 transition-all",
|
||||||
|
"hover:bg-muted/60 hover:shadow-md hover:shadow-foreground/4"
|
||||||
|
)}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-sm font-semibold text-primary">
|
||||||
|
{ws.initial}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm font-medium">{ws.name}</p>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
{ws.nodes} Nodes · {ws.frames} Frames
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ArrowUpRight className="size-4 text-muted-foreground/0 transition-colors group-hover:text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Recent Activity */}
|
||||||
|
<section>
|
||||||
|
<div className="mb-4 flex items-center gap-2 text-sm font-medium">
|
||||||
|
<Activity className="size-3.5 text-muted-foreground" />
|
||||||
|
Letzte Aktivität
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border bg-card shadow-sm shadow-foreground/3">
|
||||||
|
<div className="divide-y">
|
||||||
|
{mockRuns.map((run) => (
|
||||||
|
<div
|
||||||
|
key={run.id}
|
||||||
|
className="flex items-center gap-4 px-5 py-3.5"
|
||||||
|
>
|
||||||
|
<StatusDot status={run.status} />
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="truncate text-sm font-medium">
|
||||||
|
{run.workspace}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{run.node}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
{run.model !== "—" && (
|
||||||
|
<span className="font-mono text-[0.7rem]">{run.model}</span>
|
||||||
|
)}
|
||||||
|
{run.credits > 0 && (
|
||||||
|
<>
|
||||||
|
<span aria-hidden>·</span>
|
||||||
|
<span className="tabular-nums">{run.credits} ct</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="shrink-0 text-right">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{statusLabel(run.status)}
|
||||||
|
</span>
|
||||||
|
<p className="mt-0.5 text-[0.7rem] text-muted-foreground/70">
|
||||||
|
{run.updated}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
102
app/globals.css
@@ -49,72 +49,72 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.985 0.005 80);
|
||||||
--foreground: oklch(0.147 0.004 49.25);
|
--foreground: oklch(0.18 0.012 60);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(0.993 0.003 80);
|
||||||
--card-foreground: oklch(0.147 0.004 49.25);
|
--card-foreground: oklch(0.18 0.012 60);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(0.993 0.003 80);
|
||||||
--popover-foreground: oklch(0.147 0.004 49.25);
|
--popover-foreground: oklch(0.18 0.012 60);
|
||||||
--primary: oklch(0.511 0.096 186.391);
|
--primary: oklch(0.52 0.09 178);
|
||||||
--primary-foreground: oklch(0.984 0.014 180.72);
|
--primary-foreground: oklch(0.985 0.01 178);
|
||||||
--secondary: oklch(0.967 0.001 286.375);
|
--secondary: oklch(0.955 0.01 82);
|
||||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
--secondary-foreground: oklch(0.25 0.012 60);
|
||||||
--muted: oklch(0.97 0.001 106.424);
|
--muted: oklch(0.96 0.008 80);
|
||||||
--muted-foreground: oklch(0.553 0.013 58.071);
|
--muted-foreground: oklch(0.52 0.015 60);
|
||||||
--accent: oklch(0.97 0.001 106.424);
|
--accent: oklch(0.92 0.1 95);
|
||||||
--accent-foreground: oklch(0.216 0.006 56.043);
|
--accent-foreground: oklch(0.3 0.04 80);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.923 0.003 48.717);
|
--border: oklch(0.91 0.01 75);
|
||||||
--input: oklch(0.923 0.003 48.717);
|
--input: oklch(0.91 0.01 75);
|
||||||
--ring: oklch(0.709 0.01 56.259);
|
--ring: oklch(0.52 0.09 178);
|
||||||
--chart-1: oklch(0.845 0.143 164.978);
|
--chart-1: oklch(0.845 0.143 164.978);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.596 0.145 163.225);
|
--chart-3: oklch(0.596 0.145 163.225);
|
||||||
--chart-4: oklch(0.508 0.118 165.612);
|
--chart-4: oklch(0.508 0.118 165.612);
|
||||||
--chart-5: oklch(0.432 0.095 166.913);
|
--chart-5: oklch(0.432 0.095 166.913);
|
||||||
--radius: 0.625rem;
|
--radius: 0.75rem;
|
||||||
--sidebar: oklch(0.985 0.001 106.423);
|
--sidebar: oklch(0.975 0.006 80);
|
||||||
--sidebar-foreground: oklch(0.147 0.004 49.25);
|
--sidebar-foreground: oklch(0.18 0.012 60);
|
||||||
--sidebar-primary: oklch(0.6 0.118 184.704);
|
--sidebar-primary: oklch(0.52 0.09 178);
|
||||||
--sidebar-primary-foreground: oklch(0.984 0.014 180.72);
|
--sidebar-primary-foreground: oklch(0.985 0.01 178);
|
||||||
--sidebar-accent: oklch(0.97 0.001 106.424);
|
--sidebar-accent: oklch(0.96 0.008 80);
|
||||||
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
|
--sidebar-accent-foreground: oklch(0.25 0.012 60);
|
||||||
--sidebar-border: oklch(0.923 0.003 48.717);
|
--sidebar-border: oklch(0.91 0.01 75);
|
||||||
--sidebar-ring: oklch(0.709 0.01 56.259);
|
--sidebar-ring: oklch(0.52 0.09 178);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.147 0.004 49.25);
|
--background: oklch(0.16 0.01 65);
|
||||||
--foreground: oklch(0.985 0.001 106.423);
|
--foreground: oklch(0.93 0.008 80);
|
||||||
--card: oklch(0.216 0.006 56.043);
|
--card: oklch(0.21 0.012 65);
|
||||||
--card-foreground: oklch(0.985 0.001 106.423);
|
--card-foreground: oklch(0.93 0.008 80);
|
||||||
--popover: oklch(0.216 0.006 56.043);
|
--popover: oklch(0.21 0.012 65);
|
||||||
--popover-foreground: oklch(0.985 0.001 106.423);
|
--popover-foreground: oklch(0.93 0.008 80);
|
||||||
--primary: oklch(0.437 0.078 188.216);
|
--primary: oklch(0.62 0.1 178);
|
||||||
--primary-foreground: oklch(0.984 0.014 180.72);
|
--primary-foreground: oklch(0.15 0.03 178);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--secondary: oklch(0.26 0.01 65);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.92 0.006 80);
|
||||||
--muted: oklch(0.268 0.007 34.298);
|
--muted: oklch(0.24 0.01 65);
|
||||||
--muted-foreground: oklch(0.709 0.01 56.259);
|
--muted-foreground: oklch(0.65 0.012 70);
|
||||||
--accent: oklch(0.268 0.007 34.298);
|
--accent: oklch(0.35 0.06 90);
|
||||||
--accent-foreground: oklch(0.985 0.001 106.423);
|
--accent-foreground: oklch(0.93 0.008 80);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 8%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 12%);
|
||||||
--ring: oklch(0.553 0.013 58.071);
|
--ring: oklch(0.62 0.1 178);
|
||||||
--chart-1: oklch(0.845 0.143 164.978);
|
--chart-1: oklch(0.845 0.143 164.978);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.596 0.145 163.225);
|
--chart-3: oklch(0.596 0.145 163.225);
|
||||||
--chart-4: oklch(0.508 0.118 165.612);
|
--chart-4: oklch(0.508 0.118 165.612);
|
||||||
--chart-5: oklch(0.432 0.095 166.913);
|
--chart-5: oklch(0.432 0.095 166.913);
|
||||||
--sidebar: oklch(0.216 0.006 56.043);
|
--sidebar: oklch(0.19 0.01 65);
|
||||||
--sidebar-foreground: oklch(0.985 0.001 106.423);
|
--sidebar-foreground: oklch(0.93 0.008 80);
|
||||||
--sidebar-primary: oklch(0.704 0.14 182.503);
|
--sidebar-primary: oklch(0.62 0.1 178);
|
||||||
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
|
--sidebar-primary-foreground: oklch(0.15 0.03 178);
|
||||||
--sidebar-accent: oklch(0.268 0.007 34.298);
|
--sidebar-accent: oklch(0.24 0.01 65);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
|
--sidebar-accent-foreground: oklch(0.93 0.008 80);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(1 0 0 / 8%);
|
||||||
--sidebar-ring: oklch(0.553 0.013 58.071);
|
--sidebar-ring: oklch(0.62 0.1 178);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono, Manrope } from "next/font/google";
|
import { Geist, Geist_Mono, Manrope } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ConvexClientProvider } from "@/components/ui/convex-prover";
|
||||||
|
|
||||||
const manrope = Manrope({subsets:['latin'],variable:'--font-sans'});
|
const manrope = Manrope({subsets:['latin'],variable:'--font-sans'});
|
||||||
|
|
||||||
@@ -30,7 +31,9 @@ export default function RootLayout({
|
|||||||
lang="en"
|
lang="en"
|
||||||
className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, "font-sans", manrope.variable)}
|
className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, "font-sans", manrope.variable)}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-full flex flex-col">
|
||||||
|
<ConvexClientProvider>{children}</ConvexClientProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
87
app/page.tsx
@@ -1,65 +1,38 @@
|
|||||||
import Image from "next/image";
|
// src/app/page.tsx — minimaler Test
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useQuery } from "convex/react";
|
||||||
|
import { api } from "@/convex/_generated/api";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const user = useQuery(api.auth.getCurrentUser);
|
||||||
|
|
||||||
|
// user === undefined → Query lädt noch
|
||||||
|
// user === null → Nicht eingeloggt
|
||||||
|
// user === { id, name, email, ... } → Eingeloggt
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div>
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<h1>LemonSpace</h1>
|
||||||
<Image
|
{user ? (
|
||||||
className="dark:invert"
|
<div>
|
||||||
src="/next.svg"
|
<p>Eingeloggt als: {user.name}</p>
|
||||||
alt="Next.js logo"
|
<button onClick={() => authClient.signOut()}>Logout</button>
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
) : (
|
||||||
<a
|
<button
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
onClick={() =>
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
authClient.signUp.email({
|
||||||
target="_blank"
|
email: "test@lemonspace.io",
|
||||||
rel="noopener noreferrer"
|
password: "test1234",
|
||||||
|
name: "Test User",
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Image
|
Test Signup
|
||||||
className="dark:invert"
|
</button>
|
||||||
src="/vercel.svg"
|
)}
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
112
components/ui/avatar.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Avatar as AvatarPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||||
|
size?: "default" | "sm" | "lg"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn(
|
||||||
|
"aspect-square size-full rounded-full object-cover",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="avatar-badge"
|
||||||
|
className={cn(
|
||||||
|
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
||||||
|
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||||
|
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||||
|
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="avatar-group"
|
||||||
|
className={cn(
|
||||||
|
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarGroupCount({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="avatar-group-count"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Avatar,
|
||||||
|
AvatarImage,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarGroup,
|
||||||
|
AvatarGroupCount,
|
||||||
|
AvatarBadge,
|
||||||
|
}
|
||||||
49
components/ui/badge.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||||
|
outline:
|
||||||
|
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot.Root : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
103
components/ui/card.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn(
|
||||||
|
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
26
components/ui/convex-prover.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { ConvexReactClient } from "convex/react";
|
||||||
|
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
||||||
|
|
||||||
|
export function ConvexClientProvider({
|
||||||
|
children,
|
||||||
|
initialToken,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
initialToken?: string | null;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ConvexBetterAuthProvider
|
||||||
|
client={convex}
|
||||||
|
authClient={authClient}
|
||||||
|
initialToken={initialToken}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ConvexBetterAuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
269
components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { CheckIcon, ChevronRightIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
align = "start",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
className={cn("dark z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||||
|
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||||
|
>
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||||
|
data-slot="dropdown-menu-radio-item-indicator"
|
||||||
|
>
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn("dark z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
}
|
||||||
19
components/ui/input.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
31
components/ui/progress.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Progress as ProgressPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="size-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress }
|
||||||
55
components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
data-orientation={orientation}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="relative flex-1 rounded-full bg-border"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
||||||
28
components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
116
components/ui/table.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
90
components/ui/tabs.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Tabs as TabsPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Tabs({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
data-orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"group/tabs flex gap-2 data-horizontal:flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabsListVariants = cva(
|
||||||
|
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-muted",
|
||||||
|
line: "gap-1 bg-transparent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||||
|
VariantProps<typeof tabsListVariants>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(tabsListVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||||
|
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
||||||
|
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot="tabs-content"
|
||||||
|
className={cn("flex-1 text-sm outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||||
21226
convex/_generated/api.d.ts
vendored
Normal file
23
convex/_generated/api.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { anyApi, componentsGeneric } from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const api = anyApi;
|
||||||
|
export const internal = anyApi;
|
||||||
|
export const components = componentsGeneric();
|
||||||
58
convex/_generated/dataModel.d.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated data model types.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AnyDataModel } from "convex/server";
|
||||||
|
import type { GenericId } from "convex/values";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No `schema.ts` file found!
|
||||||
|
*
|
||||||
|
* This generated code has permissive types like `Doc = any` because
|
||||||
|
* Convex doesn't know your schema. If you'd like more type safety, see
|
||||||
|
* https://docs.convex.dev/using/schemas for instructions on how to add a
|
||||||
|
* schema file.
|
||||||
|
*
|
||||||
|
* After you change a schema, rerun codegen with `npx convex dev`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names of all of your Convex tables.
|
||||||
|
*/
|
||||||
|
export type TableNames = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of a document stored in Convex.
|
||||||
|
*/
|
||||||
|
export type Doc = any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An identifier for a document in Convex.
|
||||||
|
*
|
||||||
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||||
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||||
|
*
|
||||||
|
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
||||||
|
*
|
||||||
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||||
|
* strings when type checking.
|
||||||
|
*/
|
||||||
|
export type Id<TableName extends TableNames = TableNames> =
|
||||||
|
GenericId<TableName>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type describing your Convex data model.
|
||||||
|
*
|
||||||
|
* This type includes information about what tables you have, the type of
|
||||||
|
* documents stored in those tables, and the indexes defined on them.
|
||||||
|
*
|
||||||
|
* This type is used to parameterize methods like `queryGeneric` and
|
||||||
|
* `mutationGeneric` to make them type-safe.
|
||||||
|
*/
|
||||||
|
export type DataModel = AnyDataModel;
|
||||||
143
convex/_generated/server.d.ts
vendored
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionBuilder,
|
||||||
|
HttpActionBuilder,
|
||||||
|
MutationBuilder,
|
||||||
|
QueryBuilder,
|
||||||
|
GenericActionCtx,
|
||||||
|
GenericMutationCtx,
|
||||||
|
GenericQueryCtx,
|
||||||
|
GenericDatabaseReader,
|
||||||
|
GenericDatabaseWriter,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { DataModel } from "./dataModel.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const query: QueryBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||||
|
* code and code with side-effects, like calling third-party services.
|
||||||
|
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||||
|
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||||
|
*
|
||||||
|
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const action: ActionBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* The wrapped function will be used to respond to HTTP requests received
|
||||||
|
* by a Convex deployment if the requests matches the path and method where
|
||||||
|
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||||
|
* and a Fetch API `Request` object as its second.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export declare const httpAction: HttpActionBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex query functions.
|
||||||
|
*
|
||||||
|
* The query context is passed as the first argument to any Convex query
|
||||||
|
* function run on the server.
|
||||||
|
*
|
||||||
|
* This differs from the {@link MutationCtx} because all of the services are
|
||||||
|
* read-only.
|
||||||
|
*/
|
||||||
|
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex mutation functions.
|
||||||
|
*
|
||||||
|
* The mutation context is passed as the first argument to any Convex mutation
|
||||||
|
* function run on the server.
|
||||||
|
*/
|
||||||
|
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex action functions.
|
||||||
|
*
|
||||||
|
* The action context is passed as the first argument to any Convex action
|
||||||
|
* function run on the server.
|
||||||
|
*/
|
||||||
|
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to read from the database within Convex query functions.
|
||||||
|
*
|
||||||
|
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||||
|
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||||
|
* building a query.
|
||||||
|
*/
|
||||||
|
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to read from and write to the database within Convex mutation
|
||||||
|
* functions.
|
||||||
|
*
|
||||||
|
* Convex guarantees that all writes within a single mutation are
|
||||||
|
* executed atomically, so you never have to worry about partial writes leaving
|
||||||
|
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||||
|
* for the guarantees Convex provides your functions.
|
||||||
|
*/
|
||||||
|
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||||
93
convex/_generated/server.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
actionGeneric,
|
||||||
|
httpActionGeneric,
|
||||||
|
queryGeneric,
|
||||||
|
mutationGeneric,
|
||||||
|
internalActionGeneric,
|
||||||
|
internalMutationGeneric,
|
||||||
|
internalQueryGeneric,
|
||||||
|
} from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const query = queryGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalQuery = internalQueryGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const mutation = mutationGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalMutation = internalMutationGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||||
|
* code and code with side-effects, like calling third-party services.
|
||||||
|
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||||
|
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||||
|
*
|
||||||
|
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const action = actionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalAction = internalActionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* The wrapped function will be used to respond to HTTP requests received
|
||||||
|
* by a Convex deployment if the requests matches the path and method where
|
||||||
|
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||||
|
* and a Fetch API `Request` object as its second.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export const httpAction = httpActionGeneric;
|
||||||
6
convex/auth.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config";
|
||||||
|
import type { AuthConfig } from "convex/server";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
providers: [getAuthConfigProvider()],
|
||||||
|
} satisfies AuthConfig;
|
||||||
36
convex/auth.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
|
||||||
|
import { convex } from "@convex-dev/better-auth/plugins";
|
||||||
|
import { components } from "./_generated/api";
|
||||||
|
import { DataModel } from "./_generated/dataModel";
|
||||||
|
import { query } from "./_generated/server";
|
||||||
|
import { betterAuth } from "better-auth/minimal";
|
||||||
|
import authConfig from "./auth.config";
|
||||||
|
|
||||||
|
const siteUrl = process.env.SITE_URL!;
|
||||||
|
|
||||||
|
// Component Client — stellt Adapter, Helper und Auth-Methoden bereit
|
||||||
|
export const authComponent = createClient<DataModel>(components.betterAuth);
|
||||||
|
|
||||||
|
// Auth Factory — wird pro Request aufgerufen (Convex ist request-scoped)
|
||||||
|
export const createAuth = (ctx: GenericCtx<DataModel>) => {
|
||||||
|
return betterAuth({
|
||||||
|
baseURL: siteUrl,
|
||||||
|
trustedOrigins: [siteUrl],
|
||||||
|
database: authComponent.adapter(ctx),
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
requireEmailVerification: false, // Später auf true → useSend Integration
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
convex({ authConfig }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper Query: Aktuellen User abrufen (nutzbar in Frontend via useQuery)
|
||||||
|
export const getCurrentUser = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return authComponent.safeGetAuthUser(ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
7
convex/convex.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineApp } from "convex/server";
|
||||||
|
import betterAuth from "@convex-dev/better-auth/convex.config";
|
||||||
|
|
||||||
|
const app = defineApp();
|
||||||
|
app.use(betterAuth);
|
||||||
|
|
||||||
|
export default app;
|
||||||
9
convex/http.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { httpRouter } from "convex/server";
|
||||||
|
import { authComponent, createAuth } from "./auth";
|
||||||
|
|
||||||
|
const http = httpRouter();
|
||||||
|
|
||||||
|
// Auth-Routen registrieren (kein CORS nötig bei Next.js — same-origin)
|
||||||
|
authComponent.registerRoutes(http, createAuth);
|
||||||
|
|
||||||
|
export default http;
|
||||||
7
lib/auth-client.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
import { convexClient } from "@convex-dev/better-auth/client/plugins";
|
||||||
|
|
||||||
|
// Next.js: kein crossDomainClient nötig (same-origin via API Route Proxy)
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
plugins: [convexClient()],
|
||||||
|
});
|
||||||
19
lib/auth-server.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs";
|
||||||
|
|
||||||
|
export const {
|
||||||
|
handler, // Route Handler für /api/auth/*
|
||||||
|
preloadAuthQuery, // SSR: Query mit Auth vorladen
|
||||||
|
isAuthenticated, // Check ob User eingeloggt ist
|
||||||
|
getToken, // JWT Token abrufen
|
||||||
|
fetchAuthQuery, // Server-side: Convex Query mit Auth
|
||||||
|
fetchAuthMutation, // Server-side: Convex Mutation mit Auth
|
||||||
|
fetchAuthAction, // Server-side: Convex Action mit Auth
|
||||||
|
} = convexBetterAuthNextJs({
|
||||||
|
convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
|
||||||
|
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
|
||||||
|
// JWT-Caching für schnellere SSR (optional, aber empfohlen)
|
||||||
|
jwtCache: {
|
||||||
|
enabled: true,
|
||||||
|
isAuthError: (error) => /auth/i.test(String(error)),
|
||||||
|
},
|
||||||
|
});
|
||||||
6
lib/auth.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
secret: process.env.BETTER_AUTH_SECRET,
|
||||||
|
url: process.env.BETTER_AUTH_URL
|
||||||
|
});
|
||||||
@@ -9,8 +9,11 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@convex-dev/better-auth": "^0.11.3",
|
||||||
|
"better-auth": "^1.5.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"convex": "^1.34.0",
|
||||||
"lucide-react": "^1.6.0",
|
"lucide-react": "^1.6.0",
|
||||||
"next": "16.2.1",
|
"next": "16.2.1",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
@@ -18,7 +21,8 @@
|
|||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"shadcn": "^4.1.0",
|
"shadcn": "^4.1.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
694
pnpm-lock.yaml
generated
@@ -8,18 +8,27 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@convex-dev/better-auth':
|
||||||
|
specifier: ^0.11.3
|
||||||
|
version: 0.11.3(@standard-schema/spec@1.1.0)(better-auth@1.5.6(@opentelemetry/api@1.9.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(convex@1.34.0(react@19.2.4))(hono@4.12.9)(react@19.2.4)(typescript@5.9.3)
|
||||||
|
better-auth:
|
||||||
|
specifier: ^1.5.6
|
||||||
|
version: 1.5.6(@opentelemetry/api@1.9.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
convex:
|
||||||
|
specifier: ^1.34.0
|
||||||
|
version: 1.34.0(react@19.2.4)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.6.0(react@19.2.4)
|
version: 1.6.0(react@19.2.4)
|
||||||
next:
|
next:
|
||||||
specifier: 16.2.1
|
specifier: 16.2.1
|
||||||
version: 16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
radix-ui:
|
radix-ui:
|
||||||
specifier: ^1.4.3
|
specifier: ^1.4.3
|
||||||
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -38,6 +47,9 @@ importers:
|
|||||||
tw-animate-css:
|
tw-animate-css:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.6
|
||||||
|
version: 4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
@@ -199,6 +211,88 @@ packages:
|
|||||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@better-auth/core@1.5.6':
|
||||||
|
resolution: {integrity: sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
'@cloudflare/workers-types': '>=4'
|
||||||
|
'@opentelemetry/api': ^1.9.0
|
||||||
|
better-call: 1.3.2
|
||||||
|
jose: ^6.1.0
|
||||||
|
kysely: ^0.28.5
|
||||||
|
nanostores: ^1.0.1
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@cloudflare/workers-types':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@better-auth/drizzle-adapter@1.5.6':
|
||||||
|
resolution: {integrity: sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/core': 1.5.6
|
||||||
|
'@better-auth/utils': ^0.3.0
|
||||||
|
drizzle-orm: '>=0.41.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
drizzle-orm:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@better-auth/kysely-adapter@1.5.6':
|
||||||
|
resolution: {integrity: sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/core': 1.5.6
|
||||||
|
'@better-auth/utils': ^0.3.0
|
||||||
|
kysely: ^0.27.0 || ^0.28.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
kysely:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@better-auth/memory-adapter@1.5.6':
|
||||||
|
resolution: {integrity: sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/core': 1.5.6
|
||||||
|
'@better-auth/utils': ^0.3.0
|
||||||
|
|
||||||
|
'@better-auth/mongo-adapter@1.5.6':
|
||||||
|
resolution: {integrity: sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/core': 1.5.6
|
||||||
|
'@better-auth/utils': ^0.3.0
|
||||||
|
mongodb: ^6.0.0 || ^7.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
mongodb:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@better-auth/prisma-adapter@1.5.6':
|
||||||
|
resolution: {integrity: sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/core': 1.5.6
|
||||||
|
'@better-auth/utils': ^0.3.0
|
||||||
|
'@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
prisma: ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@prisma/client':
|
||||||
|
optional: true
|
||||||
|
prisma:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@better-auth/telemetry@1.5.6':
|
||||||
|
resolution: {integrity: sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/core': 1.5.6
|
||||||
|
|
||||||
|
'@better-auth/utils@0.3.1':
|
||||||
|
resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==}
|
||||||
|
|
||||||
|
'@better-fetch/fetch@1.1.21':
|
||||||
|
resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==}
|
||||||
|
|
||||||
|
'@convex-dev/better-auth@0.11.3':
|
||||||
|
resolution: {integrity: sha512-hun3psvKwlrjHGEpr8HiDT72hogaiJovHPGeqHHtZmU8+VYllhEVH/KkD7YNIqq02MFLQXblsd6fPZdDOdcg4A==}
|
||||||
|
peerDependencies:
|
||||||
|
better-auth: '>=1.5.0 <1.6.0'
|
||||||
|
convex: ^1.25.0
|
||||||
|
react: ^18.3.1 || ^19.0.0
|
||||||
|
|
||||||
'@dotenvx/dotenvx@1.57.2':
|
'@dotenvx/dotenvx@1.57.2':
|
||||||
resolution: {integrity: sha512-lv9+UZPnl/KOvShepevLWm3+/wc1It5kgO5Q580evnvOFMZcgKVEYFwxlL7Ohl9my1yjTsWo28N3PJYUEO8wFQ==}
|
resolution: {integrity: sha512-lv9+UZPnl/KOvShepevLWm3+/wc1It5kgO5Q580evnvOFMZcgKVEYFwxlL7Ohl9my1yjTsWo28N3PJYUEO8wFQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -218,6 +312,162 @@ packages:
|
|||||||
'@emnapi/wasi-threads@1.2.0':
|
'@emnapi/wasi-threads@1.2.0':
|
||||||
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
|
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.0':
|
||||||
|
resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.0':
|
||||||
|
resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.0':
|
||||||
|
resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.0':
|
||||||
|
resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.0':
|
||||||
|
resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.0':
|
||||||
|
resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.0':
|
||||||
|
resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1':
|
'@eslint-community/eslint-utils@4.9.1':
|
||||||
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@@ -576,6 +826,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
|
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: ^14.21.3 || >=16}
|
||||||
|
|
||||||
|
'@noble/ciphers@2.1.1':
|
||||||
|
resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
'@noble/curves@1.9.7':
|
'@noble/curves@1.9.7':
|
||||||
resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
|
resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: ^14.21.3 || >=16}
|
||||||
@@ -584,6 +838,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: ^14.21.3 || >=16}
|
||||||
|
|
||||||
|
'@noble/hashes@2.0.1':
|
||||||
|
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -609,6 +867,14 @@ packages:
|
|||||||
'@open-draft/until@2.1.0':
|
'@open-draft/until@2.1.0':
|
||||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||||
|
|
||||||
|
'@opentelemetry/api@1.9.0':
|
||||||
|
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
'@opentelemetry/semantic-conventions@1.40.0':
|
||||||
|
resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1':
|
'@radix-ui/number@1.1.1':
|
||||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||||
|
|
||||||
@@ -1309,6 +1575,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0':
|
||||||
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||||
|
|
||||||
@@ -1720,6 +1989,76 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
better-auth@1.5.6:
|
||||||
|
resolution: {integrity: sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@lynx-js/react': '*'
|
||||||
|
'@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
'@sveltejs/kit': ^2.0.0
|
||||||
|
'@tanstack/react-start': ^1.0.0
|
||||||
|
'@tanstack/solid-start': ^1.0.0
|
||||||
|
better-sqlite3: ^12.0.0
|
||||||
|
drizzle-kit: '>=0.31.4'
|
||||||
|
drizzle-orm: '>=0.41.0'
|
||||||
|
mongodb: ^6.0.0 || ^7.0.0
|
||||||
|
mysql2: ^3.0.0
|
||||||
|
next: ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||||
|
pg: ^8.0.0
|
||||||
|
prisma: ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
solid-js: ^1.0.0
|
||||||
|
svelte: ^4.0.0 || ^5.0.0
|
||||||
|
vitest: ^2.0.0 || ^3.0.0 || ^4.0.0
|
||||||
|
vue: ^3.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@lynx-js/react':
|
||||||
|
optional: true
|
||||||
|
'@prisma/client':
|
||||||
|
optional: true
|
||||||
|
'@sveltejs/kit':
|
||||||
|
optional: true
|
||||||
|
'@tanstack/react-start':
|
||||||
|
optional: true
|
||||||
|
'@tanstack/solid-start':
|
||||||
|
optional: true
|
||||||
|
better-sqlite3:
|
||||||
|
optional: true
|
||||||
|
drizzle-kit:
|
||||||
|
optional: true
|
||||||
|
drizzle-orm:
|
||||||
|
optional: true
|
||||||
|
mongodb:
|
||||||
|
optional: true
|
||||||
|
mysql2:
|
||||||
|
optional: true
|
||||||
|
next:
|
||||||
|
optional: true
|
||||||
|
pg:
|
||||||
|
optional: true
|
||||||
|
prisma:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
solid-js:
|
||||||
|
optional: true
|
||||||
|
svelte:
|
||||||
|
optional: true
|
||||||
|
vitest:
|
||||||
|
optional: true
|
||||||
|
vue:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
better-call@1.3.2:
|
||||||
|
resolution: {integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1819,6 +2158,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
common-tags@1.8.2:
|
||||||
|
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
@@ -1833,6 +2176,44 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
convex-helpers@0.1.114:
|
||||||
|
resolution: {integrity: sha512-elEdh+gG6BDv2dWIWVvBeJPbHnDQS5+WexUuwlGVJXz1EbMkXz/UIQwFIfLMZIXUwW6ot4JYf/1JJKNStrE6lg==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@standard-schema/spec': ^1.0.0
|
||||||
|
convex: ^1.32.0
|
||||||
|
hono: ^4.0.5
|
||||||
|
react: ^17.0.2 || ^18.0.0 || ^19.0.0
|
||||||
|
typescript: ^5.5
|
||||||
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@standard-schema/spec':
|
||||||
|
optional: true
|
||||||
|
hono:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
convex@1.34.0:
|
||||||
|
resolution: {integrity: sha512-TbC509Z4urZMChZR2aLPgalQ8gMhAYSz2VMxaYsCvba8YqB0Uxma7zWnXwRn7aEGXuA8ro5/uHgD1IJ0HhYYPg==}
|
||||||
|
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@auth0/auth0-react': ^2.0.1
|
||||||
|
'@clerk/clerk-react': ^4.12.8 || ^5.0.0
|
||||||
|
react: ^18.0.0 || ^19.0.0-0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@auth0/auth0-react':
|
||||||
|
optional: true
|
||||||
|
'@clerk/clerk-react':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
cookie-signature@1.2.2:
|
cookie-signature@1.2.2:
|
||||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||||
engines: {node: '>=6.6.0'}
|
engines: {node: '>=6.6.0'}
|
||||||
@@ -1941,6 +2322,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
defu@6.1.4:
|
||||||
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
depd@2.0.0:
|
depd@2.0.0:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2034,6 +2418,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
esbuild@0.27.0:
|
||||||
|
resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
escalade@3.2.0:
|
escalade@3.2.0:
|
||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2709,6 +3098,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
kysely@0.28.14:
|
||||||
|
resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
language-subtag-registry@0.3.23:
|
language-subtag-registry@0.3.23:
|
||||||
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
|
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
|
||||||
|
|
||||||
@@ -2894,6 +3287,10 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nanostores@1.2.0:
|
||||||
|
resolution: {integrity: sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==}
|
||||||
|
engines: {node: ^20.0.0 || >=22.0.0}
|
||||||
|
|
||||||
napi-postinstall@0.3.4:
|
napi-postinstall@0.3.4:
|
||||||
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
|
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
@@ -3108,6 +3505,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
prettier@3.8.1:
|
||||||
|
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
pretty-ms@9.3.0:
|
pretty-ms@9.3.0:
|
||||||
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -3209,6 +3611,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
remeda@2.33.6:
|
||||||
|
resolution: {integrity: sha512-tazDGH7s75kUPGBKLvhgBEHMgW+TdDFhjUAMdQj57IoWz6HsGa5D2RX5yDUz6IIqiRRvZiaEHzCzWdTeixc/Kg==}
|
||||||
|
|
||||||
require-directory@2.1.1:
|
require-directory@2.1.1:
|
||||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3245,6 +3650,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
|
|
||||||
|
rou3@0.7.12:
|
||||||
|
resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==}
|
||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@@ -3291,6 +3699,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
set-cookie-parser@3.1.0:
|
||||||
|
resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3520,6 +3931,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
type-fest@4.41.0:
|
||||||
|
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
type-fest@5.5.0:
|
type-fest@5.5.0:
|
||||||
resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==}
|
resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -3671,6 +4086,18 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
ws@8.18.0:
|
||||||
|
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
wsl-utils@0.3.1:
|
wsl-utils@0.3.1:
|
||||||
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -3909,6 +4336,74 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
|
'@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/semantic-conventions': 1.40.0
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
better-call: 1.3.2(zod@4.3.6)
|
||||||
|
jose: 6.2.2
|
||||||
|
kysely: 0.28.14
|
||||||
|
nanostores: 1.2.0
|
||||||
|
zod: 4.3.6
|
||||||
|
|
||||||
|
'@better-auth/drizzle-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
|
||||||
|
'@better-auth/kysely-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14)':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
optionalDependencies:
|
||||||
|
kysely: 0.28.14
|
||||||
|
|
||||||
|
'@better-auth/memory-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
|
||||||
|
'@better-auth/mongo-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
|
||||||
|
'@better-auth/prisma-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
|
||||||
|
'@better-auth/telemetry@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
|
||||||
|
'@better-auth/utils@0.3.1': {}
|
||||||
|
|
||||||
|
'@better-fetch/fetch@1.1.21': {}
|
||||||
|
|
||||||
|
'@convex-dev/better-auth@0.11.3(@standard-schema/spec@1.1.0)(better-auth@1.5.6(@opentelemetry/api@1.9.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(convex@1.34.0(react@19.2.4))(hono@4.12.9)(react@19.2.4)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
better-auth: 1.5.6(@opentelemetry/api@1.9.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
common-tags: 1.8.2
|
||||||
|
convex: 1.34.0(react@19.2.4)
|
||||||
|
convex-helpers: 0.1.114(@standard-schema/spec@1.1.0)(convex@1.34.0(react@19.2.4))(hono@4.12.9)(react@19.2.4)(typescript@5.9.3)(zod@4.3.6)
|
||||||
|
jose: 6.2.2
|
||||||
|
react: 19.2.4
|
||||||
|
remeda: 2.33.6
|
||||||
|
semver: 7.7.4
|
||||||
|
type-fest: 4.41.0
|
||||||
|
zod: 4.3.6
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@standard-schema/spec'
|
||||||
|
- hono
|
||||||
|
- typescript
|
||||||
|
|
||||||
'@dotenvx/dotenvx@1.57.2':
|
'@dotenvx/dotenvx@1.57.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 11.1.0
|
commander: 11.1.0
|
||||||
@@ -3941,6 +4436,84 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
|
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4(jiti@2.6.1)
|
||||||
@@ -4233,12 +4806,16 @@ snapshots:
|
|||||||
|
|
||||||
'@noble/ciphers@1.3.0': {}
|
'@noble/ciphers@1.3.0': {}
|
||||||
|
|
||||||
|
'@noble/ciphers@2.1.1': {}
|
||||||
|
|
||||||
'@noble/curves@1.9.7':
|
'@noble/curves@1.9.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
|
|
||||||
'@noble/hashes@1.8.0': {}
|
'@noble/hashes@1.8.0': {}
|
||||||
|
|
||||||
|
'@noble/hashes@2.0.1': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@@ -4262,6 +4839,10 @@ snapshots:
|
|||||||
|
|
||||||
'@open-draft/until@2.1.0': {}
|
'@open-draft/until@2.1.0': {}
|
||||||
|
|
||||||
|
'@opentelemetry/api@1.9.0': {}
|
||||||
|
|
||||||
|
'@opentelemetry/semantic-conventions@1.40.0': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.3': {}
|
'@radix-ui/primitive@1.1.3': {}
|
||||||
@@ -5015,6 +5596,8 @@ snapshots:
|
|||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
'@sindresorhus/merge-streams@4.0.0': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -5407,6 +5990,42 @@ snapshots:
|
|||||||
|
|
||||||
baseline-browser-mapping@2.10.10: {}
|
baseline-browser-mapping@2.10.10: {}
|
||||||
|
|
||||||
|
better-auth@1.5.6(@opentelemetry/api@1.9.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)
|
||||||
|
'@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)
|
||||||
|
'@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14)
|
||||||
|
'@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)
|
||||||
|
'@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)
|
||||||
|
'@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)
|
||||||
|
'@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
'@noble/ciphers': 2.1.1
|
||||||
|
'@noble/hashes': 2.0.1
|
||||||
|
better-call: 1.3.2(zod@4.3.6)
|
||||||
|
defu: 6.1.4
|
||||||
|
jose: 6.2.2
|
||||||
|
kysely: 0.28.14
|
||||||
|
nanostores: 1.2.0
|
||||||
|
zod: 4.3.6
|
||||||
|
optionalDependencies:
|
||||||
|
next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@cloudflare/workers-types'
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
better-call@1.3.2(zod@4.3.6):
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
rou3: 0.7.12
|
||||||
|
set-cookie-parser: 3.1.0
|
||||||
|
optionalDependencies:
|
||||||
|
zod: 4.3.6
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -5510,6 +6129,8 @@ snapshots:
|
|||||||
|
|
||||||
commander@14.0.3: {}
|
commander@14.0.3: {}
|
||||||
|
|
||||||
|
common-tags@1.8.2: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
content-disposition@1.0.1: {}
|
content-disposition@1.0.1: {}
|
||||||
@@ -5518,6 +6139,27 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
convex-helpers@0.1.114(@standard-schema/spec@1.1.0)(convex@1.34.0(react@19.2.4))(hono@4.12.9)(react@19.2.4)(typescript@5.9.3)(zod@4.3.6):
|
||||||
|
dependencies:
|
||||||
|
convex: 1.34.0(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
hono: 4.12.9
|
||||||
|
react: 19.2.4
|
||||||
|
typescript: 5.9.3
|
||||||
|
zod: 4.3.6
|
||||||
|
|
||||||
|
convex@1.34.0(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.27.0
|
||||||
|
prettier: 3.8.1
|
||||||
|
ws: 8.18.0
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
cookie-signature@1.2.2: {}
|
cookie-signature@1.2.2: {}
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
@@ -5605,6 +6247,8 @@ snapshots:
|
|||||||
has-property-descriptors: 1.0.2
|
has-property-descriptors: 1.0.2
|
||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
|
|
||||||
|
defu@6.1.4: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
@@ -5757,6 +6401,35 @@ snapshots:
|
|||||||
is-date-object: 1.1.0
|
is-date-object: 1.1.0
|
||||||
is-symbol: 1.1.1
|
is-symbol: 1.1.1
|
||||||
|
|
||||||
|
esbuild@0.27.0:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.27.0
|
||||||
|
'@esbuild/android-arm': 0.27.0
|
||||||
|
'@esbuild/android-arm64': 0.27.0
|
||||||
|
'@esbuild/android-x64': 0.27.0
|
||||||
|
'@esbuild/darwin-arm64': 0.27.0
|
||||||
|
'@esbuild/darwin-x64': 0.27.0
|
||||||
|
'@esbuild/freebsd-arm64': 0.27.0
|
||||||
|
'@esbuild/freebsd-x64': 0.27.0
|
||||||
|
'@esbuild/linux-arm': 0.27.0
|
||||||
|
'@esbuild/linux-arm64': 0.27.0
|
||||||
|
'@esbuild/linux-ia32': 0.27.0
|
||||||
|
'@esbuild/linux-loong64': 0.27.0
|
||||||
|
'@esbuild/linux-mips64el': 0.27.0
|
||||||
|
'@esbuild/linux-ppc64': 0.27.0
|
||||||
|
'@esbuild/linux-riscv64': 0.27.0
|
||||||
|
'@esbuild/linux-s390x': 0.27.0
|
||||||
|
'@esbuild/linux-x64': 0.27.0
|
||||||
|
'@esbuild/netbsd-arm64': 0.27.0
|
||||||
|
'@esbuild/netbsd-x64': 0.27.0
|
||||||
|
'@esbuild/openbsd-arm64': 0.27.0
|
||||||
|
'@esbuild/openbsd-x64': 0.27.0
|
||||||
|
'@esbuild/openharmony-arm64': 0.27.0
|
||||||
|
'@esbuild/sunos-x64': 0.27.0
|
||||||
|
'@esbuild/win32-arm64': 0.27.0
|
||||||
|
'@esbuild/win32-ia32': 0.27.0
|
||||||
|
'@esbuild/win32-x64': 0.27.0
|
||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-html@1.0.3: {}
|
escape-html@1.0.3: {}
|
||||||
@@ -6510,6 +7183,8 @@ snapshots:
|
|||||||
|
|
||||||
kleur@4.1.5: {}
|
kleur@4.1.5: {}
|
||||||
|
|
||||||
|
kysely@0.28.14: {}
|
||||||
|
|
||||||
language-subtag-registry@0.3.23: {}
|
language-subtag-registry@0.3.23: {}
|
||||||
|
|
||||||
language-tags@1.0.9:
|
language-tags@1.0.9:
|
||||||
@@ -6665,13 +7340,15 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
nanostores@1.2.0: {}
|
||||||
|
|
||||||
napi-postinstall@0.3.4: {}
|
napi-postinstall@0.3.4: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.2.1
|
'@next/env': 16.2.1
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -6690,6 +7367,7 @@ snapshots:
|
|||||||
'@next/swc-linux-x64-musl': 16.2.1
|
'@next/swc-linux-x64-musl': 16.2.1
|
||||||
'@next/swc-win32-arm64-msvc': 16.2.1
|
'@next/swc-win32-arm64-msvc': 16.2.1
|
||||||
'@next/swc-win32-x64-msvc': 16.2.1
|
'@next/swc-win32-x64-msvc': 16.2.1
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@@ -6887,6 +7565,8 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
prettier@3.8.1: {}
|
||||||
|
|
||||||
pretty-ms@9.3.0:
|
pretty-ms@9.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
parse-ms: 4.0.0
|
parse-ms: 4.0.0
|
||||||
@@ -7051,6 +7731,8 @@ snapshots:
|
|||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
set-function-name: 2.0.2
|
set-function-name: 2.0.2
|
||||||
|
|
||||||
|
remeda@2.33.6: {}
|
||||||
|
|
||||||
require-directory@2.1.1: {}
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
require-from-string@2.0.2: {}
|
require-from-string@2.0.2: {}
|
||||||
@@ -7083,6 +7765,8 @@ snapshots:
|
|||||||
|
|
||||||
reusify@1.1.0: {}
|
reusify@1.1.0: {}
|
||||||
|
|
||||||
|
rou3@0.7.12: {}
|
||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -7151,6 +7835,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
set-cookie-parser@3.1.0: {}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-data-property: 1.1.4
|
define-data-property: 1.1.4
|
||||||
@@ -7465,6 +8151,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
|
type-fest@4.41.0: {}
|
||||||
|
|
||||||
type-fest@5.5.0:
|
type-fest@5.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tagged-tag: 1.0.0
|
tagged-tag: 1.0.0
|
||||||
@@ -7664,6 +8352,8 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
ws@8.18.0: {}
|
||||||
|
|
||||||
wsl-utils@0.3.1:
|
wsl-utils@0.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-wsl: 3.1.1
|
is-wsl: 3.1.1
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 391 B |
@@ -1 +0,0 @@
|
|||||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
1
public/logos/lemonspace-logo-v2-black-rgb.svg
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
1
public/logos/lemonspace-logo-v2-primary-rgb.svg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
1
public/logos/lemonspace-logo-v2-white-rgb.svg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 128 B |
@@ -1 +0,0 @@
|
|||||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 385 B |