diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..506c68b
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,8 @@
+APP_PORT=8080
+POSTGRES_USER=cloud_notes
+POSTGRES_PASSWORD=replace-with-a-long-random-password
+POSTGRES_DB=cloud_notes
+JWT_SECRET_KEY=replace-with-a-random-secret-at-least-32-bytes-long
+COOKIE_SECURE=false
+REGISTRATION_ENABLED=true
+SQL_ECHO=false
diff --git a/.gitignore b/.gitignore
index d05d320..5ce381b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -184,3 +184,36 @@ cython_debug/
backend/uploads/*
!backend/uploads/README.md
+!frontend/src/lib/
+!frontend/src/lib/**
+
+# Node.js / frontend dependencies and build artifacts
+node_modules/
+.pnpm-store/
+dist/
+dist-ssr/
+*.tsbuildinfo
+vite.config.*.timestamp-*
+
+# Frontend tooling caches
+.vite/
+.eslintcache
+.stylelintcache
+
+# Local environment files (examples must stay versioned)
+.env.*
+!.env.example
+!**/.env.example
+
+# Package manager and development logs
+logs/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editors and operating systems
+.vscode/
+*.swp
+*.swo
+Thumbs.db
diff --git a/README.md b/README.md
index 7762d2e..c907d08 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,148 @@
-# ⚙️ Cloud_Notes_api
+# ⚙️ Cloud Notes
-Async cloud notes backend powered by FastAPI + PostgreSQL. Clean architecture, authentication, and containerization out of the box.
+Self-hosted cloud notes with a focused React editor, FastAPI backend, PostgreSQL storage, cookie authentication, image uploads, and English/Russian localization.
---
## 🛠️ Technology Stack
-[](https://python.org)
+[](https://react.dev)
+[](https://typescriptlang.org)
[](https://fastapi.tiangolo.com)
[](https://postgresql.org)
[](https://docker.com)
-[](https://www.jwt.io)
-[](https://pydantic.dev)
+[](https://nginx.org)
-- **Backend Framework:** [FastAPI](https://tiangolo.com) (Asynchronous, High performance)
-- **Database Engine:** [PostgreSQL](https://postgresql.org)
-- **ORM:** [SQLAlchemy 2.0](https://sqlalchemy.org) (Async extension with `asyncpg`)
-- **Data Validation:** [Pydantic v2](https://pydantic.dev)
-- **Security:** JWT (JSON Web Tokens) inside secure **HttpOnly Cookies** + `bcrypt` password hashing
-- **Environment:** [Docker](https://docker.com) / [OrbStack](https://orbstack.dev) for full containerization
+- **Frontend:** React 19, TypeScript, Vite, TipTap, and Lucide icons
+- **Backend:** FastAPI, Pydantic, and async SQLAlchemy 2.0
+- **Database:** PostgreSQL 16
+- **Authentication:** JWT in HttpOnly cookies with bcrypt password hashing
+- **Web server:** Nginx serving the production frontend and proxying `/api`
+- **Deployment:** Docker Compose with health checks and persistent volumes
## 🚀 Quick Start (Docker)
-Make sure you have **Docker** or **OrbStack** running on your system. You don't need to install Python or PostgreSQL locally.
+Docker or OrbStack is the only requirement. Python, Node.js, Nginx, and PostgreSQL do not need to be installed on the host.
1. **Clone the repository:**
+
```bash
git clone https://github.com/reallyShould/Cloud_Notes_api.git
cd Cloud_Notes_api
```
-2. **Launch the environment:**
- This command automatically creates the database, runs migrations (tables initialization), and starts the backend server:
+2. **Start the complete application with one command:**
+
```bash
- docker compose up --build
+ docker compose up -d --build
```
-3. **Explore the API:**
- Once the terminal prints `Uvicorn running on http://0.0.0.0`, open your browser at:
- - **Interactive API Docs (Swagger UI):** `http://localhost:8000/docs`
+3. **Open Cloud Notes:**
+
+ - On the Docker host: `http://localhost:8080`
+ - From another device on the home network: `http://SERVER_IP:8080`
+ - API documentation: `http://SERVER_IP:8080/api/docs`
+
+The default published port is **8080**. PostgreSQL and FastAPI are not published on the host and are reachable only through the private Docker network. All browser requests use the same Nginx origin, so no domain is required.
+
+---
+
+## ⚙️ Optional Configuration
+
+The application works without an `.env` file. For a permanent home-server installation, copy the example before the first start:
+
+```bash
+cp .env.example .env
+```
+
+Recommended settings:
+
+```dotenv
+APP_PORT=8080
+POSTGRES_USER=cloud_notes
+POSTGRES_PASSWORD=use-a-long-random-password
+POSTGRES_DB=cloud_notes
+JWT_SECRET_KEY=use-a-random-secret-at-least-32-bytes-long
+COOKIE_SECURE=false
+REGISTRATION_ENABLED=true
+SQL_ECHO=false
+```
+
+- `APP_PORT` changes the single host port exposed by the stack.
+- `JWT_SECRET_KEY` keeps existing login sessions valid after backend recreation. Without it, a secure temporary key is generated at every backend start.
+- `COOKIE_SECURE=false` is required for plain HTTP access by local IP. Set it to `true` only when the application is served through HTTPS.
+- Set `REGISTRATION_ENABLED=false` after creating the required accounts if public registration is not needed.
+- Database credentials must be selected before the PostgreSQL volume is initialized. Changing them later does not modify an existing database volume.
+
+Apply configuration changes with:
+
+```bash
+docker compose up -d --build
+```
+
+## 🔌 Published Ports
+
+| Service | Container port | Host port | Exposure |
+|---|---:|---:|---|
+| Nginx frontend | 80 | `8080` by default | Home network |
+| FastAPI backend | 8000 | Not published | Docker network only |
+| PostgreSQL | 5432 | Not published | Docker network only |
+
+If `APP_PORT=9090` is set, the application will be available at `http://SERVER_IP:9090` and no other host ports will be opened by this Compose project.
+
+---
+
+## 💾 Persistent Data and Backups
+
+Docker volumes preserve both database records and uploaded images:
+
+- `pgdata` stores PostgreSQL data.
+- `uploads` stores note attachments.
+
+On upgrade, files from the legacy `backend/uploads` directory are copied into the attachment volume automatically. Existing attachment links that used `localhost:8000` are normalized by the API.
+
+Stop containers without deleting data:
+
+```bash
+docker compose down
+```
+
+Do not use `docker compose down -v` unless all notes and uploaded images may be permanently deleted.
+
+Create a database backup:
+
+```bash
+docker compose exec -T db pg_dump -U admin main_db > cloud-notes-backup.sql
+```
+
+When custom PostgreSQL values are configured, replace `admin` and `main_db` in the backup command.
+
+## 🩺 Operations
+
+Check container health and published ports:
+
+```bash
+docker compose ps
+```
+
+Follow application logs:
+
+```bash
+docker compose logs -f frontend server db
+```
+
+Restart the stack:
+
+```bash
+docker compose restart
+```
+
+Update after pulling new code:
+
+```bash
+git pull
+docker compose up -d --build
+```
---
@@ -48,26 +152,29 @@ Make sure you have **Docker** or **OrbStack** running on your system. You don't
Cloud_Notes_api/
├── backend/
│ ├── app/
-│ │ ├── api/ # Modular API Routers (Separation of Concerns)
-│ │ │ ├── notes.py # Notes CRUD logic (Markdown supported)
-│ │ │ ├── system.py # Health checks and monitoring
-│ │ │ └── users.py # Registration & Authentication
-│ │ ├── database.py # Async SQLAlchemy engine & session setup
-│ │ ├── dependencies.py # Secure request filters (Token decoding & User fetch)
-│ │ ├── main.py # Application entrypoint & Lifespan setup
-│ │ ├── models.py # SQLAlchemy DB models (User, Note)
-│ │ ├── schemas.py # Pydantic validation DTOs
-│ │ └── utils.py # Hashing & JWT utility functions
-│ ├── Dockerfile
-│ └── requirements.txt
+│ │ ├── api/ # Notes, users, attachments, and health routes
+│ │ ├── database.py # Async SQLAlchemy engine and sessions
+│ │ ├── dependencies.py # Authentication dependencies
+│ │ ├── main.py # FastAPI application and startup lifecycle
+│ │ ├── models.py # User, note, and attachment models
+│ │ ├── schemas.py # Request and response validation
+│ │ └── utils.py # Password hashing and JWT helpers
+│ └── Dockerfile
+├── frontend/
+│ ├── src/ # React application, styles, API client, and localization
+│ ├── Dockerfile # Vite build and Nginx runtime
+│ └── nginx.conf # Static frontend and `/api` reverse proxy
├── docker-compose.yml
+├── .env.example
└── README.md
```
----
-
-## 🔒 Security Features
+## 🔒 Security Notes
-- **XSS Protection:** Access tokens are strictly transmitted via **HttpOnly** cookies, preventing malicious JavaScript from stealing sessions.
-- **CSRF Mitigation:** Cookie management utilizes `samesite="lax"` policy configuration.
-- **Credential Safety:** Passwords are encrypted with a slow `bcrypt` hashing context. No raw credentials ever enter the database.
+- Only the Nginx application port is published by default.
+- Authentication cookies are HttpOnly and use `SameSite=Lax`.
+- Login attempts are limited per client address.
+- Uploaded images are size-limited, streamed to disk, and checked by extension, MIME type, and file signature.
+- SQL parameter logging is disabled by default.
+- The backend container runs as an unprivileged user.
+- Plain HTTP does not encrypt credentials or note contents. Use the application only on a trusted LAN, or place it behind HTTPS, Tailscale, or another VPN when remote access is required.
diff --git a/backend/.dockerignore b/backend/.dockerignore
new file mode 100644
index 0000000..74ef339
--- /dev/null
+++ b/backend/.dockerignore
@@ -0,0 +1,12 @@
+__pycache__
+*.py[cod]
+.pytest_cache
+.mypy_cache
+.ruff_cache
+.venv
+venv
+uploads
+.env
+.env.*
+.git
+*.log
diff --git a/backend/Dockerfile b/backend/Dockerfile
index f10f57e..6628b5e 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -11,6 +11,12 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY . /code/
+RUN useradd --create-home --uid 10001 appuser \
+ && mkdir -p /data/uploads \
+ && chown -R appuser:appuser /code /data/uploads
+
+USER appuser
+
EXPOSE 8000
-CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"]
diff --git a/backend/app/api/attachments.py b/backend/app/api/attachments.py
index 4cd9949..46bf1d1 100644
--- a/backend/app/api/attachments.py
+++ b/backend/app/api/attachments.py
@@ -1,80 +1,106 @@
+import os
+import uuid
+from pathlib import Path
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
-
-from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
-import os, uuid
-
-from ..dependencies import get_current_user
from ..database import get_db
-from ..models import User, Attachment
+from ..dependencies import get_current_user
+from ..models import Attachment, User
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
+ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"}
MAX_FILE_SIZE = 25 * 1024 * 1024
+CHUNK_SIZE = 1024 * 1024
+UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "uploads"))
attachments_router = APIRouter(prefix="/attachments", tags=["Attachments"])
+def has_valid_image_signature(extension: str, header: bytes) -> bool:
+ if extension in {".jpg", ".jpeg"}:
+ return header.startswith(b"\xff\xd8\xff")
+ if extension == ".png":
+ return header.startswith(b"\x89PNG\r\n\x1a\n")
+ if extension == ".gif":
+ return header.startswith((b"GIF87a", b"GIF89a"))
+ if extension == ".webp":
+ return len(header) >= 12 and header[:4] == b"RIFF" and header[8:12] == b"WEBP"
+ return False
+
+
@attachments_router.post("")
-async def upload_attachments(
- file: UploadFile,
- db: AsyncSession = Depends(get_db),
- current_user: User = Depends(get_current_user)
+async def upload_attachment(
+ file: UploadFile,
+ db: AsyncSession = Depends(get_db),
+ current_user: User = Depends(get_current_user),
):
- extension = os.path.splitext(file.filename)[1].lower()
+ original_name = file.filename or "upload"
+ extension = Path(original_name).suffix.lower()
- if extension not in ALLOWED_EXTENSIONS:
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail=f"Unsupported file type. Allowed types: {', '.join(ALLOWED_EXTENSIONS)}"
- )
-
- if file.size > MAX_FILE_SIZE:
- raise HTTPException(
- status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
- detail="File is too large. Maximum allowed size is 25 MB."
- )
+ if extension not in ALLOWED_EXTENSIONS or file.content_type not in ALLOWED_CONTENT_TYPES:
+ raise HTTPException(status_code=400, detail="Unsupported image type")
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
file_uuid = uuid.uuid4()
- saved_name = f"{file_uuid}{extension}"
-
- file_path = f"uploads/{saved_name}"
- file_bytes = await file.read()
-
- with open(file_path, "wb") as buffer:
- buffer.write(file_bytes)
-
- new_attachment = Attachment(
- id=str(file_uuid),
- original_name=file.filename,
- creator_id=current_user.id
- )
+ file_path = UPLOAD_DIR / f"{file_uuid}{extension}"
+ total_size = 0
+ header = b""
+
+ try:
+ with file_path.open("wb") as destination:
+ while chunk := await file.read(CHUNK_SIZE):
+ if not header:
+ header = chunk[:16]
+ total_size += len(chunk)
+ if total_size > MAX_FILE_SIZE:
+ raise HTTPException(
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
+ detail="File is too large. Maximum allowed size is 25 MB.",
+ )
+ destination.write(chunk)
+
+ if not has_valid_image_signature(extension, header):
+ raise HTTPException(status_code=400, detail="File content is not a valid image")
+
+ attachment = Attachment(
+ id=str(file_uuid),
+ original_name=original_name,
+ creator_id=current_user.id,
+ )
+ db.add(attachment)
+ await db.commit()
+ except Exception:
+ file_path.unlink(missing_ok=True)
+ raise
+ finally:
+ await file.close()
- db.add(new_attachment)
- await db.commit()
+ return {"url": f"/api/attachments/download/{file_uuid}"}
- return {"url": f"http://localhost:8000/attachments/download/{file_uuid}"}
@attachments_router.get("/download/{file_uuid}")
async def download_attachment(
- file_uuid: str,
+ file_uuid: uuid.UUID,
db: AsyncSession = Depends(get_db),
- current_user: User = Depends(get_current_user)
+ current_user: User = Depends(get_current_user),
):
- query = select(Attachment).where(Attachment.id == file_uuid)
- result = await db.execute(query)
+ attachment_id = str(file_uuid)
+ result = await db.execute(
+ select(Attachment).where(
+ Attachment.id == attachment_id,
+ Attachment.creator_id == current_user.id,
+ )
+ )
attachment = result.scalar_one_or_none()
-
- if attachment is None or attachment.creator_id != current_user.id:
+ if attachment is None:
raise HTTPException(status_code=404, detail="File not found")
- files = [f for f in os.listdir("uploads") if f.startswith(file_uuid)]
- if not files:
+ files = list(UPLOAD_DIR.glob(f"{attachment_id}.*"))
+ if len(files) != 1:
raise HTTPException(status_code=404, detail="File on disk not found")
- file_name = files[0]
- file_path = f"uploads/{file_name}"
-
- return FileResponse(file_path)
-
+ return FileResponse(files[0], filename=attachment.original_name)
diff --git a/backend/app/api/notes.py b/backend/app/api/notes.py
index d15da08..3e029ef 100644
--- a/backend/app/api/notes.py
+++ b/backend/app/api/notes.py
@@ -1,37 +1,100 @@
+import json
+import re
+
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select
+from sqlalchemy import select, case, desc
from ..models import User, Note
from ..database import get_db
from ..dependencies import get_current_user
-from ..schemas import NoteCreate, NoteUpdate
+from ..schemas import NoteCreate, NoteUpdate, NotePublic
notes_router = APIRouter(prefix="/notes", tags=["Notes"])
+LEGACY_ATTACHMENT_URL = re.compile(
+ r"https?://(?:localhost|127\.0\.0\.1):8000/attachments/download/"
+)
+
+
+def normalize_tags(tags: list[str]) -> list[str]:
+ seen: set[str] = set()
+ normalized: list[str] = []
+
+ for tag in tags:
+ clean = tag.strip().lower()
+ if not clean or clean in seen:
+ continue
+ seen.add(clean)
+ normalized.append(clean[:24])
+
+ return normalized[:12]
+
+def serialize_note(note: Note) -> NotePublic:
+ try:
+ tags = json.loads(note.tags or "[]")
+ except json.JSONDecodeError:
+ tags = []
+
+ normalized_text = LEGACY_ATTACHMENT_URL.sub(
+ "/api/attachments/download/",
+ note.text or "",
+ )
+
+ return NotePublic(
+ id=note.id,
+ title=note.title,
+ text=normalized_text,
+ summary=note.summary,
+ tags=tags if isinstance(tags, list) else [],
+ is_pinned=note.is_pinned,
+ is_favorite=note.is_favorite,
+ is_archived=note.is_archived,
+ created_time=note.created_time,
+ edit_time=note.edit_time,
+ creator_id=note.creator_id,
+ )
+
+
-@notes_router.post("")
+@notes_router.post("", response_model=NotePublic)
async def create_note(userdata: NoteCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
- note = Note(title=userdata.title, text=userdata.text, creator_id=current_user.id)
+ note = Note(
+ title=userdata.title,
+ text=userdata.text,
+ summary=userdata.summary,
+ tags=json.dumps(normalize_tags(userdata.tags)),
+ is_pinned=userdata.is_pinned,
+ is_favorite=userdata.is_favorite,
+ is_archived=userdata.is_archived,
+ creator_id=current_user.id,
+ )
db.add(note)
await db.commit()
await db.refresh(note)
- return note
+ return serialize_note(note)
-@notes_router.get("")
+@notes_router.get("", response_model=list[NotePublic])
async def get_notes(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
- query = select(Note).where(Note.creator_id == current_user.id)
+ query = (
+ select(Note)
+ .where(Note.creator_id == current_user.id)
+ .order_by(
+ desc(case((Note.is_pinned.is_(True), 1), else_=0)),
+ desc(Note.edit_time),
+ )
+ )
result = await db.execute(query)
- return result.scalars().all()
+ return [serialize_note(note) for note in result.scalars().all()]
-@notes_router.get("/{note_id}")
+@notes_router.get("/{note_id}", response_model=NotePublic)
async def get_note(note_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
query = select(Note).where(Note.id == note_id, Note.creator_id == current_user.id)
result = await db.execute(query)
note = result.scalar_one_or_none()
if note is None:
raise HTTPException(status_code=404, detail="Note not found")
- return note
+ return serialize_note(note)
@notes_router.delete("/{note_id}")
async def delete_note(note_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
@@ -44,7 +107,7 @@ async def delete_note(note_id: int, db: AsyncSession = Depends(get_db), current_
await db.commit()
return {"message": "Note deleted successfully"}
-@notes_router.put("/{note_id}")
+@notes_router.put("/{note_id}", response_model=NotePublic)
async def update_note(
note_id: int,
userdata: NoteUpdate,
@@ -60,7 +123,12 @@ async def update_note(
note.title = userdata.title
note.text = userdata.text
+ note.summary = userdata.summary
+ note.tags = json.dumps(normalize_tags(userdata.tags))
+ note.is_pinned = userdata.is_pinned
+ note.is_favorite = userdata.is_favorite
+ note.is_archived = userdata.is_archived
await db.commit()
await db.refresh(note)
- return note
+ return serialize_note(note)
diff --git a/backend/app/api/users.py b/backend/app/api/users.py
index 0e0e646..06a86cd 100644
--- a/backend/app/api/users.py
+++ b/backend/app/api/users.py
@@ -1,17 +1,30 @@
-from fastapi import APIRouter, Depends, HTTPException, Response
+from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
-from ..schemas import Register, Login
+import os
+import time
+
+from ..schemas import Register, Login, ThemeUpdate, UserPublic
from ..database import get_db
from ..models import User
+from ..dependencies import get_current_user
from ..utils import hash_password, verify_password, create_access_token
user_router = APIRouter(prefix="/users", tags=["Users"])
-@user_router.post("/register")
+COOKIE_SECURE = os.getenv("COOKIE_SECURE", "false").lower() == "true"
+REGISTRATION_ENABLED = os.getenv("REGISTRATION_ENABLED", "true").lower() == "true"
+LOGIN_WINDOW_SECONDS = 60
+LOGIN_MAX_ATTEMPTS = 5
+login_attempts: dict[str, list[float]] = {}
+
+@user_router.post("/register", response_model=UserPublic)
async def register(userdata:Register, db: AsyncSession = Depends(get_db)):
+ if not REGISTRATION_ENABLED:
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Registration is disabled")
+
query = select(User).where(User.login == userdata.login)
result = await db.execute(query)
existing_user = result.scalar_one_or_none()
@@ -26,22 +39,39 @@ async def register(userdata:Register, db: AsyncSession = Depends(get_db)):
return user
@user_router.post("/login")
-async def login(userdata:Login, response:Response, db: AsyncSession = Depends(get_db)):
+async def login(userdata:Login, request: Request, response:Response, db: AsyncSession = Depends(get_db)):
+ client_id = request.client.host if request.client else "unknown"
+ now = time.monotonic()
+ recent_attempts = [
+ attempt for attempt in login_attempts.get(client_id, [])
+ if now - attempt < LOGIN_WINDOW_SECONDS
+ ]
+ if len(recent_attempts) >= LOGIN_MAX_ATTEMPTS:
+ raise HTTPException(
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
+ detail="Too many login attempts. Try again in one minute.",
+ )
+
query = select(User).where(User.login == userdata.login)
result = await db.execute(query)
user = result.scalar_one_or_none()
if user is None:
+ login_attempts[client_id] = [*recent_attempts, now]
raise HTTPException(status_code=401, detail="Invalid credentials")
if not verify_password(userdata.password, user.pass_hash):
+ login_attempts[client_id] = [*recent_attempts, now]
raise HTTPException(status_code=401, detail="Invalid credentials")
+ login_attempts.pop(client_id, None)
+
token = create_access_token(user_id=user.id)
response.set_cookie(
key="access_token",
value=token,
httponly=True,
+ secure=COOKIE_SECURE,
samesite="lax",
max_age=1800
)
@@ -51,7 +81,22 @@ async def login(userdata:Login, response:Response, db: AsyncSession = Depends(ge
@user_router.post("/logout")
async def logout(response: Response):
- response.delete_cookie(key="access_token")
+ response.delete_cookie(key="access_token", secure=COOKIE_SECURE, samesite="lax")
return {"message": "Logout successful"}
+
+@user_router.get("/me", response_model=UserPublic)
+async def current_user(current_user: User = Depends(get_current_user)):
+ return current_user
+
+@user_router.patch("/me/theme", response_model=UserPublic)
+async def update_theme(
+ payload: ThemeUpdate,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ current_user.theme = payload.theme
+ await db.commit()
+ await db.refresh(current_user)
+ return current_user
diff --git a/backend/app/database.py b/backend/app/database.py
index 86d9970..120df60 100644
--- a/backend/app/database.py
+++ b/backend/app/database.py
@@ -3,8 +3,9 @@
from sqlalchemy.orm import DeclarativeBase
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://dev_user:dev_password@db:5432/dev_db")
+SQL_ECHO = os.getenv("SQL_ECHO", "false").lower() == "true"
-engine = create_async_engine(DATABASE_URL, echo=True)
+engine = create_async_engine(DATABASE_URL, echo=SQL_ECHO, pool_pre_ping=True)
AsyncSessionLocal = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
diff --git a/backend/app/main.py b/backend/app/main.py
index 8fa59eb..c4e47bc 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,19 +1,18 @@
import os
import asyncio
+import shutil
+from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI
-from sqlalchemy.ext.asyncio import create_async_engine
+from fastapi.middleware.cors import CORSMiddleware
-from .database import Base
+from .database import Base, engine
from .models import User, Note
from .api.users import user_router
from .api.notes import notes_router
from .api.system import system_router
from .api.attachments import attachments_router
-DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://admin:admin@db:5432/main_db")
-engine = create_async_engine(DATABASE_URL, echo=True)
-
@asynccontextmanager
async def lifespan(app: FastAPI):
retries = 5
@@ -26,10 +25,44 @@ async def lifespan(app: FastAPI):
except Exception as e:
retries -= 1
print(f"Database is not ready yet. Retrying in 2 seconds... ({retries} retries left)")
+ if retries == 0:
+ raise RuntimeError("Could not connect to the database") from e
await asyncio.sleep(2)
- yield
-app = FastAPI(lifespan=lifespan, swagger_ui_parameters={"withCredentials": True})
+ upload_dir = Path(os.getenv("UPLOAD_DIR", "uploads"))
+ legacy_upload_dir = Path("/legacy-uploads")
+ upload_dir.mkdir(parents=True, exist_ok=True)
+ if legacy_upload_dir.is_dir():
+ for legacy_file in legacy_upload_dir.iterdir():
+ destination = upload_dir / legacy_file.name
+ if legacy_file.is_file() and legacy_file.name != "README.md" and not destination.exists():
+ shutil.copy2(legacy_file, destination)
+
+ try:
+ yield
+ finally:
+ await engine.dispose()
+
+app = FastAPI(
+ lifespan=lifespan,
+ root_path=os.getenv("ROOT_PATH", ""),
+ swagger_ui_parameters={"withCredentials": True},
+)
+
+cors_origins = [
+ origin.strip()
+ for origin in os.getenv("CORS_ORIGINS", "").split(",")
+ if origin.strip()
+]
+
+if cors_origins:
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=cors_origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
app.include_router(user_router)
app.include_router(notes_router)
@@ -39,5 +72,3 @@ async def lifespan(app: FastAPI):
@app.get("/")
async def root():
return {"message": "Hello from FastAPI backend!"}
-
-
diff --git a/backend/app/models.py b/backend/app/models.py
index 0c8ac0a..dc840c9 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -1,5 +1,5 @@
from datetime import datetime
-from sqlalchemy import String, Integer, ForeignKey, Text, DateTime
+from sqlalchemy import String, Integer, ForeignKey, Text, DateTime, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .database import Base
@@ -21,6 +21,12 @@ class Note(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(100), nullable=False)
text: Mapped[str] = mapped_column(Text, nullable=True)
+ summary: Mapped[str | None] = mapped_column(String(280), nullable=True)
+ tags: Mapped[str] = mapped_column(Text, default="[]", nullable=False)
+ is_pinned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ is_favorite: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ is_archived: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ created_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
edit_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
creator_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
@@ -32,4 +38,4 @@ class Attachment(Base):
id: Mapped[str] = mapped_column(String(256), primary_key=True)
original_name: Mapped[str] = mapped_column(String(256), nullable=False)
- creator_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
+ creator_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
\ No newline at end of file
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 5873e23..de8d0d1 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -1,17 +1,55 @@
+from datetime import datetime
+from typing import Literal
+
from pydantic import BaseModel, Field
class Register(BaseModel):
- login: str
- password: str
+ login: str = Field(..., min_length=3, max_length=50)
+ password: str = Field(..., min_length=8, max_length=128)
class Login(BaseModel):
- login: str
- password: str
+ login: str = Field(..., min_length=3, max_length=50)
+ password: str = Field(..., min_length=8, max_length=128)
+
+class ThemeUpdate(BaseModel):
+ theme: Literal["light", "dark"]
class NoteCreate(BaseModel):
- title: str
- text: str
+ title: str = Field(..., min_length=1, max_length=100)
+ text: str | None = None
+ summary: str | None = Field(default=None, max_length=280)
+ tags: list[str] = Field(default_factory=list, max_length=12)
+ is_pinned: bool = False
+ is_favorite: bool = False
+ is_archived: bool = False
class NoteUpdate(BaseModel):
title: str = Field(..., min_length=1, max_length=100)
text: str | None = None
+ summary: str | None = Field(default=None, max_length=280)
+ tags: list[str] = Field(default_factory=list, max_length=12)
+ is_pinned: bool = False
+ is_favorite: bool = False
+ is_archived: bool = False
+
+class NotePublic(BaseModel):
+ id: int
+ title: str
+ text: str | None
+ summary: str | None
+ tags: list[str]
+ is_pinned: bool
+ is_favorite: bool
+ is_archived: bool
+ created_time: datetime
+ edit_time: datetime
+ creator_id: int
+
+ model_config = {"from_attributes": True}
+
+class UserPublic(BaseModel):
+ id: int
+ login: str
+ theme: str
+
+ model_config = {"from_attributes": True}
diff --git a/backend/app/utils.py b/backend/app/utils.py
index 06c6946..af49eb6 100644
--- a/backend/app/utils.py
+++ b/backend/app/utils.py
@@ -1,9 +1,11 @@
from passlib.context import CryptContext
+import os
+import secrets
import jwt
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
-SECRET_KEY = "SUPER_SECRET_KEY_CHANGEME_IN_PRODUCTION"
+SECRET_KEY = os.getenv("JWT_SECRET_KEY") or secrets.token_urlsafe(48)
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
@@ -17,7 +19,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(user_id: int) -> str:
- expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {
"sub": str(user_id),
diff --git a/docker-compose.yml b/docker-compose.yml
index 978de4e..35e3f16 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,18 +1,18 @@
-version: '3.8'
-
services:
db:
image: postgres:16-alpine
- container_name: backend_postgres
- restart: always
+ restart: unless-stopped
environment:
- POSTGRES_USER: admin
- POSTGRES_PASSWORD: admin
- POSTGRES_DB: main_db
- ports:
- - "5432:5432"
+ POSTGRES_USER: ${POSTGRES_USER:-admin}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin}
+ POSTGRES_DB: ${POSTGRES_DB:-main_db}
volumes:
- pgdata:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
networks:
- app-network
@@ -20,23 +20,56 @@ services:
build:
context: ./backend
dockerfile: Dockerfile
- container_name: fastapi_server
- restart: always
- ports:
- - "8000:8000"
+ restart: unless-stopped
environment:
- DATABASE_URL: "postgresql+asyncpg://admin:admin@db:5432/main_db"
+ DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-admin}:${POSTGRES_PASSWORD:-admin}@db:5432/${POSTGRES_DB:-main_db}
+ JWT_SECRET_KEY: ${JWT_SECRET_KEY:-}
+ COOKIE_SECURE: ${COOKIE_SECURE:-false}
+ SQL_ECHO: ${SQL_ECHO:-false}
+ REGISTRATION_ENABLED: ${REGISTRATION_ENABLED:-true}
+ UPLOAD_DIR: /data/uploads
+ ROOT_PATH: /api
volumes:
- - ./backend:/code
+ - uploads:/data/uploads
+ - ./backend/uploads:/legacy-uploads:ro
+ expose:
+ - "8000"
depends_on:
- - db
+ db:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/system/health-check', timeout=3)"]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ start_period: 5s
+ networks:
+ - app-network
+
+ frontend:
+ build:
+ context: ./frontend
+ dockerfile: Dockerfile
+ args:
+ VITE_API_BASE_URL: /api
+ restart: unless-stopped
+ ports:
+ - "${APP_PORT:-8080}:80"
+ depends_on:
+ server:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
networks:
- app-network
volumes:
pgdata:
+ uploads:
networks:
app-network:
driver: bridge
-
diff --git a/frontend/.dockerignore b/frontend/.dockerignore
new file mode 100644
index 0000000..6bf4ecd
--- /dev/null
+++ b/frontend/.dockerignore
@@ -0,0 +1,9 @@
+node_modules
+dist
+.vite
+.git
+.gitignore
+*.log
+.env
+.env.*
+!.env.example
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 0000000..14ea4ad
--- /dev/null
+++ b/frontend/.env.example
@@ -0,0 +1 @@
+VITE_API_BASE_URL=/api
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json
new file mode 100644
index 0000000..6fa991d
--- /dev/null
+++ b/frontend/.oxlintrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..e67eaa2
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,22 @@
+FROM node:24-alpine AS build
+
+WORKDIR /app
+
+COPY package*.json ./
+RUN npm ci
+
+COPY . .
+
+ARG VITE_API_BASE_URL=/api
+ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
+
+RUN npm run build
+
+FROM nginx:1.29-alpine
+
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist /usr/share/nginx/html
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..a831c41
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,11 @@
+# Cloud Notes Frontend
+
+React, TypeScript, Vite, and TipTap frontend for Cloud Notes.
+
+The production image is built with `frontend/Dockerfile` and served by Nginx. API requests use the relative `/api` path and are proxied to the FastAPI service inside the Docker network.
+
+For the recommended full-stack setup, follow the repository root `README.md` and run:
+
+```bash
+docker compose up -d --build
+```
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..0fca6f0
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ frontend
+
+
+
+
+
+
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 0000000..9854923
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,32 @@
+server {
+ listen 80;
+ server_name _;
+ client_max_body_size 26m;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "same-origin" always;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+
+ location /api/ {
+ proxy_pass http://server:8000/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 60s;
+ }
+
+ location /assets/ {
+ try_files $uri =404;
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..add280a
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,2244 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tiptap/extension-color": "^3.27.4",
+ "@tiptap/extension-highlight": "^3.27.4",
+ "@tiptap/extension-image": "^3.27.4",
+ "@tiptap/extension-link": "^3.27.4",
+ "@tiptap/extension-placeholder": "^3.27.4",
+ "@tiptap/extension-table": "^3.27.4",
+ "@tiptap/extension-table-cell": "^3.27.4",
+ "@tiptap/extension-table-header": "^3.27.4",
+ "@tiptap/extension-table-row": "^3.27.4",
+ "@tiptap/extension-task-item": "^3.27.4",
+ "@tiptap/extension-task-list": "^3.27.4",
+ "@tiptap/extension-text-align": "^3.27.4",
+ "@tiptap/extension-text-style": "^3.27.4",
+ "@tiptap/extension-underline": "^3.27.4",
+ "@tiptap/react": "^3.27.4",
+ "@tiptap/starter-kit": "^3.27.4",
+ "lucide-react": "^1.24.0",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7"
+ },
+ "devDependencies": {
+ "@types/node": "^24.13.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.3",
+ "oxlint": "^1.71.0",
+ "typescript": "~6.0.2",
+ "vite": "^8.1.1"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
+ "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
+ "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/core": "^1.8.0",
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
+ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.139.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm-eabi": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz",
+ "integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz",
+ "integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-arm64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz",
+ "integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-x64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz",
+ "integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-freebsd-x64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz",
+ "integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz",
+ "integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz",
+ "integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz",
+ "integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-musl": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz",
+ "integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz",
+ "integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz",
+ "integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz",
+ "integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz",
+ "integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz",
+ "integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-musl": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz",
+ "integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-openharmony-arm64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz",
+ "integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz",
+ "integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz",
+ "integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.73.0.tgz",
+ "integrity": "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tiptap/core": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.27.4.tgz",
+ "integrity": "sha512-8W/GwlEn0JwNdpyVfTWcXwHYUpj9BWwO++YxtizmgjJzlwigSh7/xLVJMwVykuQHQ2fCq5rkUvmBRtpHOMLUQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-blockquote": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.27.4.tgz",
+ "integrity": "sha512-d1tOHgP3R5cOE+Ot8qL/dkLXRByajgn+j6cCXHqDtmJO2wsK9knmbKQ0SEjbKrU6OgHrTnY/EotNxBEBW9HGoA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bold": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.27.4.tgz",
+ "integrity": "sha512-wTtJUUAxCAZ01ICH2DNlOBzzHKRQ1ZST8aRYtIhBPzqEUhnJaKGcjnDB4X49fqPi48iXaPxzhsInDl+rVUujWg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bubble-menu": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.27.4.tgz",
+ "integrity": "sha512-Poy7xwcD3POG5ew/TW7mYXv7m++vCchvHxPUqIfnTxBxvvvqDZkPYFWZS1lvPrSBtm1DcfUTQAgVutM5NDZ99Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bullet-list": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.27.4.tgz",
+ "integrity": "sha512-rvja0N1RnwGJAVwDdbUfDIJ4NoT+KjPFaZudKiPuEMfMHfbqe4xcbbC2hsfs61JNcl2xmx+ohV6lzD9YxxJl1w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-code": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.27.4.tgz",
+ "integrity": "sha512-aPc7opCR1ylK4m4c2lsjLsGpEBD1fLQQKWd5PbZiJvrTF8gkdGZlYLt9A6VukpxeJyHhb22Jaj4fxgKmGMeTtw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-code-block": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.27.4.tgz",
+ "integrity": "sha512-a5caWfWN6Z6usy48vzJDDOhWoA6+rFFCHGpQM7jXn/7rRzYPcvBzTZUGptjEbltj4YqtrQ2tVwTJcCtbb+mknA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-color": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.27.4.tgz",
+ "integrity": "sha512-uGbgErKGKO4OTBGqnXOA1CGXa3IqoBaiPBGcHu/px01fS8EVFu0A7q5HS6vxFiMK6qW5x71sEpCA+FUINMAb3g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-text-style": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-document": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.27.4.tgz",
+ "integrity": "sha512-7nAqgfkgb9HADBeCTnOHuTiyZuxfxvMPT3nH4OZeY+cmtkI1On3QffqlmtcUPvNbkhT3o9ehA1hVfCnQ1Ye4LQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-dropcursor": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.27.4.tgz",
+ "integrity": "sha512-RiZasQJuUTUO3aME16Bn8eJH7cYnvhT5JCFDFq0ya/1iFI9wUQA2NJC5tb5TrZ74+sQwkYU9VzexnchM481Y9w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-floating-menu": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.27.4.tgz",
+ "integrity": "sha512-tnZywwoNDuEcUZmYYIztXl3PpIKUq+gKeaYPuZhpYEVTThU44tzK3ZuFOmd+qf2aAa1MQwxKWqUuLpNK77bwNw==",
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@floating-ui/dom": "^1.0.0",
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-gapcursor": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.27.4.tgz",
+ "integrity": "sha512-svLwSKcFhzpcJeXvxxKkRFuQpykmXrQefVhEsaXq0L95yJIIAGKMRmQC3mxKdzL2j0P9cY7V41bNVSyOAyvclw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-hard-break": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.27.4.tgz",
+ "integrity": "sha512-W+Z9pmDgqjbdu3NeZOQrzA15iM4w60Yd8l2CYzxcdApPVIfYzb2S3a7+u1RqW9wnTYb6xyZjASmFNfxXS4P4cg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-heading": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.27.4.tgz",
+ "integrity": "sha512-RgvpxzuYk6QEK+az+eiXpWvGlUso42zNcGnjyUrvskoZjS47MbhSg8ylRYQSRtXE0ETlXhAx4J7iGlGr72kyIw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-highlight": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.27.4.tgz",
+ "integrity": "sha512-STRX1qJLhTZslBF8fEE5qpTGrFd/g7Ufidjxt84p0uT8FrtcbfPUwyweeYwhZp7Iw8n+qODGYnheJTOpfaW2JA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-horizontal-rule": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.27.4.tgz",
+ "integrity": "sha512-2eQU/55nE5mhMJHALtLMuBL3dcVJUDVVT7n+uZYMaYE63BtCvC4VS08YLFSR7JZSVJIlgVAmdt5nAw0B+rEPNA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-image": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.27.4.tgz",
+ "integrity": "sha512-yQ8CazyOL4z1/NbV1NLGv6DvchVhOXHH3uQ7md5VX/IGZruFpnm8IpF9MDpdAUxUFmTsXJDYyO2lWGO1PYWG8A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-italic": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.27.4.tgz",
+ "integrity": "sha512-PeZT4XbyxAp7Lqo/hfA1k5LI27g1RlgS+YgXp2CeHXIrUfSpO5HlZXh02Bvb0pOdl3RFw2tEKtlHzjt8Y1+Nwg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-link": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.27.4.tgz",
+ "integrity": "sha512-6K/FkNwMLWWQbNWKlycrUPTN7YcyVFdFwZncoBXe5WyarRjLTGw7ywafnCI9PDIWSq7ttzVL4NgjN2IN8kBXww==",
+ "license": "MIT",
+ "dependencies": {
+ "linkifyjs": "^4.3.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.27.4.tgz",
+ "integrity": "sha512-A0BgmRO1RE0yLCx9w7GQITtKfS9wLE5cdngSYDiSpwulcXJhJjKm5mZ4OUZmks2VN4HO5jMl2BWCGt2NSDhA+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list-item": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.27.4.tgz",
+ "integrity": "sha512-z5TVuPw2mkK0B/x+gFg3uUV7tBdaElDFg0zVgnXZCqlSVTLfIyInOOnG5LTWoAd9BdzBjGrzE3PohDcLVDDGBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list-keymap": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.27.4.tgz",
+ "integrity": "sha512-on7JNDi7Eqz7UdZeZdiO83bQHo0flVDHzjmtR+v/nrCGW9H15D3CHs5+4ozLDiCvTK8tbkBuut/l9AWNxcCE/Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-ordered-list": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.27.4.tgz",
+ "integrity": "sha512-bHwLiof0FqJfWzB0act7oEKMTZatEKQ4IYCvmyF5EktjMs4kxEatkPp4Yx/1LSYSjLy1MMT7oLELyaz2FFYyXA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-paragraph": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.27.4.tgz",
+ "integrity": "sha512-8Dnr1J5s/s4XYYuEF3b784NnCxLjXOlQpmGyXRxTAzW7JaOP08tIUJWVNvSMekfXc2vXa33HUbqjxyyWZEQ6LQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-placeholder": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.27.4.tgz",
+ "integrity": "sha512-7hBoFLeddCv1WzkqB0x3coZ1Hp9WZ9wLoRXIUtUhRKMpzFq2IlTtW1iw88g9pTJnL98bCCElN4DZ4mYtaQvmgA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-strike": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.27.4.tgz",
+ "integrity": "sha512-8OXwcPKuV3ToBBgyvDxH1jQdObK5FIKCGiyIim6qNWiOpi9BhM3XYD+aO1khjv8qIjtoI/DYbizF4ewj09fX2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-table": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.27.4.tgz",
+ "integrity": "sha512-ejQjqt8GjUn4YswG/SsiLr/W3LZApZGUEDW0N7NoOduE0dBZ/pVJHPuqWu33kK+phJjSNCIN+bSAkoWE01rZSw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-table-cell": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.27.4.tgz",
+ "integrity": "sha512-1B7J4ZiaXaGxT2IB3hrwtz0433bFVSWsDB+B125vt2DZQr9bgdX40GLesSPlymqOdMxKJwksCPMan2ON2LkVAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-table": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-table-header": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.27.4.tgz",
+ "integrity": "sha512-V/O690Z6VcHMAWDfgVFiOCwx3eE2/HM2gH+Fp0eQe3WnajQj2DPViW4VWR/rNVl3DspHoL/EU+eEsnhcvO34Vw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-table": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-table-row": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.27.4.tgz",
+ "integrity": "sha512-2RTJtcy90Tc2+HpW1JyKMTwg/zz4u6hHof4CTcJS/eXLk3g1L60DDzYvz4GzBxQvsOT3QVcbaU9QOk9A3Sx7Fg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-table": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-task-item": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-3.27.4.tgz",
+ "integrity": "sha512-0/sGxmuUgintoCIh7fF7bblM1Hlr5W498njBLUPSJAd7cO78XXCjoB/UUEiCoBEPEyhHcDYesnYdZxsNfjChGg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-task-list": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-3.27.4.tgz",
+ "integrity": "sha512-nlFw+pOhnj9pF1wN0CyjqL0JppM6KNw05XtZWD5kt68tBUpBb7+aEy02tDXpZoRefiFhcIouKVJSeSzILZlCrA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.27.4.tgz",
+ "integrity": "sha512-lKQH/hP4FBXsziHypd6Ywj8JFvMLM5GVkK1xsH6yApNuXbHq95rd42ZOYWpYILIBib7tlaz93z61d74UrJiuiw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text-align": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.27.4.tgz",
+ "integrity": "sha512-ArfL7GLOXCSmrmiBiaWwAf7RPHXDdxLGPru29qKDLQDthjXcNOdlwlPBbolRu7mXwlSmaYpqWGrG75/AEat3mw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text-style": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.27.4.tgz",
+ "integrity": "sha512-Wmj64TQXY85gc7lUNbubW32sDCnVOJlpGraMeARRC9Z0EKBX1JpAxddo/64F17bn3kzLxXVszBFyLRcNgTdU2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extension-underline": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.27.4.tgz",
+ "integrity": "sha512-nRJGvRyEXDtINlHTW+C2oWcL3vmX1URVxAPpkD3Zwn5Rb/vEeOU/pk/w97I0iid816MR4iVbvl1XhbUVegK9gQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/extensions": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.27.4.tgz",
+ "integrity": "sha512-d8opkg2iGtVwJmNGIqv0blfRxnvWOJp1brz+Z8CsP4ojSS2ZtaE46d6JSQ5OeJ7nMpjhT+9wh4UQcA7OSEO59w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4"
+ }
+ },
+ "node_modules/@tiptap/pm": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.27.4.tgz",
+ "integrity": "sha512-UB8lcyomfWk7YGI2PZKNqcYXfyRA+PFj+QntlsUXyrsiA5JJIaE8SHKYjxKlGG/xtW3EtPm1b0p38T9Mk4xiFw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-changeset": "^2.4.1",
+ "prosemirror-commands": "^1.7.1",
+ "prosemirror-dropcursor": "^1.8.2",
+ "prosemirror-gapcursor": "^1.4.1",
+ "prosemirror-history": "^1.5.0",
+ "prosemirror-inputrules": "^1.5.1",
+ "prosemirror-keymap": "^1.2.3",
+ "prosemirror-model": "^1.25.9",
+ "prosemirror-schema-list": "^1.5.1",
+ "prosemirror-state": "^1.4.4",
+ "prosemirror-tables": "^1.8.5",
+ "prosemirror-transform": "^1.12.0",
+ "prosemirror-view": "^1.41.9"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
+ "node_modules/@tiptap/react": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.27.4.tgz",
+ "integrity": "sha512-rTY1V9Y1jzwmo5ItRi3v2Og/mbcYsr9AjUvGoqpXzR9Z31WhXYphw0y05aYzryh0MHXYzkiE+gbGvrbg+cjwEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "fast-equals": "^5.3.3",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "optionalDependencies": {
+ "@tiptap/extension-bubble-menu": "^3.27.4",
+ "@tiptap/extension-floating-menu": "^3.27.4"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.27.4",
+ "@tiptap/pm": "3.27.4",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tiptap/starter-kit": {
+ "version": "3.27.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.27.4.tgz",
+ "integrity": "sha512-/sb6rFxNt5BO4hWpUwvHh+Yh1kNyCQuuz3oDpGef5HUUjSdu9p9rfNiHWIUKBadK8VXuw5es7N+UlZ4hma+gvA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tiptap/core": "^3.27.4",
+ "@tiptap/extension-blockquote": "^3.27.4",
+ "@tiptap/extension-bold": "^3.27.4",
+ "@tiptap/extension-bullet-list": "^3.27.4",
+ "@tiptap/extension-code": "^3.27.4",
+ "@tiptap/extension-code-block": "^3.27.4",
+ "@tiptap/extension-document": "^3.27.4",
+ "@tiptap/extension-dropcursor": "^3.27.4",
+ "@tiptap/extension-gapcursor": "^3.27.4",
+ "@tiptap/extension-hard-break": "^3.27.4",
+ "@tiptap/extension-heading": "^3.27.4",
+ "@tiptap/extension-horizontal-rule": "^3.27.4",
+ "@tiptap/extension-italic": "^3.27.4",
+ "@tiptap/extension-link": "^3.27.4",
+ "@tiptap/extension-list": "^3.27.4",
+ "@tiptap/extension-list-item": "^3.27.4",
+ "@tiptap/extension-list-keymap": "^3.27.4",
+ "@tiptap/extension-ordered-list": "^3.27.4",
+ "@tiptap/extension-paragraph": "^3.27.4",
+ "@tiptap/extension-strike": "^3.27.4",
+ "@tiptap/extension-text": "^3.27.4",
+ "@tiptap/extension-underline": "^3.27.4",
+ "@tiptap/extensions": "^3.27.4",
+ "@tiptap/pm": "^3.27.4"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
+ "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fast-equals": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz",
+ "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/linkifyjs": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
+ "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
+ "license": "MIT"
+ },
+ "node_modules/lucide-react": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz",
+ "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/orderedmap": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
+ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
+ "license": "MIT"
+ },
+ "node_modules/oxlint": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz",
+ "integrity": "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "oxlint": "bin/oxlint"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxlint/binding-android-arm-eabi": "1.73.0",
+ "@oxlint/binding-android-arm64": "1.73.0",
+ "@oxlint/binding-darwin-arm64": "1.73.0",
+ "@oxlint/binding-darwin-x64": "1.73.0",
+ "@oxlint/binding-freebsd-x64": "1.73.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.73.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.73.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.73.0",
+ "@oxlint/binding-linux-arm64-musl": "1.73.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.73.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.73.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.73.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.73.0",
+ "@oxlint/binding-linux-x64-gnu": "1.73.0",
+ "@oxlint/binding-linux-x64-musl": "1.73.0",
+ "@oxlint/binding-openharmony-arm64": "1.73.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.73.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.73.0",
+ "@oxlint/binding-win32-x64-msvc": "1.73.0"
+ },
+ "peerDependencies": {
+ "oxlint-tsgolint": ">=0.24.0",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "oxlint-tsgolint": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.19",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
+ "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prosemirror-changeset": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
+ "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-commands": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
+ "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.10.2"
+ }
+ },
+ "node_modules/prosemirror-dropcursor": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz",
+ "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0",
+ "prosemirror-view": "^1.1.0"
+ }
+ },
+ "node_modules/prosemirror-gapcursor": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
+ "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-keymap": "^1.0.0",
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-view": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-history": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
+ "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.2.2",
+ "prosemirror-transform": "^1.0.0",
+ "prosemirror-view": "^1.31.0",
+ "rope-sequence": "^1.3.0"
+ }
+ },
+ "node_modules/prosemirror-inputrules": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
+ "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-keymap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
+ "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "w3c-keyname": "^2.2.0"
+ }
+ },
+ "node_modules/prosemirror-model": {
+ "version": "1.25.11",
+ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz",
+ "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "orderedmap": "^2.0.0"
+ }
+ },
+ "node_modules/prosemirror-schema-list": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
+ "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.7.3"
+ }
+ },
+ "node_modules/prosemirror-state": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
+ "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-transform": "^1.0.0",
+ "prosemirror-view": "^1.27.0"
+ }
+ },
+ "node_modules/prosemirror-tables": {
+ "version": "1.8.5",
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
+ "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-keymap": "^1.2.3",
+ "prosemirror-model": "^1.25.4",
+ "prosemirror-state": "^1.4.4",
+ "prosemirror-transform": "^1.10.5",
+ "prosemirror-view": "^1.41.4"
+ }
+ },
+ "node_modules/prosemirror-transform": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
+ "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.21.0"
+ }
+ },
+ "node_modules/prosemirror-view": {
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.1.tgz",
+ "integrity": "sha512-rRqzZnRgkyh69XoOMrfFJHwauHscLBmHbq772kwbic1ymQAM8gXjzEbJse5j1ep2UO2HRIAQL0bY3kZ/RoqjVw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.25.8",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.139.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.5",
+ "@rolldown/binding-darwin-arm64": "1.1.5",
+ "@rolldown/binding-darwin-x64": "1.1.5",
+ "@rolldown/binding-freebsd-x64": "1.1.5",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-musl": "1.1.5",
+ "@rolldown/binding-openharmony-arm64": "1.1.5",
+ "@rolldown/binding-wasm32-wasi": "1.1.5",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
+ }
+ },
+ "node_modules/rope-sequence": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
+ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.1.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
+ "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.16",
+ "rolldown": "~1.1.4",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..be1ffa7
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "oxlint",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tiptap/extension-color": "^3.27.4",
+ "@tiptap/extension-highlight": "^3.27.4",
+ "@tiptap/extension-image": "^3.27.4",
+ "@tiptap/extension-link": "^3.27.4",
+ "@tiptap/extension-placeholder": "^3.27.4",
+ "@tiptap/extension-table": "^3.27.4",
+ "@tiptap/extension-table-cell": "^3.27.4",
+ "@tiptap/extension-table-header": "^3.27.4",
+ "@tiptap/extension-table-row": "^3.27.4",
+ "@tiptap/extension-task-item": "^3.27.4",
+ "@tiptap/extension-task-list": "^3.27.4",
+ "@tiptap/extension-text-align": "^3.27.4",
+ "@tiptap/extension-text-style": "^3.27.4",
+ "@tiptap/extension-underline": "^3.27.4",
+ "@tiptap/react": "^3.27.4",
+ "@tiptap/starter-kit": "^3.27.4",
+ "lucide-react": "^1.24.0",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7"
+ },
+ "devDependencies": {
+ "@types/node": "^24.13.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.3",
+ "oxlint": "^1.71.0",
+ "typescript": "~6.0.2",
+ "vite": "^8.1.1"
+ }
+}
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
new file mode 100644
index 0000000..af6388e
--- /dev/null
+++ b/frontend/pnpm-lock.yaml
@@ -0,0 +1,1483 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@tiptap/extension-color':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/extension-text-style@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4)))
+ '@tiptap/extension-highlight':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-image':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-link':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/extension-placeholder':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/extensions@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-table':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/extension-table-cell':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/extension-table@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-table-header':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/extension-table@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-table-row':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/extension-table@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-task-item':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-task-list':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-text-align':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-text-style':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-underline':
+ specifier: ^3.27.4
+ version: 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/react':
+ specifier: ^3.27.4
+ version: 3.27.4(@floating-ui/dom@1.8.0)(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@tiptap/starter-kit':
+ specifier: ^3.27.4
+ version: 3.27.4
+ lucide-react:
+ specifier: ^1.24.0
+ version: 1.24.0(react@19.2.7)
+ react:
+ specifier: ^19.2.7
+ version: 19.2.7
+ react-dom:
+ specifier: ^19.2.7
+ version: 19.2.7(react@19.2.7)
+ devDependencies:
+ '@types/node':
+ specifier: ^24.13.2
+ version: 24.13.3
+ '@types/react':
+ specifier: ^19.2.17
+ version: 19.2.17
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.17)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.3
+ version: 6.0.3(vite@8.1.4(@types/node@24.13.3))
+ oxlint:
+ specifier: ^1.71.0
+ version: 1.74.0
+ typescript:
+ specifier: ~6.0.2
+ version: 6.0.3
+ vite:
+ specifier: ^8.1.1
+ version: 8.1.4(@types/node@24.13.3)
+
+packages:
+
+ '@emnapi/core@1.11.1':
+ resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
+
+ '@emnapi/runtime@1.11.1':
+ resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
+
+ '@emnapi/wasi-threads@1.2.2':
+ resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+
+ '@floating-ui/core@1.8.0':
+ resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
+
+ '@floating-ui/dom@1.8.0':
+ resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
+
+ '@floating-ui/utils@0.2.12':
+ resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
+
+ '@napi-rs/wasm-runtime@1.1.6':
+ resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.139.0':
+ resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==}
+
+ '@oxlint/binding-android-arm-eabi@1.74.0':
+ resolution: {integrity: sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxlint/binding-android-arm64@1.74.0':
+ resolution: {integrity: sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxlint/binding-darwin-arm64@1.74.0':
+ resolution: {integrity: sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxlint/binding-darwin-x64@1.74.0':
+ resolution: {integrity: sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxlint/binding-freebsd-x64@1.74.0':
+ resolution: {integrity: sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.74.0':
+ resolution: {integrity: sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm-musleabihf@1.74.0':
+ resolution: {integrity: sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-gnu@1.74.0':
+ resolution: {integrity: sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-arm64-musl@1.74.0':
+ resolution: {integrity: sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxlint/binding-linux-ppc64-gnu@1.74.0':
+ resolution: {integrity: sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-riscv64-gnu@1.74.0':
+ resolution: {integrity: sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-riscv64-musl@1.74.0':
+ resolution: {integrity: sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxlint/binding-linux-s390x-gnu@1.74.0':
+ resolution: {integrity: sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-x64-gnu@1.74.0':
+ resolution: {integrity: sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-x64-musl@1.74.0':
+ resolution: {integrity: sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxlint/binding-openharmony-arm64@1.74.0':
+ resolution: {integrity: sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxlint/binding-win32-arm64-msvc@1.74.0':
+ resolution: {integrity: sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxlint/binding-win32-ia32-msvc@1.74.0':
+ resolution: {integrity: sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxlint/binding-win32-x64-msvc@1.74.0':
+ resolution: {integrity: sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/binding-android-arm64@1.1.5':
+ resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.1.5':
+ resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.1.5':
+ resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.1.5':
+ resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.5':
+ resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.5':
+ resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.1.5':
+ resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.5':
+ resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.5':
+ resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.1.5':
+ resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.1.5':
+ resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.1.5':
+ resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.1.5':
+ resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.5':
+ resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.1.5':
+ resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@tiptap/core@3.27.4':
+ resolution: {integrity: sha512-8W/GwlEn0JwNdpyVfTWcXwHYUpj9BWwO++YxtizmgjJzlwigSh7/xLVJMwVykuQHQ2fCq5rkUvmBRtpHOMLUQA==}
+ peerDependencies:
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-blockquote@3.27.4':
+ resolution: {integrity: sha512-d1tOHgP3R5cOE+Ot8qL/dkLXRByajgn+j6cCXHqDtmJO2wsK9knmbKQ0SEjbKrU6OgHrTnY/EotNxBEBW9HGoA==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-bold@3.27.4':
+ resolution: {integrity: sha512-wTtJUUAxCAZ01ICH2DNlOBzzHKRQ1ZST8aRYtIhBPzqEUhnJaKGcjnDB4X49fqPi48iXaPxzhsInDl+rVUujWg==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-bubble-menu@3.27.4':
+ resolution: {integrity: sha512-Poy7xwcD3POG5ew/TW7mYXv7m++vCchvHxPUqIfnTxBxvvvqDZkPYFWZS1lvPrSBtm1DcfUTQAgVutM5NDZ99Q==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-bullet-list@3.27.4':
+ resolution: {integrity: sha512-rvja0N1RnwGJAVwDdbUfDIJ4NoT+KjPFaZudKiPuEMfMHfbqe4xcbbC2hsfs61JNcl2xmx+ohV6lzD9YxxJl1w==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.27.4
+
+ '@tiptap/extension-code-block@3.27.4':
+ resolution: {integrity: sha512-a5caWfWN6Z6usy48vzJDDOhWoA6+rFFCHGpQM7jXn/7rRzYPcvBzTZUGptjEbltj4YqtrQ2tVwTJcCtbb+mknA==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-code@3.27.4':
+ resolution: {integrity: sha512-aPc7opCR1ylK4m4c2lsjLsGpEBD1fLQQKWd5PbZiJvrTF8gkdGZlYLt9A6VukpxeJyHhb22Jaj4fxgKmGMeTtw==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-color@3.27.4':
+ resolution: {integrity: sha512-uGbgErKGKO4OTBGqnXOA1CGXa3IqoBaiPBGcHu/px01fS8EVFu0A7q5HS6vxFiMK6qW5x71sEpCA+FUINMAb3g==}
+ peerDependencies:
+ '@tiptap/extension-text-style': 3.27.4
+
+ '@tiptap/extension-document@3.27.4':
+ resolution: {integrity: sha512-7nAqgfkgb9HADBeCTnOHuTiyZuxfxvMPT3nH4OZeY+cmtkI1On3QffqlmtcUPvNbkhT3o9ehA1hVfCnQ1Ye4LQ==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-dropcursor@3.27.4':
+ resolution: {integrity: sha512-RiZasQJuUTUO3aME16Bn8eJH7cYnvhT5JCFDFq0ya/1iFI9wUQA2NJC5tb5TrZ74+sQwkYU9VzexnchM481Y9w==}
+ peerDependencies:
+ '@tiptap/extensions': 3.27.4
+
+ '@tiptap/extension-floating-menu@3.27.4':
+ resolution: {integrity: sha512-tnZywwoNDuEcUZmYYIztXl3PpIKUq+gKeaYPuZhpYEVTThU44tzK3ZuFOmd+qf2aAa1MQwxKWqUuLpNK77bwNw==}
+ peerDependencies:
+ '@floating-ui/dom': ^1.0.0
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-gapcursor@3.27.4':
+ resolution: {integrity: sha512-svLwSKcFhzpcJeXvxxKkRFuQpykmXrQefVhEsaXq0L95yJIIAGKMRmQC3mxKdzL2j0P9cY7V41bNVSyOAyvclw==}
+ peerDependencies:
+ '@tiptap/extensions': 3.27.4
+
+ '@tiptap/extension-hard-break@3.27.4':
+ resolution: {integrity: sha512-W+Z9pmDgqjbdu3NeZOQrzA15iM4w60Yd8l2CYzxcdApPVIfYzb2S3a7+u1RqW9wnTYb6xyZjASmFNfxXS4P4cg==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-heading@3.27.4':
+ resolution: {integrity: sha512-RgvpxzuYk6QEK+az+eiXpWvGlUso42zNcGnjyUrvskoZjS47MbhSg8ylRYQSRtXE0ETlXhAx4J7iGlGr72kyIw==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-highlight@3.27.4':
+ resolution: {integrity: sha512-STRX1qJLhTZslBF8fEE5qpTGrFd/g7Ufidjxt84p0uT8FrtcbfPUwyweeYwhZp7Iw8n+qODGYnheJTOpfaW2JA==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-horizontal-rule@3.27.4':
+ resolution: {integrity: sha512-2eQU/55nE5mhMJHALtLMuBL3dcVJUDVVT7n+uZYMaYE63BtCvC4VS08YLFSR7JZSVJIlgVAmdt5nAw0B+rEPNA==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-image@3.27.4':
+ resolution: {integrity: sha512-yQ8CazyOL4z1/NbV1NLGv6DvchVhOXHH3uQ7md5VX/IGZruFpnm8IpF9MDpdAUxUFmTsXJDYyO2lWGO1PYWG8A==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-italic@3.27.4':
+ resolution: {integrity: sha512-PeZT4XbyxAp7Lqo/hfA1k5LI27g1RlgS+YgXp2CeHXIrUfSpO5HlZXh02Bvb0pOdl3RFw2tEKtlHzjt8Y1+Nwg==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-link@3.27.4':
+ resolution: {integrity: sha512-6K/FkNwMLWWQbNWKlycrUPTN7YcyVFdFwZncoBXe5WyarRjLTGw7ywafnCI9PDIWSq7ttzVL4NgjN2IN8kBXww==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-list-item@3.27.4':
+ resolution: {integrity: sha512-z5TVuPw2mkK0B/x+gFg3uUV7tBdaElDFg0zVgnXZCqlSVTLfIyInOOnG5LTWoAd9BdzBjGrzE3PohDcLVDDGBQ==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.27.4
+
+ '@tiptap/extension-list-keymap@3.27.4':
+ resolution: {integrity: sha512-on7JNDi7Eqz7UdZeZdiO83bQHo0flVDHzjmtR+v/nrCGW9H15D3CHs5+4ozLDiCvTK8tbkBuut/l9AWNxcCE/Q==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.27.4
+
+ '@tiptap/extension-list@3.27.4':
+ resolution: {integrity: sha512-A0BgmRO1RE0yLCx9w7GQITtKfS9wLE5cdngSYDiSpwulcXJhJjKm5mZ4OUZmks2VN4HO5jMl2BWCGt2NSDhA+w==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-ordered-list@3.27.4':
+ resolution: {integrity: sha512-bHwLiof0FqJfWzB0act7oEKMTZatEKQ4IYCvmyF5EktjMs4kxEatkPp4Yx/1LSYSjLy1MMT7oLELyaz2FFYyXA==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.27.4
+
+ '@tiptap/extension-paragraph@3.27.4':
+ resolution: {integrity: sha512-8Dnr1J5s/s4XYYuEF3b784NnCxLjXOlQpmGyXRxTAzW7JaOP08tIUJWVNvSMekfXc2vXa33HUbqjxyyWZEQ6LQ==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-placeholder@3.27.4':
+ resolution: {integrity: sha512-7hBoFLeddCv1WzkqB0x3coZ1Hp9WZ9wLoRXIUtUhRKMpzFq2IlTtW1iw88g9pTJnL98bCCElN4DZ4mYtaQvmgA==}
+ peerDependencies:
+ '@tiptap/extensions': 3.27.4
+
+ '@tiptap/extension-strike@3.27.4':
+ resolution: {integrity: sha512-8OXwcPKuV3ToBBgyvDxH1jQdObK5FIKCGiyIim6qNWiOpi9BhM3XYD+aO1khjv8qIjtoI/DYbizF4ewj09fX2g==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-table-cell@3.27.4':
+ resolution: {integrity: sha512-1B7J4ZiaXaGxT2IB3hrwtz0433bFVSWsDB+B125vt2DZQr9bgdX40GLesSPlymqOdMxKJwksCPMan2ON2LkVAg==}
+ peerDependencies:
+ '@tiptap/extension-table': 3.27.4
+
+ '@tiptap/extension-table-header@3.27.4':
+ resolution: {integrity: sha512-V/O690Z6VcHMAWDfgVFiOCwx3eE2/HM2gH+Fp0eQe3WnajQj2DPViW4VWR/rNVl3DspHoL/EU+eEsnhcvO34Vw==}
+ peerDependencies:
+ '@tiptap/extension-table': 3.27.4
+
+ '@tiptap/extension-table-row@3.27.4':
+ resolution: {integrity: sha512-2RTJtcy90Tc2+HpW1JyKMTwg/zz4u6hHof4CTcJS/eXLk3g1L60DDzYvz4GzBxQvsOT3QVcbaU9QOk9A3Sx7Fg==}
+ peerDependencies:
+ '@tiptap/extension-table': 3.27.4
+
+ '@tiptap/extension-table@3.27.4':
+ resolution: {integrity: sha512-ejQjqt8GjUn4YswG/SsiLr/W3LZApZGUEDW0N7NoOduE0dBZ/pVJHPuqWu33kK+phJjSNCIN+bSAkoWE01rZSw==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-task-item@3.27.4':
+ resolution: {integrity: sha512-0/sGxmuUgintoCIh7fF7bblM1Hlr5W498njBLUPSJAd7cO78XXCjoB/UUEiCoBEPEyhHcDYesnYdZxsNfjChGg==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.27.4
+
+ '@tiptap/extension-task-list@3.27.4':
+ resolution: {integrity: sha512-nlFw+pOhnj9pF1wN0CyjqL0JppM6KNw05XtZWD5kt68tBUpBb7+aEy02tDXpZoRefiFhcIouKVJSeSzILZlCrA==}
+ peerDependencies:
+ '@tiptap/extension-list': 3.27.4
+
+ '@tiptap/extension-text-align@3.27.4':
+ resolution: {integrity: sha512-ArfL7GLOXCSmrmiBiaWwAf7RPHXDdxLGPru29qKDLQDthjXcNOdlwlPBbolRu7mXwlSmaYpqWGrG75/AEat3mw==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-text-style@3.27.4':
+ resolution: {integrity: sha512-Wmj64TQXY85gc7lUNbubW32sDCnVOJlpGraMeARRC9Z0EKBX1JpAxddo/64F17bn3kzLxXVszBFyLRcNgTdU2g==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-text@3.27.4':
+ resolution: {integrity: sha512-lKQH/hP4FBXsziHypd6Ywj8JFvMLM5GVkK1xsH6yApNuXbHq95rd42ZOYWpYILIBib7tlaz93z61d74UrJiuiw==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extension-underline@3.27.4':
+ resolution: {integrity: sha512-nRJGvRyEXDtINlHTW+C2oWcL3vmX1URVxAPpkD3Zwn5Rb/vEeOU/pk/w97I0iid816MR4iVbvl1XhbUVegK9gQ==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+
+ '@tiptap/extensions@3.27.4':
+ resolution: {integrity: sha512-d8opkg2iGtVwJmNGIqv0blfRxnvWOJp1brz+Z8CsP4ojSS2ZtaE46d6JSQ5OeJ7nMpjhT+9wh4UQcA7OSEO59w==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/pm@3.27.4':
+ resolution: {integrity: sha512-UB8lcyomfWk7YGI2PZKNqcYXfyRA+PFj+QntlsUXyrsiA5JJIaE8SHKYjxKlGG/xtW3EtPm1b0p38T9Mk4xiFw==}
+
+ '@tiptap/react@3.27.4':
+ resolution: {integrity: sha512-rTY1V9Y1jzwmo5ItRi3v2Og/mbcYsr9AjUvGoqpXzR9Z31WhXYphw0y05aYzryh0MHXYzkiE+gbGvrbg+cjwEg==}
+ peerDependencies:
+ '@tiptap/core': 3.27.4
+ '@tiptap/pm': 3.27.4
+ '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tiptap/starter-kit@3.27.4':
+ resolution: {integrity: sha512-/sb6rFxNt5BO4hWpUwvHh+Yh1kNyCQuuz3oDpGef5HUUjSdu9p9rfNiHWIUKBadK8VXuw5es7N+UlZ4hma+gvA==}
+
+ '@tybys/wasm-util@0.10.3':
+ resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+
+ '@types/node@24.13.3':
+ resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.17':
+ resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
+
+ '@types/use-sync-external-store@0.0.6':
+ resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+
+ '@vitejs/plugin-react@6.0.3':
+ resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
+ babel-plugin-react-compiler: ^1.0.0
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@rolldown/plugin-babel':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ fast-equals@5.4.1:
+ resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==}
+ engines: {node: '>=6.0.0'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ linkifyjs@4.3.3:
+ resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==}
+
+ lucide-react@1.24.0:
+ resolution: {integrity: sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ nanoid@3.3.16:
+ resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ orderedmap@2.1.1:
+ resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
+
+ oxlint@1.74.0:
+ resolution: {integrity: sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ oxlint-tsgolint: '>=0.24.0'
+ vite-plus: '*'
+ peerDependenciesMeta:
+ oxlint-tsgolint:
+ optional: true
+ vite-plus:
+ optional: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.19:
+ resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prosemirror-changeset@2.4.1:
+ resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==}
+
+ prosemirror-commands@1.7.1:
+ resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
+
+ prosemirror-dropcursor@1.8.3:
+ resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==}
+
+ prosemirror-gapcursor@1.4.1:
+ resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==}
+
+ prosemirror-history@1.5.0:
+ resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==}
+
+ prosemirror-inputrules@1.5.1:
+ resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
+
+ prosemirror-keymap@1.2.3:
+ resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
+
+ prosemirror-model@1.25.11:
+ resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==}
+
+ prosemirror-schema-list@1.5.1:
+ resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
+
+ prosemirror-state@1.4.4:
+ resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==}
+
+ prosemirror-tables@1.8.5:
+ resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==}
+
+ prosemirror-transform@1.12.0:
+ resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==}
+
+ prosemirror-view@1.42.1:
+ resolution: {integrity: sha512-rRqzZnRgkyh69XoOMrfFJHwauHscLBmHbq772kwbic1ymQAM8gXjzEbJse5j1ep2UO2HRIAQL0bY3kZ/RoqjVw==}
+
+ react-dom@19.2.7:
+ resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==}
+ peerDependencies:
+ react: ^19.2.7
+
+ react@19.2.7:
+ resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
+ engines: {node: '>=0.10.0'}
+
+ rolldown@1.1.5:
+ resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ rope-sequence@1.3.4:
+ resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.18.2:
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ vite@8.1.4:
+ resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.3.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
+snapshots:
+
+ '@emnapi/core@1.11.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.2
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@floating-ui/core@1.8.0':
+ dependencies:
+ '@floating-ui/utils': 0.2.12
+ optional: true
+
+ '@floating-ui/dom@1.8.0':
+ dependencies:
+ '@floating-ui/core': 1.8.0
+ '@floating-ui/utils': 0.2.12
+ optional: true
+
+ '@floating-ui/utils@0.2.12':
+ optional: true
+
+ '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@tybys/wasm-util': 0.10.3
+ optional: true
+
+ '@oxc-project/types@0.139.0': {}
+
+ '@oxlint/binding-android-arm-eabi@1.74.0':
+ optional: true
+
+ '@oxlint/binding-android-arm64@1.74.0':
+ optional: true
+
+ '@oxlint/binding-darwin-arm64@1.74.0':
+ optional: true
+
+ '@oxlint/binding-darwin-x64@1.74.0':
+ optional: true
+
+ '@oxlint/binding-freebsd-x64@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-musleabihf@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-gnu@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-musl@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-ppc64-gnu@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-gnu@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-musl@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-s390x-gnu@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-gnu@1.74.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-musl@1.74.0':
+ optional: true
+
+ '@oxlint/binding-openharmony-arm64@1.74.0':
+ optional: true
+
+ '@oxlint/binding-win32-arm64-msvc@1.74.0':
+ optional: true
+
+ '@oxlint/binding-win32-ia32-msvc@1.74.0':
+ optional: true
+
+ '@oxlint/binding-win32-x64-msvc@1.74.0':
+ optional: true
+
+ '@rolldown/binding-android-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.1.5':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.1.5':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.5':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.1.5':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@tiptap/core@3.27.4(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-blockquote@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-bold@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-bubble-menu@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@floating-ui/dom': 1.8.0
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+ optional: true
+
+ '@tiptap/extension-bullet-list@3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-list': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-code-block@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-code@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-color@3.27.4(@tiptap/extension-text-style@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4)))':
+ dependencies:
+ '@tiptap/extension-text-style': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+
+ '@tiptap/extension-document@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-dropcursor@3.27.4(@tiptap/extensions@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extensions': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-floating-menu@3.27.4(@floating-ui/dom@1.8.0)(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@floating-ui/dom': 1.8.0
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+ optional: true
+
+ '@tiptap/extension-gapcursor@3.27.4(@tiptap/extensions@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extensions': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-hard-break@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-heading@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-highlight@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-horizontal-rule@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-image@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-italic@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-link@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+ linkifyjs: 4.3.3
+
+ '@tiptap/extension-list-item@3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-list': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-list-keymap@3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-list': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-ordered-list@3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-list': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-paragraph@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-placeholder@3.27.4(@tiptap/extensions@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extensions': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-strike@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-table-cell@3.27.4(@tiptap/extension-table@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-table': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-table-header@3.27.4(@tiptap/extension-table@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-table': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-table-row@3.27.4(@tiptap/extension-table@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-table': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-table@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/extension-task-item@3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-list': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-task-list@3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/extension-list': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-text-align@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-text-style@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-text@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extension-underline@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+
+ '@tiptap/extensions@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+
+ '@tiptap/pm@3.27.4':
+ dependencies:
+ prosemirror-changeset: 2.4.1
+ prosemirror-commands: 1.7.1
+ prosemirror-dropcursor: 1.8.3
+ prosemirror-gapcursor: 1.4.1
+ prosemirror-history: 1.5.0
+ prosemirror-inputrules: 1.5.1
+ prosemirror-keymap: 1.2.3
+ prosemirror-model: 1.25.11
+ prosemirror-schema-list: 1.5.1
+ prosemirror-state: 1.4.4
+ prosemirror-tables: 1.8.5
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.1
+
+ '@tiptap/react@3.27.4(@floating-ui/dom@1.8.0)(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+ '@types/use-sync-external-store': 0.0.6
+ fast-equals: 5.4.1
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ use-sync-external-store: 1.6.0(react@19.2.7)
+ optionalDependencies:
+ '@tiptap/extension-bubble-menu': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/extension-floating-menu': 3.27.4(@floating-ui/dom@1.8.0)(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ transitivePeerDependencies:
+ - '@floating-ui/dom'
+
+ '@tiptap/starter-kit@3.27.4':
+ dependencies:
+ '@tiptap/core': 3.27.4(@tiptap/pm@3.27.4)
+ '@tiptap/extension-blockquote': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/extension-bold': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-bullet-list': 3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-code': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-code-block': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/extension-document': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-dropcursor': 3.27.4(@tiptap/extensions@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-gapcursor': 3.27.4(@tiptap/extensions@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-hard-break': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-heading': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-horizontal-rule': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/extension-italic': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-link': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/extension-list': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/extension-list-item': 3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-list-keymap': 3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-ordered-list': 3.27.4(@tiptap/extension-list@3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4))
+ '@tiptap/extension-paragraph': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-strike': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-text': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extension-underline': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))
+ '@tiptap/extensions': 3.27.4(@tiptap/core@3.27.4(@tiptap/pm@3.27.4))(@tiptap/pm@3.27.4)
+ '@tiptap/pm': 3.27.4
+
+ '@tybys/wasm-util@0.10.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/node@24.13.3':
+ dependencies:
+ undici-types: 7.18.2
+
+ '@types/react-dom@19.2.3(@types/react@19.2.17)':
+ dependencies:
+ '@types/react': 19.2.17
+
+ '@types/react@19.2.17':
+ dependencies:
+ csstype: 3.2.3
+
+ '@types/use-sync-external-store@0.0.6': {}
+
+ '@vitejs/plugin-react@6.0.3(vite@8.1.4(@types/node@24.13.3))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ vite: 8.1.4(@types/node@24.13.3)
+
+ csstype@3.2.3: {}
+
+ detect-libc@2.1.2: {}
+
+ fast-equals@5.4.1: {}
+
+ fdir@6.5.0(picomatch@4.0.5):
+ optionalDependencies:
+ picomatch: 4.0.5
+
+ fsevents@2.3.3:
+ optional: true
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ linkifyjs@4.3.3: {}
+
+ lucide-react@1.24.0(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+
+ nanoid@3.3.16: {}
+
+ orderedmap@2.1.1: {}
+
+ oxlint@1.74.0:
+ optionalDependencies:
+ '@oxlint/binding-android-arm-eabi': 1.74.0
+ '@oxlint/binding-android-arm64': 1.74.0
+ '@oxlint/binding-darwin-arm64': 1.74.0
+ '@oxlint/binding-darwin-x64': 1.74.0
+ '@oxlint/binding-freebsd-x64': 1.74.0
+ '@oxlint/binding-linux-arm-gnueabihf': 1.74.0
+ '@oxlint/binding-linux-arm-musleabihf': 1.74.0
+ '@oxlint/binding-linux-arm64-gnu': 1.74.0
+ '@oxlint/binding-linux-arm64-musl': 1.74.0
+ '@oxlint/binding-linux-ppc64-gnu': 1.74.0
+ '@oxlint/binding-linux-riscv64-gnu': 1.74.0
+ '@oxlint/binding-linux-riscv64-musl': 1.74.0
+ '@oxlint/binding-linux-s390x-gnu': 1.74.0
+ '@oxlint/binding-linux-x64-gnu': 1.74.0
+ '@oxlint/binding-linux-x64-musl': 1.74.0
+ '@oxlint/binding-openharmony-arm64': 1.74.0
+ '@oxlint/binding-win32-arm64-msvc': 1.74.0
+ '@oxlint/binding-win32-ia32-msvc': 1.74.0
+ '@oxlint/binding-win32-x64-msvc': 1.74.0
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.5: {}
+
+ postcss@8.5.19:
+ dependencies:
+ nanoid: 3.3.16
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prosemirror-changeset@2.4.1:
+ dependencies:
+ prosemirror-transform: 1.12.0
+
+ prosemirror-commands@1.7.1:
+ dependencies:
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+
+ prosemirror-dropcursor@1.8.3:
+ dependencies:
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.1
+
+ prosemirror-gapcursor@1.4.1:
+ dependencies:
+ prosemirror-keymap: 1.2.3
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-view: 1.42.1
+
+ prosemirror-history@1.5.0:
+ dependencies:
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.1
+ rope-sequence: 1.3.4
+
+ prosemirror-inputrules@1.5.1:
+ dependencies:
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+
+ prosemirror-keymap@1.2.3:
+ dependencies:
+ prosemirror-state: 1.4.4
+ w3c-keyname: 2.2.8
+
+ prosemirror-model@1.25.11:
+ dependencies:
+ orderedmap: 2.1.1
+
+ prosemirror-schema-list@1.5.1:
+ dependencies:
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+
+ prosemirror-state@1.4.4:
+ dependencies:
+ prosemirror-model: 1.25.11
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.1
+
+ prosemirror-tables@1.8.5:
+ dependencies:
+ prosemirror-keymap: 1.2.3
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+ prosemirror-view: 1.42.1
+
+ prosemirror-transform@1.12.0:
+ dependencies:
+ prosemirror-model: 1.25.11
+
+ prosemirror-view@1.42.1:
+ dependencies:
+ prosemirror-model: 1.25.11
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.12.0
+
+ react-dom@19.2.7(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ scheduler: 0.27.0
+
+ react@19.2.7: {}
+
+ rolldown@1.1.5:
+ dependencies:
+ '@oxc-project/types': 0.139.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.1.5
+ '@rolldown/binding-darwin-arm64': 1.1.5
+ '@rolldown/binding-darwin-x64': 1.1.5
+ '@rolldown/binding-freebsd-x64': 1.1.5
+ '@rolldown/binding-linux-arm-gnueabihf': 1.1.5
+ '@rolldown/binding-linux-arm64-gnu': 1.1.5
+ '@rolldown/binding-linux-arm64-musl': 1.1.5
+ '@rolldown/binding-linux-ppc64-gnu': 1.1.5
+ '@rolldown/binding-linux-s390x-gnu': 1.1.5
+ '@rolldown/binding-linux-x64-gnu': 1.1.5
+ '@rolldown/binding-linux-x64-musl': 1.1.5
+ '@rolldown/binding-openharmony-arm64': 1.1.5
+ '@rolldown/binding-wasm32-wasi': 1.1.5
+ '@rolldown/binding-win32-arm64-msvc': 1.1.5
+ '@rolldown/binding-win32-x64-msvc': 1.1.5
+
+ rope-sequence@1.3.4: {}
+
+ scheduler@0.27.0: {}
+
+ source-map-js@1.2.1: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+
+ tslib@2.8.1:
+ optional: true
+
+ typescript@6.0.3: {}
+
+ undici-types@7.18.2: {}
+
+ use-sync-external-store@1.6.0(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+
+ vite@8.1.4(@types/node@24.13.3):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.5
+ postcss: 8.5.19
+ rolldown: 1.1.5
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 24.13.3
+ fsevents: 2.3.3
+
+ w3c-keyname@2.2.8: {}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..fa4acc6
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,1155 @@
+.app-shell {
+ min-height: 100vh;
+ background:
+ radial-gradient(circle at top, rgba(74, 74, 74, 0.18), transparent 18%),
+ linear-gradient(180deg, #171717 0%, #1d1d1d 100%);
+ color: #f4f4f2;
+}
+
+.app-shell--loading {
+ display: grid;
+ place-items: center;
+}
+
+.loading-mark {
+ display: grid;
+ gap: 16px;
+ justify-items: center;
+ color: #b8b8b2;
+}
+
+.loading-mark__orbit {
+ width: 72px;
+ height: 72px;
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ border-top-color: #f4f4f2;
+ animation: spin 1.1s linear infinite;
+}
+
+.auth-screen {
+ display: grid;
+ place-items: center;
+ padding: 24px;
+}
+
+.auth-screen__panel {
+ width: min(420px, 100%);
+ padding: 0;
+ border-radius: 32px;
+ background: transparent;
+ border: 0;
+ box-shadow: 0 30px 90px rgba(0, 0, 0, 0.35);
+}
+
+.auth-screen__brand {
+ padding: 28px;
+}
+
+.auth-screen__badge {
+ display: inline-flex;
+ padding: 8px 12px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.08);
+ color: #b7b7b0;
+ font-size: 0.85rem;
+}
+
+.auth-screen__brand h1 {
+ margin: 24px 0 12px;
+ font-size: clamp(2.8rem, 5vw, 4.4rem);
+ line-height: 0.94;
+ letter-spacing: -0.06em;
+ font-family: Georgia, 'Times New Roman', serif;
+}
+
+.auth-screen__brand p {
+ margin: 0;
+ max-width: 42ch;
+ color: #9b9b94;
+ font-size: 1.02rem;
+}
+
+.auth-form--dark {
+ padding: 24px;
+ border-radius: 28px;
+ background: rgba(34, 34, 34, 0.92);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.auth-form,
+.auth-form label {
+ display: grid;
+ gap: 12px;
+}
+
+.auth-form span {
+ color: #a4a49d;
+ font-size: 0.88rem;
+}
+
+.auth-form input {
+ width: 100%;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 16px;
+ padding: 14px 16px;
+ background: rgba(255, 255, 255, 0.04);
+ color: #f3f3ef;
+ font: inherit;
+ box-sizing: border-box;
+}
+
+.auth-form input:focus,
+.toolbar-search:focus,
+.toolbar-tags:focus,
+.notes-title:focus,
+.notes-editor__content:focus {
+ outline: none;
+ border-color: rgba(255, 255, 255, 0.18);
+}
+
+.button,
+.icon-button,
+.ghost-button,
+.toolbar-pill__button,
+.format-popover button,
+.note-row {
+ font: inherit;
+}
+
+.button {
+ border: 0;
+ border-radius: 16px;
+ padding: 14px 18px;
+ cursor: pointer;
+}
+
+.button--bright {
+ background: #f4f4f2;
+ color: #171717;
+}
+
+.auth-switch {
+ margin-top: 8px;
+ border: 0;
+ background: transparent;
+ color: #9c9c96;
+ padding: 0;
+ cursor: pointer;
+ text-align: left;
+}
+
+.form-error {
+ margin: 0;
+ color: #ff8f8f;
+}
+
+.notes-app {
+ display: grid;
+ grid-template-columns: 300px minmax(0, 1fr);
+ gap: 0;
+ max-width: 100%;
+ min-height: 100vh;
+ overflow-x: clip;
+ background: #ffffff;
+ color: #1d1d1f;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif;
+}
+
+.notes-app--focus {
+ grid-template-columns: minmax(0, 1fr);
+}
+
+.notes-sidebar {
+ min-height: 100vh;
+ padding: 14px 12px;
+ border-right: 1px solid #d8d8dc;
+ background: rgba(242, 242, 247, 0.96);
+ box-sizing: border-box;
+ color: #1d1d1f;
+ overflow: auto;
+}
+
+.notes-sidebar--hidden {
+ display: none;
+}
+
+.notes-sidebar__top,
+.notes-sidebar__section,
+.notes-toolbar,
+.notes-toolbar__left,
+.notes-toolbar__center,
+.notes-toolbar__right,
+.notes-stage__meta,
+.notes-footer,
+.notes-footer__stats,
+.notes-footer__actions {
+ display: flex;
+ align-items: center;
+}
+
+.notes-sidebar__top {
+ justify-content: flex-end;
+ margin-bottom: 22px;
+}
+
+.notes-sidebar__section {
+ justify-content: space-between;
+ color: #8e8e93;
+ margin-bottom: 14px;
+}
+
+.notes-sidebar__section h2 {
+ margin: 0;
+ font-size: 1rem;
+ color: #1d1d1f;
+}
+
+.icon-button {
+ width: 36px;
+ height: 36px;
+ border: 0;
+ border-radius: 9px;
+ background: transparent;
+ color: #6b6b70;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.icon-button--active {
+ background: #e7e7eb;
+ color: #c98b00;
+}
+
+.notes-list--dark {
+ display: grid;
+ gap: 8px;
+ margin-top: 14px;
+}
+
+.sidebar-shelves {
+ display: grid;
+ gap: 6px;
+}
+
+.sidebar-shelf {
+ border: 0;
+ border-radius: 14px;
+ background: transparent;
+ color: #515154;
+ padding: 8px 10px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ text-align: left;
+}
+
+.sidebar-shelf:hover,
+.sidebar-shelf--active {
+ background: #e5e5ea;
+ color: #1d1d1f;
+}
+
+.note-row {
+ display: grid;
+ gap: 6px;
+ text-align: left;
+ padding: 12px 10px;
+ border: 0;
+ border-radius: 16px;
+ background: transparent;
+ color: #1d1d1f;
+ cursor: pointer;
+}
+
+.note-row strong {
+ font-size: 1rem;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.note-row span,
+.muted,
+.notes-stage__meta,
+.notes-footer__stats {
+ color: #8e8e93;
+}
+
+.note-row span {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ -webkit-line-clamp: 2;
+}
+
+.note-row--active {
+ background: #ffd85a;
+ color: #2c2100;
+}
+
+.note-row--active span {
+ color: #5c4400;
+}
+
+.notes-workspace {
+ min-width: 0;
+ max-width: 100%;
+ min-height: 100vh;
+ padding: 0;
+ background: #ffffff;
+}
+
+.notes-toolbar {
+ justify-content: space-between;
+ gap: 20px;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ min-height: 54px;
+ padding: 6px 14px;
+ box-sizing: border-box;
+ border-bottom: 1px solid #dedee2;
+ background: rgba(250, 250, 250, 0.88);
+ backdrop-filter: blur(20px) saturate(180%);
+}
+
+.notes-toolbar__left,
+.notes-toolbar__right {
+ gap: 12px;
+ flex: 0 0 auto;
+}
+
+.notes-toolbar__center {
+ position: relative;
+ justify-content: center;
+ min-width: 0;
+ flex: 1 1 auto;
+}
+
+.toolbar-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ max-width: 100%;
+ padding: 4px;
+ border-radius: 999px;
+ background: #ececef;
+ border: 1px solid #dedee2;
+ box-shadow: none;
+}
+
+.toolbar-pill__button {
+ min-width: 34px;
+ height: 34px;
+ border: 0;
+ border-radius: 999px;
+ background: transparent;
+ color: #55555a;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 0 10px;
+}
+
+.notes-toolbar .icon-button {
+ border-radius: 999px;
+}
+
+.notes-toolbar .locale-button {
+ width: 48px;
+ min-width: 48px;
+ height: 48px;
+ padding: 0;
+ border: 1px solid #d8d8dc;
+ background: #f1f1f4;
+}
+
+.notes-toolbar .toolbar-theme-button {
+ width: 48px;
+ height: 48px;
+ flex: 0 0 48px;
+ border: 1px solid #d8d8dc;
+ background: #f1f1f4;
+}
+
+.locale-code {
+ font-size: 0.82rem;
+ font-weight: 600;
+ line-height: 1;
+}
+
+.notes-app--dark .locale-button {
+ background: #2c2c2e;
+ border-color: #48484a;
+}
+
+.notes-app--dark .toolbar-theme-button {
+ background: #2c2c2e;
+ border-color: #48484a;
+}
+
+.toolbar-pill__button.is-active,
+.toolbar-pill__button:hover,
+.format-popover button:hover,
+.ghost-button:hover,
+.icon-button:hover {
+ background: rgba(0, 0, 0, 0.07);
+}
+
+.toolbar-signout {
+ border: 1px solid #f0c944;
+ border-radius: 999px;
+ padding: 11px 16px;
+ background: #ffd85a;
+ color: #2c2100;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ font: inherit;
+ font-weight: 400;
+ line-height: inherit;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.toolbar-signout:hover {
+ background: #ffdf70;
+}
+
+.toolbar-signout:disabled {
+ cursor: default;
+ opacity: 0.65;
+}
+
+.format-popover {
+ position: absolute;
+ top: calc(100% + 12px);
+ left: 50%;
+ transform: translateX(-50%);
+ width: 240px;
+ padding: 8px;
+ border-radius: 18px;
+ background: rgba(250, 250, 250, 0.98);
+ border: 1px solid #d8d8dc;
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.16);
+ display: grid;
+ gap: 4px;
+}
+
+.format-popover button {
+ border: 0;
+ background: transparent;
+ color: #2c2c2e;
+ border-radius: 12px;
+ padding: 10px 12px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+}
+
+.format-popover__text {
+ width: 16px;
+ flex: 0 0 16px;
+ text-align: center;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.format-popover__swatch {
+ width: 14px;
+ height: 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ flex: 0 0 auto;
+}
+
+.format-popover__swatch--purple {
+ background: #caa8ff;
+}
+
+.format-popover__swatch--clear {
+ background:
+ linear-gradient(135deg, transparent 42%, #ff7a7a 42%, #ff7a7a 58%, transparent 58%),
+ rgba(255, 255, 255, 0.06);
+}
+
+.toolbar-search,
+.toolbar-tags {
+ width: 220px;
+ border: 1px solid #d8d8dc;
+ border-radius: 999px;
+ padding: 11px 16px;
+ background: #f1f1f4;
+ color: #1d1d1f;
+ font: inherit;
+}
+
+.toolbar-tags {
+ width: 150px;
+}
+
+.notes-stage {
+ width: 100%;
+ margin: 0;
+}
+
+.notes-stage__meta {
+ justify-content: center;
+ gap: 28px;
+ font-size: 0.9rem;
+ min-height: 32px;
+ margin: 0;
+ padding: 6px 24px;
+ box-sizing: border-box;
+ border-bottom: 1px solid #eeeeef;
+ flex-wrap: wrap;
+ text-align: center;
+}
+
+.notes-canvas {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: calc(100dvh - 86px);
+ min-height: 0;
+ overflow: hidden;
+ padding: 28px clamp(28px, 5vw, 80px) 14px;
+ box-sizing: border-box;
+ border-radius: 0;
+ background: #ffffff;
+ border: 0;
+ box-shadow: none;
+}
+
+.notes-canvas--drag {
+ border-color: rgba(188, 216, 255, 0.45);
+ box-shadow: 0 0 0 1px rgba(188, 216, 255, 0.18), 0 24px 80px rgba(0, 0, 0, 0.22);
+}
+
+.drop-overlay {
+ position: absolute;
+ inset: 14px;
+ border-radius: 24px;
+ border: 1px dashed rgba(188, 216, 255, 0.38);
+ background: rgba(20, 24, 30, 0.86);
+ display: grid;
+ place-content: center;
+ justify-items: center;
+ gap: 8px;
+ z-index: 3;
+ pointer-events: none;
+ text-align: center;
+}
+
+.drop-overlay strong {
+ font-size: 1.05rem;
+ color: #f5f7fb;
+}
+
+.drop-overlay span {
+ color: #aab1bc;
+}
+
+.notes-title {
+ width: 100%;
+ border: 0;
+ background: transparent;
+ color: #1d1d1f;
+ font-size: 2rem;
+ font-weight: 700;
+ margin-bottom: 18px;
+ padding: 0;
+ font-family: inherit;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.notes-title::placeholder {
+ color: #aeaeb2;
+}
+
+.tag-cloud {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.tag-cloud {
+ margin: 0 0 16px;
+}
+
+.tag-chip {
+ border: 1px solid #dedee2;
+ border-radius: 999px;
+ background: #f5f5f7;
+ color: #515154;
+ padding: 8px 12px;
+ font: inherit;
+ cursor: pointer;
+}
+
+.tag-chip:hover {
+ background: #e9e9ed;
+}
+
+.command-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(7, 7, 8, 0.58);
+ backdrop-filter: blur(18px);
+ display: grid;
+ place-items: start center;
+ padding: 84px 20px 20px;
+ z-index: 40;
+}
+
+.command-panel {
+ width: min(640px, 100%);
+ border-radius: 24px;
+ background: rgba(24, 24, 25, 0.96);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ box-shadow: 0 36px 120px rgba(0, 0, 0, 0.38);
+ overflow: hidden;
+}
+
+.command-panel__head {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px 18px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.07);
+ color: #e5e5df;
+}
+
+.command-panel__input {
+ width: 100%;
+ border: 0;
+ background: transparent;
+ color: #f2f2ee;
+ font: inherit;
+ font-size: 1rem;
+}
+
+.command-panel__input:focus {
+ outline: none;
+}
+
+.command-panel__list {
+ display: grid;
+ gap: 2px;
+ padding: 10px;
+ max-height: min(65vh, 520px);
+ overflow: auto;
+}
+
+.command-item {
+ border: 0;
+ border-radius: 16px;
+ background: transparent;
+ color: #f2f2ee;
+ padding: 12px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ text-align: left;
+ cursor: pointer;
+}
+
+.command-item:hover {
+ background: rgba(255, 255, 255, 0.06);
+}
+
+.command-item__icon {
+ width: 34px;
+ height: 34px;
+ border-radius: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.06);
+ color: #e7e7e1;
+ flex: 0 0 auto;
+}
+
+.command-item__copy {
+ display: grid;
+ gap: 2px;
+}
+
+.command-item__copy strong {
+ font-weight: 600;
+}
+
+.command-item__copy small,
+.command-empty {
+ color: #9f9f98;
+}
+
+.command-empty {
+ margin: 0;
+ padding: 18px 12px 14px;
+}
+
+.notes-editor {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow-y: auto;
+ overscroll-behavior-y: contain;
+ scrollbar-gutter: stable;
+}
+
+.notes-editor__content {
+ min-height: 100%;
+ color: #2c2c2e;
+ line-height: 1.7;
+ font-size: 1.02rem;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.notes-editor__content p {
+ margin: 0 0 12px;
+}
+
+.notes-editor__content hr {
+ border: 0;
+ height: 1px;
+ background: #dedee2;
+ margin: 20px 0;
+}
+
+.notes-editor__content h1,
+.notes-editor__content h2,
+.notes-editor__content h3 {
+ margin: 0 0 12px;
+ line-height: 1.15;
+}
+
+.notes-editor__content h1 {
+ font-size: 2.1rem;
+ overflow-wrap: anywhere;
+}
+
+.notes-editor__content h2 {
+ font-size: 1.5rem;
+}
+
+.notes-editor__content ul,
+.notes-editor__content ol {
+ padding-left: 22px;
+}
+
+.notes-editor__content ul[data-type='taskList'] {
+ list-style: none;
+ padding-left: 0;
+}
+
+.notes-editor__content ul[data-type='taskList'] li {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+
+.notes-editor__content ul[data-type='taskList'] li > label {
+ margin-top: 3px;
+}
+
+.notes-editor__content ul[data-type='taskList'] li > div {
+ flex: 1 1 auto;
+}
+
+.notes-editor__content blockquote {
+ margin: 0 0 12px;
+ padding-left: 16px;
+ border-left: 3px solid #f0b429;
+ color: #636366;
+}
+
+.notes-editor__content a {
+ color: #007aff;
+ overflow-wrap: anywhere;
+}
+
+.notes-editor__content img {
+ max-width: min(520px, 100%);
+ height: auto;
+ border-radius: 18px;
+ display: block;
+ margin: 14px 0;
+}
+
+.notes-editor__content code {
+ padding: 2px 6px;
+ border-radius: 8px;
+ background: #f0f0f2;
+}
+
+.notes-editor__content pre {
+ padding: 16px;
+ border-radius: 14px;
+ background: #0f0f10;
+ overflow: auto;
+}
+
+.notes-editor__content table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 16px 0;
+ overflow: hidden;
+ border-radius: 14px;
+}
+
+.notes-editor__content th,
+.notes-editor__content td {
+ border: 1px solid #d8d8dc;
+ padding: 10px 12px;
+ text-align: left;
+}
+
+.notes-editor__content th {
+ background: #f2f2f7;
+}
+
+.notes-footer {
+ justify-content: space-between;
+ gap: 18px;
+ flex: 0 0 auto;
+ min-width: 0;
+ max-width: 100%;
+ margin-top: auto;
+ padding-top: 18px;
+ border-top: 1px solid #e5e5ea;
+}
+
+.notes-footer__stats,
+.notes-footer__actions {
+ gap: 14px;
+ min-width: 0;
+ flex-wrap: wrap;
+}
+
+.ghost-button {
+ border: 1px solid #d8d8dc;
+ border-radius: 14px;
+ background: #f7f7f8;
+ color: #3a3a3c;
+ padding: 10px 14px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.ghost-button--danger {
+ color: #b42318;
+ background: #fff1f0;
+ border-color: #f2b8b5;
+ font-weight: 600;
+}
+
+.ghost-button--bright {
+ color: #5c4400;
+ background: #ffd85a;
+ border-color: #f0c944;
+}
+
+.notes-app--dark {
+ background: #1c1c1e;
+ color: #f2f2f7;
+}
+
+.notes-app--dark .notes-sidebar {
+ background: #252527;
+ border-color: #3a3a3c;
+ color: #f2f2f7;
+}
+
+.notes-app--dark .notes-sidebar__section h2,
+.notes-app--dark .note-row,
+.notes-app--dark .sidebar-shelf {
+ color: #f2f2f7;
+}
+
+.notes-app--dark .sidebar-shelf:hover,
+.notes-app--dark .sidebar-shelf--active {
+ background: #3a3a3c;
+}
+
+.notes-app--dark .note-row--active {
+ background: #ffd85a;
+ color: #2c2100;
+}
+
+.notes-app--dark .note-row--active span {
+ color: #5c4400;
+}
+
+.notes-app--dark .notes-workspace,
+.notes-app--dark .notes-canvas {
+ background: #1c1c1e;
+}
+
+.notes-app--dark .notes-toolbar {
+ background: rgba(38, 38, 40, 0.9);
+ border-color: #3a3a3c;
+}
+
+.notes-app--dark .notes-stage__meta,
+.notes-app--dark .notes-footer {
+ border-color: #353537;
+}
+
+.notes-app--dark .icon-button,
+.notes-app--dark .toolbar-pill__button {
+ color: #e5e5ea;
+}
+
+.notes-app--dark .icon-button:hover,
+.notes-app--dark .icon-button--active,
+.notes-app--dark .toolbar-pill__button:hover,
+.notes-app--dark .toolbar-pill__button.is-active {
+ background: #48484a;
+}
+
+.notes-app--dark .toolbar-pill {
+ background: #323234;
+ border-color: #48484a;
+}
+
+.notes-app--dark .toolbar-search,
+.notes-app--dark .toolbar-tags,
+.notes-app--dark .tag-chip,
+.notes-app--dark .ghost-button {
+ background: #2c2c2e;
+ border-color: #48484a;
+ color: #f2f2f7;
+}
+
+.notes-app--dark .format-popover {
+ background: rgba(44, 44, 46, 0.98);
+ border-color: #48484a;
+}
+
+.notes-app--dark .format-popover button,
+.notes-app--dark .notes-title,
+.notes-app--dark .notes-editor__content {
+ color: #f2f2f7;
+}
+
+.notes-app--dark .notes-editor__content hr {
+ background: #48484a;
+}
+
+.notes-app--dark .notes-editor__content blockquote {
+ color: #c7c7cc;
+}
+
+.notes-app--dark .notes-editor__content code,
+.notes-app--dark .notes-editor__content th {
+ background: #2c2c2e;
+}
+
+.notes-app--dark .notes-editor__content th,
+.notes-app--dark .notes-editor__content td {
+ border-color: #48484a;
+}
+
+.notes-app--dark .ghost-button--danger {
+ color: #ff6961;
+ background: #3a2020;
+ border-color: #7a3734;
+}
+
+.notes-app--dark .ghost-button--bright {
+ color: #2c2100;
+ background: #ffd85a;
+ border-color: #f0c944;
+}
+
+.notes-empty {
+ min-height: calc(100vh - 160px);
+ display: grid;
+ place-content: center;
+ justify-items: start;
+ gap: 10px;
+}
+
+.notes-empty h2,
+.notes-empty p {
+ margin: 0;
+}
+
+.toast-stack {
+ position: fixed;
+ right: 24px;
+ bottom: 24px;
+ display: grid;
+ gap: 10px;
+}
+
+.toast {
+ padding: 12px 14px;
+ border-radius: 14px;
+ background: rgba(245, 245, 241, 0.94);
+ color: #151515;
+ box-shadow: 0 14px 32px rgba(0, 0, 0, 0.25);
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (max-width: 980px) {
+ .auth-screen__panel,
+ .notes-app {
+ grid-template-columns: 1fr;
+ }
+
+ .notes-sidebar {
+ min-height: 0;
+ border-right: 0;
+ border-bottom: 1px solid #d8d8dc;
+ }
+
+ .notes-stage {
+ width: 100%;
+ }
+
+ .notes-toolbar {
+ flex-wrap: wrap;
+ }
+
+ .notes-toolbar__center,
+ .notes-toolbar__right {
+ width: 100%;
+ }
+
+ .notes-toolbar__center {
+ order: 2;
+ }
+
+ .notes-toolbar__right {
+ min-width: 0;
+ flex: 1 1 100%;
+ flex-wrap: wrap;
+ }
+
+ .toolbar-pill {
+ width: 100%;
+ overflow-x: auto;
+ overscroll-behavior-inline: contain;
+ scrollbar-width: none;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .toolbar-pill::-webkit-scrollbar {
+ display: none;
+ }
+
+ .toolbar-pill__button {
+ flex: 0 0 auto;
+ }
+
+ .notes-canvas {
+ height: auto;
+ min-height: 0;
+ overflow: visible;
+ }
+
+ .notes-editor {
+ overflow: visible;
+ }
+}
+
+@media (max-width: 720px) {
+ .notes-workspace {
+ padding-inline: 0;
+ }
+
+ .notes-toolbar,
+ .notes-footer,
+ .notes-toolbar__right,
+ .notes-stage__meta {
+ flex-wrap: wrap;
+ }
+
+ .notes-toolbar {
+ gap: 8px;
+ padding-inline: 10px;
+ }
+
+ .notes-toolbar__right {
+ gap: 6px;
+ }
+
+ .notes-toolbar__right .icon-button {
+ flex: 1 0 36px;
+ }
+
+ .toolbar-signout {
+ flex: 1 0 auto;
+ }
+
+ .toolbar-search {
+ width: 100%;
+ }
+
+ .toolbar-tags {
+ width: 100%;
+ }
+
+ .notes-title {
+ font-size: 1.7rem;
+ }
+
+ .notes-canvas {
+ padding: 22px 18px 12px;
+ border-radius: 0;
+ }
+
+ .notes-footer {
+ align-items: stretch;
+ flex-direction: column;
+ padding-bottom: max(4px, env(safe-area-inset-bottom));
+ }
+
+ .notes-footer__stats {
+ gap: 8px 14px;
+ }
+
+ .notes-footer__actions {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ width: 100%;
+ }
+
+ .notes-footer__actions .ghost-button {
+ justify-content: center;
+ min-width: 0;
+ width: 100%;
+ }
+
+ .notes-editor__content {
+ min-height: 50vh;
+ }
+}
+
+@media (max-width: 420px) {
+ .notes-footer__actions {
+ grid-template-columns: minmax(0, 1fr);
+ }
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..22668b2
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,1335 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import type { FormEvent } from 'react'
+import { EditorContent, useEditor } from '@tiptap/react'
+import StarterKit from '@tiptap/starter-kit'
+import Underline from '@tiptap/extension-underline'
+import Link from '@tiptap/extension-link'
+import Placeholder from '@tiptap/extension-placeholder'
+import TextAlign from '@tiptap/extension-text-align'
+import TaskList from '@tiptap/extension-task-list'
+import TaskItem from '@tiptap/extension-task-item'
+import Highlight from '@tiptap/extension-highlight'
+import { TextStyle } from '@tiptap/extension-text-style'
+import Color from '@tiptap/extension-color'
+import Image from '@tiptap/extension-image'
+import { Table } from '@tiptap/extension-table'
+import TableRow from '@tiptap/extension-table-row'
+import TableCell from '@tiptap/extension-table-cell'
+import TableHeader from '@tiptap/extension-table-header'
+import {
+ Archive,
+ Bold,
+ CheckSquare,
+ Code2,
+ Copy,
+ Download,
+ Highlighter,
+ Heading1,
+ Heading2,
+ Heading3,
+ ImagePlus,
+ Italic,
+ Link2,
+ List,
+ ListOrdered,
+ Languages,
+ LogOut,
+ Minus,
+ Moon,
+ Pilcrow,
+ Pin,
+ Rows3,
+ Star,
+ Quote,
+ Search,
+ Slash,
+ SquarePen,
+ Strikethrough,
+ Table2,
+ Trash2,
+ Underline as UnderlineIcon,
+ Focus,
+ Sun,
+} from 'lucide-react'
+
+import './App.css'
+import {
+ createNote,
+ deleteNote,
+ getCurrentUser,
+ getNotes,
+ login,
+ logout,
+ register,
+ updateNote,
+ updateUserTheme,
+ uploadAttachment,
+} from './lib/api'
+import type { AuthPayload, Note, NotePayload } from './types'
+import { translate } from './i18n'
+import type { Locale, TranslationKey } from './i18n'
+
+type SessionStatus = 'booting' | 'anonymous' | 'authenticated'
+type AuthMode = 'login' | 'register'
+type SaveState = 'idle' | 'dirty' | 'saving' | 'saved'
+type Shelf = 'all' | 'pinned' | 'favorites' | 'archived'
+
+interface Toast {
+ id: number
+ text: string
+}
+
+interface CommandItem {
+ id: string
+ label: string
+ hint: string
+ keywords: string
+ icon: typeof Pilcrow
+ action: () => void
+}
+
+const defaultDraft: NotePayload = {
+ title: '',
+ text: '',
+ summary: '',
+ tags: [],
+ is_pinned: false,
+ is_favorite: false,
+ is_archived: false,
+}
+
+function formatDate(value: string, locale: Locale) {
+ return new Intl.DateTimeFormat(locale === 'ru' ? 'ru-RU' : 'en-US', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ }).format(new Date(value))
+}
+
+function sortNotes(items: Note[]) {
+ return [...items].sort(
+ (left, right) =>
+ new Date(right.edit_time).getTime() - new Date(left.edit_time).getTime(),
+ )
+}
+
+function htmlToPlainText(value: string) {
+ if (!value) {
+ return ''
+ }
+
+ const doc = new DOMParser().parseFromString(value, 'text/html')
+ return doc.body.textContent?.replace(/\s+/g, ' ').trim() ?? ''
+}
+
+function extractSummary(value: string) {
+ const text = htmlToPlainText(value)
+ return text.slice(0, 280)
+}
+
+function estimateReadingTime(words: number) {
+ return Math.max(1, Math.ceil(words / 180))
+}
+
+function noteToPayload(note: Note): NotePayload {
+ return {
+ title: note.title,
+ text: note.text ?? '',
+ summary: note.summary ?? '',
+ tags: note.tags,
+ is_pinned: note.is_pinned,
+ is_favorite: note.is_favorite,
+ is_archived: note.is_archived,
+ }
+}
+
+function payloadEqualsNote(payload: NotePayload, note: Note | null) {
+ if (!note) {
+ return false
+ }
+
+ return JSON.stringify({
+ ...payload,
+ text: payload.text ?? '',
+ summary: payload.summary ?? '',
+ }) === JSON.stringify(noteToPayload(note))
+}
+
+function App() {
+ const [sessionStatus, setSessionStatus] = useState('booting')
+ const [authMode, setAuthMode] = useState('login')
+ const [authForm, setAuthForm] = useState({ login: '', password: '' })
+ const [authError, setAuthError] = useState('')
+ const [authBusy, setAuthBusy] = useState(false)
+
+ const [notes, setNotes] = useState([])
+ const [selectedNoteId, setSelectedNoteId] = useState(null)
+ const [draft, setDraft] = useState(defaultDraft)
+ const [saveState, setSaveState] = useState('idle')
+ const [notesBusy, setNotesBusy] = useState(false)
+ const [deleteBusy, setDeleteBusy] = useState(false)
+ const [notice, setNotice] = useState('Private notes synced with secure cookie auth.')
+ const [searchQuery, setSearchQuery] = useState('')
+ const [activeShelf, setActiveShelf] = useState('all')
+ const [toolbarOpen, setToolbarOpen] = useState(false)
+ const [focusMode, setFocusMode] = useState(false)
+ const [darkMode, setDarkMode] = useState(() => {
+ const savedTheme = window.localStorage.getItem('notes-theme')
+ return savedTheme
+ ? savedTheme === 'dark'
+ : window.matchMedia('(prefers-color-scheme: dark)').matches
+ })
+ const [locale, setLocale] = useState(() =>
+ window.localStorage.getItem('notes-locale') === 'ru' ? 'ru' : 'en',
+ )
+ const [tagInput, setTagInput] = useState('')
+ const [, setUploadBusy] = useState(false)
+ const [dragActive, setDragActive] = useState(false)
+ const [commandOpen, setCommandOpen] = useState(false)
+ const [commandQuery, setCommandQuery] = useState('')
+ const [toasts, setToasts] = useState([])
+
+ const toastIdRef = useRef(0)
+ const autosaveTimerRef = useRef(null)
+ const titleRef = useRef(null)
+ const fileInputRef = useRef(null)
+ const hydratedNoteIdRef = useRef(null)
+ const dragDepthRef = useRef(0)
+ const localeRef = useRef(locale)
+ const t = useCallback(
+ (key: TranslationKey, values?: Record) => translate(locale, key, values),
+ [locale],
+ )
+
+ const selectedNote = notes.find((note) => note.id === selectedNoteId) ?? null
+ const shelfCounts = useMemo(
+ () => ({
+ all: notes.filter((note) => !note.is_archived).length,
+ pinned: notes.filter((note) => note.is_pinned && !note.is_archived).length,
+ favorites: notes.filter((note) => note.is_favorite && !note.is_archived).length,
+ archived: notes.filter((note) => note.is_archived).length,
+ }),
+ [notes],
+ )
+
+ const filteredNotes = useMemo(() => {
+ const normalized = searchQuery.trim().toLowerCase()
+
+ const shelfItems = notes.filter((note) => {
+ if (activeShelf === 'pinned') {
+ return note.is_pinned && !note.is_archived
+ }
+
+ if (activeShelf === 'favorites') {
+ return note.is_favorite && !note.is_archived
+ }
+
+ if (activeShelf === 'archived') {
+ return note.is_archived
+ }
+
+ return !note.is_archived
+ })
+
+ if (!normalized) {
+ return shelfItems
+ }
+
+ return shelfItems.filter((note) => {
+ const haystack = [
+ note.title,
+ note.summary ?? '',
+ htmlToPlainText(note.text ?? ''),
+ ]
+ .join('\n')
+ .toLowerCase()
+
+ return haystack.includes(normalized)
+ })
+ }, [activeShelf, notes, searchQuery])
+
+ const pushToast = useCallback((text: string) => {
+ const nextToast = { id: ++toastIdRef.current, text }
+ setToasts((current) => [...current, nextToast])
+ window.setTimeout(() => {
+ setToasts((current) => current.filter((toast) => toast.id !== nextToast.id))
+ }, 2600)
+ }, [])
+
+ useEffect(() => {
+ window.localStorage.setItem('notes-theme', darkMode ? 'dark' : 'light')
+ }, [darkMode])
+
+ useEffect(() => {
+ window.localStorage.setItem('notes-locale', locale)
+ document.documentElement.lang = locale
+ }, [locale])
+
+ useEffect(() => {
+ if (sessionStatus !== 'authenticated') {
+ return
+ }
+
+ void updateUserTheme(darkMode ? 'dark' : 'light').catch((error) => {
+ setNotice(error instanceof Error ? error.message : 'Failed to save theme.')
+ })
+ }, [darkMode, sessionStatus])
+
+ const editor = useEditor({
+ extensions: [
+ StarterKit.configure({
+ heading: {
+ levels: [1, 2, 3],
+ },
+ }),
+ Underline,
+ Link.configure({
+ openOnClick: false,
+ }),
+ Placeholder.configure({
+ placeholder: () => translate(localeRef.current, 'editorPlaceholder'),
+ }),
+ TaskList,
+ TaskItem.configure({
+ nested: true,
+ }),
+ Highlight,
+ TextStyle,
+ Color,
+ Image.configure({
+ inline: false,
+ allowBase64: false,
+ }),
+ Table.configure({
+ resizable: true,
+ }),
+ TableRow,
+ TableHeader,
+ TableCell,
+ TextAlign.configure({
+ types: ['heading', 'paragraph'],
+ }),
+ ],
+ content: '',
+ editorProps: {
+ attributes: {
+ class: 'notes-editor__content',
+ },
+ },
+ onUpdate: ({ editor: instance }) => {
+ const html = instance.getHTML()
+ setDraft((current) => ({
+ ...current,
+ text: html,
+ summary: extractSummary(html),
+ }))
+ },
+ })
+
+ useEffect(() => {
+ localeRef.current = locale
+ editor?.view.dispatch(editor.state.tr)
+ }, [editor, locale])
+
+ const dirty = selectedNote ? !payloadEqualsNote(draft, selectedNote) : false
+ const plainText = htmlToPlainText(draft.text ?? '')
+ const wordCount = plainText ? plainText.split(/\s+/).length : 0
+ const readingTime = estimateReadingTime(wordCount)
+
+ const commandItems = useMemo(() => {
+ if (!editor) {
+ return []
+ }
+
+ return [
+ {
+ id: 'paragraph',
+ label: t('bodyText'),
+ hint: t('defaultParagraph'),
+ keywords: 'paragraph body text normal',
+ icon: Pilcrow,
+ action: () => editor.chain().focus().setParagraph().run(),
+ },
+ {
+ id: 'title',
+ label: t('largeHeading'),
+ hint: t('primaryTitle'),
+ keywords: 'heading h1 title big',
+ icon: Heading1,
+ action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
+ },
+ {
+ id: 'section',
+ label: t('sectionHeading'),
+ hint: t('secondaryHeading'),
+ keywords: 'heading h2 section subtitle',
+ icon: Heading2,
+ action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
+ },
+ {
+ id: 'subsection',
+ label: t('subsection'),
+ hint: t('compactHeading'),
+ keywords: 'heading h3 subsection',
+ icon: Heading3,
+ action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
+ },
+ {
+ id: 'quote',
+ label: t('quote'),
+ hint: t('emphasizedQuote'),
+ keywords: 'quote citation blockquote',
+ icon: Quote,
+ action: () => editor.chain().focus().toggleBlockquote().run(),
+ },
+ {
+ id: 'bullets',
+ label: t('bulletList'),
+ hint: t('unorderedList'),
+ keywords: 'list bullets unordered',
+ icon: List,
+ action: () => editor.chain().focus().toggleBulletList().run(),
+ },
+ {
+ id: 'numbered',
+ label: t('numberedList'),
+ hint: t('orderedSequence'),
+ keywords: 'list ordered numbered',
+ icon: ListOrdered,
+ action: () => editor.chain().focus().toggleOrderedList().run(),
+ },
+ {
+ id: 'checklist',
+ label: t('checklist'),
+ hint: t('taskList'),
+ keywords: 'task checklist todos',
+ icon: CheckSquare,
+ action: () => editor.chain().focus().toggleTaskList().run(),
+ },
+ {
+ id: 'code',
+ label: t('codeBlock'),
+ hint: t('preformattedBlock'),
+ keywords: 'code snippet block',
+ icon: Code2,
+ action: () => editor.chain().focus().toggleCodeBlock().run(),
+ },
+ {
+ id: 'divider',
+ label: t('divider'),
+ hint: t('sectionBreak'),
+ keywords: 'divider separator line rule',
+ icon: Minus,
+ action: () => editor.chain().focus().insertContent('
').run(),
+ },
+ {
+ id: 'table',
+ label: t('table'),
+ hint: t('grid'),
+ keywords: 'table grid columns rows',
+ icon: Table2,
+ action: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
+ },
+ {
+ id: 'image',
+ label: t('image'),
+ hint: t('uploadDevice'),
+ keywords: 'image media upload picture photo',
+ icon: ImagePlus,
+ action: () => fileInputRef.current?.click(),
+ },
+ ]
+ }, [editor, t])
+
+ const filteredCommands = useMemo(() => {
+ const query = commandQuery.trim().toLowerCase()
+ if (!query) {
+ return commandItems
+ }
+
+ return commandItems.filter((item) =>
+ `${item.label} ${item.hint} ${item.keywords}`.toLowerCase().includes(query),
+ )
+ }, [commandItems, commandQuery])
+
+ const syncDraft = useCallback((nextNote: Note | null) => {
+ const payload = nextNote ? noteToPayload(nextNote) : defaultDraft
+ setDraft(payload)
+ setTagInput(payload.tags.join(', '))
+ setSaveState('idle')
+ setToolbarOpen(false)
+
+ if (editor) {
+ editor.commands.setContent(payload.text || '', { emitUpdate: false })
+ }
+ }, [editor])
+
+ const loadNotes = useCallback(async (preferredId?: number | null) => {
+ setNotesBusy(true)
+
+ try {
+ const nextNotes = sortNotes(await getNotes())
+ setNotes(nextNotes)
+ setSelectedNoteId((currentId) => {
+ const candidate = preferredId ?? currentId
+ if (candidate && nextNotes.some((note) => note.id === candidate)) {
+ return candidate
+ }
+ return nextNotes[0]?.id ?? null
+ })
+ } catch (error) {
+ setNotice(error instanceof Error ? error.message : 'Failed to load notes.')
+ } finally {
+ setNotesBusy(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ void (async () => {
+ try {
+ const user = await getCurrentUser()
+ setDarkMode(user.theme === 'dark')
+ setSessionStatus('authenticated')
+ setNotice(`Welcome back, ${user.login}.`)
+ await loadNotes()
+ } catch {
+ setNotes([])
+ setSelectedNoteId(null)
+ setSessionStatus('anonymous')
+ setNotice('Sign in to open your notes workspace.')
+ }
+ })()
+ }, [loadNotes])
+
+ useEffect(() => {
+ if (!editor) {
+ return
+ }
+
+ if (selectedNoteId === null) {
+ if (hydratedNoteIdRef.current !== null) {
+ hydratedNoteIdRef.current = null
+ syncDraft(null)
+ }
+ return
+ }
+
+ if (hydratedNoteIdRef.current === selectedNoteId) {
+ return
+ }
+
+ const nextNote = notes.find((note) => note.id === selectedNoteId) ?? null
+ hydratedNoteIdRef.current = selectedNoteId
+ syncDraft(nextNote)
+ }, [editor, notes, selectedNoteId, syncDraft])
+
+ const persistCurrentNote = useCallback(async (successNotice?: string, silent = false) => {
+ if (!selectedNoteId || !selectedNote) {
+ return
+ }
+
+ setSaveState('saving')
+
+ try {
+ const payload = {
+ ...draft,
+ title: draft.title.trim() || 'Untitled note',
+ text: draft.text || '',
+ summary: extractSummary(draft.text || ''),
+ }
+
+ const updated = await updateNote(selectedNoteId, payload)
+ setNotes((current) =>
+ sortNotes(current.map((note) => (note.id === updated.id ? updated : note))),
+ )
+ setDraft(noteToPayload(updated))
+ setTagInput(updated.tags.join(', '))
+ setSaveState('saved')
+ setNotice(successNotice ?? 'Note saved.')
+ if (!silent) {
+ pushToast(successNotice ?? 'Saved')
+ }
+ } catch (error) {
+ setSaveState('dirty')
+ setNotice(error instanceof Error ? error.message : 'Failed to save note.')
+ if (!silent) {
+ pushToast('Save failed')
+ }
+ }
+ }, [draft, pushToast, selectedNote, selectedNoteId])
+
+ useEffect(() => {
+ if (!selectedNote || !dirty) {
+ if (saveState !== 'saving') {
+ setSaveState(selectedNote ? 'saved' : 'idle')
+ }
+ return
+ }
+
+ setSaveState('dirty')
+ if (autosaveTimerRef.current) {
+ window.clearTimeout(autosaveTimerRef.current)
+ }
+
+ autosaveTimerRef.current = window.setTimeout(() => {
+ void persistCurrentNote('Autosaved.', true)
+ }, 900)
+
+ return () => {
+ if (autosaveTimerRef.current) {
+ window.clearTimeout(autosaveTimerRef.current)
+ }
+ }
+ }, [dirty, persistCurrentNote, saveState, selectedNote])
+
+ const handleCreateNote = useCallback(async () => {
+ try {
+ const note = await createNote({
+ ...defaultDraft,
+ title: `Untitled note ${notes.length + 1}`,
+ })
+
+ setNotes((current) => sortNotes([note, ...current.filter((item) => item.id !== note.id)]))
+ setSelectedNoteId(note.id)
+ setNotice('New note created.')
+ pushToast('New note')
+ window.setTimeout(() => titleRef.current?.focus(), 60)
+ } catch (error) {
+ setNotice(error instanceof Error ? error.message : 'Failed to create note.')
+ }
+ }, [notes.length, pushToast])
+
+ const handleDuplicateNote = useCallback(async () => {
+ if (!selectedNote) {
+ return
+ }
+
+ try {
+ const duplicate = await createNote({
+ ...noteToPayload(selectedNote),
+ title: `${selectedNote.title} copy`,
+ })
+ setNotes((current) => sortNotes([duplicate, ...current]))
+ setSelectedNoteId(duplicate.id)
+ setNotice('Note duplicated.')
+ pushToast('Duplicated')
+ } catch (error) {
+ setNotice(error instanceof Error ? error.message : 'Failed to duplicate note.')
+ }
+ }, [pushToast, selectedNote])
+
+ const handleDeleteNote = useCallback(async () => {
+ if (!selectedNoteId || !selectedNote) {
+ return
+ }
+
+ if (!window.confirm(`Delete "${selectedNote.title}"?`)) {
+ return
+ }
+
+ setDeleteBusy(true)
+ try {
+ await deleteNote(selectedNoteId)
+ const remaining = notes.filter((note) => note.id !== selectedNoteId)
+ setNotes(remaining)
+ setSelectedNoteId(remaining[0]?.id ?? null)
+ setNotice('Note deleted.')
+ pushToast('Deleted')
+ } catch (error) {
+ setNotice(error instanceof Error ? error.message : 'Failed to delete note.')
+ } finally {
+ setDeleteBusy(false)
+ }
+ }, [notes, pushToast, selectedNote, selectedNoteId])
+
+ const handleExportNote = useCallback(() => {
+ if (!selectedNote) {
+ return
+ }
+
+ const content = `
+
+
+
+
+ ${draft.title || 'Untitled note'}
+
+
+
+ ${draft.title || 'Untitled note'}
+ ${draft.text || ''}
+
+`
+
+ const blob = new Blob([content], { type: 'text/html;charset=utf-8' })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = `${(draft.title || 'untitled-note').replace(/\s+/g, '-').toLowerCase()}.html`
+ link.click()
+ URL.revokeObjectURL(url)
+ pushToast('Exported')
+ }, [draft, pushToast, selectedNote])
+
+ const updateDraftFlag = useCallback((key: 'is_pinned' | 'is_favorite' | 'is_archived') => {
+ setDraft((current) => ({ ...current, [key]: !current[key] }))
+ }, [])
+
+ const applyTags = useCallback((value: string) => {
+ const nextTags = value
+ .split(',')
+ .map((tag) => tag.trim())
+ .filter(Boolean)
+
+ setTagInput(value)
+ setDraft((current) => ({ ...current, tags: nextTags }))
+ }, [])
+
+ const handleCommandSelect = useCallback((command: CommandItem) => {
+ command.action()
+ setCommandOpen(false)
+ setCommandQuery('')
+ setToolbarOpen(false)
+ }, [])
+
+ const uploadImageFile = useCallback(async (file: File) => {
+ if (!editor || !selectedNoteId) {
+ return
+ }
+
+ setUploadBusy(true)
+ try {
+ const result = await uploadAttachment(file)
+ editor.chain().focus().setImage({ src: result.url, alt: file.name }).run()
+ setNotice('Image inserted into note.')
+ pushToast('Image added')
+ } catch (error) {
+ setNotice(error instanceof Error ? error.message : 'Failed to upload image.')
+ pushToast('Upload failed')
+ } finally {
+ setUploadBusy(false)
+ }
+ }, [editor, pushToast, selectedNoteId])
+
+ async function handleAuthSubmit(event: FormEvent) {
+ event.preventDefault()
+ setAuthBusy(true)
+ setAuthError('')
+
+ try {
+ if (authMode === 'register') {
+ await register(authForm)
+ }
+ await login(authForm)
+ const user = await getCurrentUser()
+ setDarkMode(user.theme === 'dark')
+ setSessionStatus('authenticated')
+ setAuthForm({ login: '', password: '' })
+ setNotice(authMode === 'register' ? `Account created for ${user.login}.` : `Signed in as ${user.login}.`)
+ await loadNotes()
+ } catch (error) {
+ setAuthError(error instanceof Error ? error.message : 'Authentication failed.')
+ } finally {
+ setAuthBusy(false)
+ }
+ }
+
+ async function handleLogout() {
+ setAuthBusy(true)
+ try {
+ await logout()
+ } finally {
+ setNotes([])
+ setSelectedNoteId(null)
+ setDraft(defaultDraft)
+ setSessionStatus('anonymous')
+ setAuthBusy(false)
+ setNotice('Signed out.')
+ }
+ }
+
+ async function handleUploadImage() {
+ const file = fileInputRef.current?.files?.[0]
+ if (!file) {
+ return
+ }
+
+ try {
+ await uploadImageFile(file)
+ } finally {
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''
+ }
+ }
+ }
+
+ useEffect(() => {
+ function onKeyDown(event: globalThis.KeyboardEvent) {
+ const meta = event.metaKey || event.ctrlKey
+
+ if (meta && event.key.toLowerCase() === 's') {
+ event.preventDefault()
+ void persistCurrentNote('Saved manually.')
+ }
+
+ if (meta && event.key.toLowerCase() === 'n') {
+ event.preventDefault()
+ void handleCreateNote()
+ }
+
+ if (meta && event.key.toLowerCase() === 'k') {
+ event.preventDefault()
+ setCommandOpen(true)
+ }
+
+ if (event.key === '/' && editor?.isFocused) {
+ const currentBlockText = editor.state.selection.$from.parent.textContent.trim()
+ if (!currentBlockText) {
+ event.preventDefault()
+ setCommandOpen(true)
+ }
+ }
+
+ if (event.key === 'Escape') {
+ setCommandOpen(false)
+ }
+ }
+
+ window.addEventListener('keydown', onKeyDown)
+ return () => window.removeEventListener('keydown', onKeyDown)
+ }, [editor, handleCreateNote, persistCurrentNote])
+
+ if (sessionStatus === 'booting') {
+ return (
+
+
+
+ )
+ }
+
+ if (sessionStatus === 'anonymous') {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {toolbarOpen ? (
+
+
+
+
+
+
+
+
+
+
+
+ ) : null}
+
+
+
+
+ {selectedNote ? (
+ <>
+
+
+
+ >
+ ) : null}
+
+
+
+
applyTags(tagInput)}
+ onChange={(event) => setTagInput(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ applyTags(tagInput)
+ }
+ }}
+ />
+
setSearchQuery(event.target.value)}
+ />
+
+
+
+
+
+
+
+
+ {selectedNote ? formatDate(selectedNote.edit_time, locale) : t('noNoteSelected')}
+ {saveState === 'saving' ? t('saving') : saveState === 'dirty' ? t('unsaved') : notice}
+ {selectedNote ? (
+
+ {draft.is_pinned ? t('pinned') : draft.is_favorite ? t('favorite') : draft.is_archived ? t('archived') : t('draft')}
+
+ ) : null}
+
+
+ {selectedNote ? (
+
setToolbarOpen(false)}
+ onDragEnter={(event) => {
+ event.preventDefault()
+ dragDepthRef.current += 1
+ setDragActive(true)
+ }}
+ onDragOver={(event) => {
+ event.preventDefault()
+ event.dataTransfer.dropEffect = 'copy'
+ }}
+ onDragLeave={(event) => {
+ event.preventDefault()
+ dragDepthRef.current = Math.max(0, dragDepthRef.current - 1)
+ if (dragDepthRef.current === 0) {
+ setDragActive(false)
+ }
+ }}
+ onDrop={(event) => {
+ event.preventDefault()
+ dragDepthRef.current = 0
+ setDragActive(false)
+
+ const file = Array.from(event.dataTransfer.files).find((item) =>
+ item.type.startsWith('image/'),
+ )
+
+ if (file) {
+ void uploadImageFile(file)
+ }
+ }}
+ >
+ {dragActive ? (
+
+ {t('dropImage')}
+ {t('imageFormats')}
+
+ ) : null}
+
+
+ setDraft((current) => ({ ...current, title: event.target.value }))
+ }
+ />
+
+ {draft.tags.length ? (
+
+ {draft.tags.map((tag) => (
+
+ ))}
+
+ ) : null}
+
+
+
+
+
+
+
+ ) : (
+
+
{t('noNoteSelected')}
+
{t('createFirst')}
+
+
+ )}
+
+
+
+ void handleUploadImage()}
+ />
+
+
+ {toasts.map((toast) => (
+
+ {toast.text}
+
+ ))}
+
+
+ {commandOpen ? (
+ setCommandOpen(false)}>
+
event.stopPropagation()}>
+
+
+ setCommandQuery(event.target.value)}
+ />
+
+
+
+ {filteredCommands.map((command) => {
+ const Icon = command.icon
+ return (
+
+ )
+ })}
+ {!filteredCommands.length ? (
+
{t('noCommands')}
+ ) : null}
+
+
+
+ ) : null}
+
+ )
+}
+
+export default App
diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png
new file mode 100644
index 0000000..02251f4
Binary files /dev/null and b/frontend/src/assets/hero.png differ
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg
new file mode 100644
index 0000000..5101b67
--- /dev/null
+++ b/frontend/src/assets/vite.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
new file mode 100644
index 0000000..91358dd
--- /dev/null
+++ b/frontend/src/i18n.ts
@@ -0,0 +1,60 @@
+export type Locale = 'en' | 'ru'
+
+const translations = {
+ en: {
+ notes: 'Notes', pinned: 'Pinned', favorites: 'Favorites', archive: 'Archive',
+ loading: 'Loading…', emptyNote: 'Empty note', noShelfNotes: 'No notes in this shelf.',
+ editorPlaceholder: 'Start writing your note…', title: 'Title', heading: 'Heading', text: 'Text', bodyText: 'Body text',
+ blockQuote: 'Block quote', enterLink: 'Enter a link', link: 'Link', checklist: 'Checklist', table: 'Table',
+ purpleText: 'Purple text', resetColor: 'Reset color', tags: 'Tags', search: 'Search',
+ lightTheme: 'Light theme', darkTheme: 'Dark theme', switchLight: 'Switch to light theme', switchDark: 'Switch to dark theme',
+ signOut: 'Sign out', signingOut: 'Signing out…', noNoteSelected: 'No note selected', saving: 'Saving…', unsaved: 'Unsaved',
+ favorite: 'Favorite', archived: 'Archived', draft: 'Draft', dropImage: 'Drop image to insert', imageFormats: 'PNG, JPG, WEBP or GIF',
+ words: '{count} words', minRead: '{count} min read', tagCount: '{count} tags', noTags: 'No tags',
+ summaryChars: '{count} summary chars', noSummary: 'No summary', duplicate: 'Duplicate', save: 'Save', delete: 'Delete', deleting: 'Deleting…',
+ createFirst: 'Create a note to start writing.', newNote: 'New note', searchCommands: 'Search commands', noCommands: 'No commands found.',
+ opening: 'Opening notes…', cloudNotes: 'Cloud Notes', authHeading: 'Dark, focused notes with visual formatting.',
+ authCopy: 'Apple Notes inspired workspace with secure sign in, formatting buttons, note library and autosave.',
+ login: 'Login', password: 'Password', openWorkspace: 'Open workspace', createAccount: 'Create account',
+ createInstead: 'Create account instead', existingAccount: 'I already have an account', language: 'Language',
+ largeHeading: 'Large heading', sectionHeading: 'Section heading', subsection: 'Subsection', quote: 'Quote', bulletList: 'Bullet list',
+ numberedList: 'Numbered list', codeBlock: 'Code block', divider: 'Divider', image: 'Image',
+ defaultParagraph: 'Default paragraph block', primaryTitle: 'Primary section title', secondaryHeading: 'Secondary heading block',
+ compactHeading: 'Compact heading block', emphasizedQuote: 'Block quote with emphasis', unorderedList: 'Unordered list',
+ orderedSequence: 'Ordered sequence', taskList: 'Trackable task list', preformattedBlock: 'Monospaced preformatted block',
+ sectionBreak: 'Visual section break', grid: '3 by 3 grid', uploadDevice: 'Upload from device',
+ },
+ ru: {
+ notes: 'Заметки', pinned: 'Закреплённые', favorites: 'Избранное', archive: 'Архив',
+ loading: 'Загрузка…', emptyNote: 'Пустая заметка', noShelfNotes: 'В этом разделе нет заметок.',
+ editorPlaceholder: 'Начните писать заметку…', title: 'Название', heading: 'Заголовок', text: 'Текст', bodyText: 'Основной текст',
+ blockQuote: 'Блок цитаты', enterLink: 'Введите ссылку', link: 'Ссылка', checklist: 'Чек-лист', table: 'Таблица',
+ purpleText: 'Фиолетовый текст', resetColor: 'Сбросить цвет', tags: 'Теги', search: 'Поиск',
+ lightTheme: 'Светлая тема', darkTheme: 'Тёмная тема', switchLight: 'Включить светлую тему', switchDark: 'Включить тёмную тему',
+ signOut: 'Выйти', signingOut: 'Выход…', noNoteSelected: 'Заметка не выбрана', saving: 'Сохранение…', unsaved: 'Не сохранено',
+ favorite: 'Избранное', archived: 'В архиве', draft: 'Черновик', dropImage: 'Перетащите изображение сюда', imageFormats: 'PNG, JPG, WEBP или GIF',
+ words: 'Слов: {count}', minRead: 'Чтение: {count} мин', tagCount: 'Тегов: {count}', noTags: 'Нет тегов',
+ summaryChars: 'Символов в описании: {count}', noSummary: 'Нет описания', duplicate: 'Дублировать', save: 'Сохранить', delete: 'Удалить', deleting: 'Удаление…',
+ createFirst: 'Создайте заметку, чтобы начать.', newNote: 'Новая заметка', searchCommands: 'Поиск команд', noCommands: 'Команды не найдены.',
+ opening: 'Открываем заметки…', cloudNotes: 'Облачные заметки', authHeading: 'Сосредоточьтесь на заметках и форматировании.',
+ authCopy: 'Рабочее пространство в стиле Apple Notes с безопасным входом, форматированием, библиотекой заметок и автосохранением.',
+ login: 'Логин', password: 'Пароль', openWorkspace: 'Открыть заметки', createAccount: 'Создать аккаунт',
+ createInstead: 'Создать аккаунт', existingAccount: 'У меня уже есть аккаунт', language: 'Язык',
+ largeHeading: 'Большой заголовок', sectionHeading: 'Заголовок раздела', subsection: 'Подзаголовок', quote: 'Цитата', bulletList: 'Маркированный список',
+ numberedList: 'Нумерованный список', codeBlock: 'Блок кода', divider: 'Разделитель', image: 'Изображение',
+ defaultParagraph: 'Обычный абзац', primaryTitle: 'Главный заголовок раздела', secondaryHeading: 'Вторичный заголовок',
+ compactHeading: 'Компактный заголовок', emphasizedQuote: 'Выделенный блок цитаты', unorderedList: 'Неупорядоченный список',
+ orderedSequence: 'Упорядоченная последовательность', taskList: 'Список задач', preformattedBlock: 'Моноширинный блок кода',
+ sectionBreak: 'Визуальный разделитель', grid: 'Сетка 3 на 3', uploadDevice: 'Загрузить с устройства',
+ },
+} as const
+
+export type TranslationKey = keyof typeof translations.en
+
+export function translate(locale: Locale, key: TranslationKey, values?: Record) {
+ let value: string = translations[locale][key]
+ for (const [name, replacement] of Object.entries(values ?? {})) {
+ value = value.replace(`{${name}}`, String(replacement))
+ }
+ return value
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..7806ab2
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,56 @@
+@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,600;9..144,700&display=swap');
+
+:root {
+ font-family: var(--font-sans);
+ line-height: 1.5;
+ font-weight: 400;
+ color: var(--ink);
+ background:
+ radial-gradient(circle at top left, rgba(231, 135, 62, 0.16), transparent 28%),
+ radial-gradient(circle at right 20%, rgba(27, 112, 93, 0.1), transparent 24%),
+ linear-gradient(180deg, #f8efe0 0%, #f5ecd9 40%, #f7f1e7 100%);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ --font-sans: 'Manrope', 'Segoe UI', sans-serif;
+ --font-display: 'Fraunces', Georgia, serif;
+ --ink: #111827;
+ --ink-soft: #60574a;
+ --accent: #e7873e;
+ --accent-deep: #9c531c;
+ --forest: #1b705d;
+ --danger: #9b3336;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+}
+
+button,
+input,
+textarea {
+ font: inherit;
+}
+
+button {
+ -webkit-tap-highlight-color: transparent;
+}
+
+#root {
+ min-height: 100vh;
+}
+
+a {
+ color: inherit;
+}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
new file mode 100644
index 0000000..4515614
--- /dev/null
+++ b/frontend/src/lib/api.ts
@@ -0,0 +1,108 @@
+import type { AuthPayload, Note, NotePayload, UploadResponse, User } from '../types'
+
+const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '/api').replace(
+ /\/$/,
+ '',
+)
+
+async function request(path: string, init?: RequestInit): Promise {
+ const response = await fetch(`${API_BASE_URL}${path}`, {
+ credentials: 'include',
+ headers: {
+ Accept: 'application/json',
+ ...(init?.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }),
+ ...init?.headers,
+ },
+ ...init,
+ })
+
+ if (!response.ok) {
+ let message = 'Request failed.'
+
+ try {
+ const data = (await response.json()) as { detail?: string }
+ if (data.detail) {
+ message = data.detail
+ }
+ } catch {
+ message = response.statusText || message
+ }
+
+ throw new Error(message)
+ }
+
+ if (response.status === 204) {
+ return undefined as T
+ }
+
+ return (await response.json()) as T
+}
+
+export function register(payload: AuthPayload) {
+ return request('/users/register', {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ })
+}
+
+export function login(payload: AuthPayload) {
+ return request<{ message: string }>('/users/login', {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ })
+}
+
+export function logout() {
+ return request<{ message: string }>('/users/logout', {
+ method: 'POST',
+ })
+}
+
+export function getCurrentUser() {
+ return request('/users/me')
+}
+
+export function updateUserTheme(theme: 'light' | 'dark') {
+ return request('/users/me/theme', {
+ method: 'PATCH',
+ body: JSON.stringify({ theme }),
+ })
+}
+
+export function getNotes() {
+ return request('/notes')
+}
+
+export function createNote(payload: NotePayload) {
+ return request('/notes', {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ })
+}
+
+export function getNote(noteId: number) {
+ return request(`/notes/${noteId}`)
+}
+
+export function updateNote(noteId: number, payload: NotePayload) {
+ return request(`/notes/${noteId}`, {
+ method: 'PUT',
+ body: JSON.stringify(payload),
+ })
+}
+
+export function deleteNote(noteId: number) {
+ return request<{ message: string }>(`/notes/${noteId}`, {
+ method: 'DELETE',
+ })
+}
+
+export function uploadAttachment(file: File) {
+ const formData = new FormData()
+ formData.append('file', file)
+
+ return request('/attachments', {
+ method: 'POST',
+ body: formData,
+ })
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
new file mode 100644
index 0000000..04876e7
--- /dev/null
+++ b/frontend/src/types.ts
@@ -0,0 +1,38 @@
+export interface User {
+ id: number
+ login: string
+ theme: string
+}
+
+export interface Note {
+ id: number
+ title: string
+ text: string | null
+ summary: string | null
+ tags: string[]
+ is_pinned: boolean
+ is_favorite: boolean
+ is_archived: boolean
+ created_time: string
+ edit_time: string
+ creator_id: number
+}
+
+export interface AuthPayload {
+ login: string
+ password: string
+}
+
+export interface NotePayload {
+ title: string
+ text: string | null
+ summary: string | null
+ tags: string[]
+ is_pinned: boolean
+ is_favorite: boolean
+ is_archived: boolean
+}
+
+export interface UploadResponse {
+ url: string
+}
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..6830b6f
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..8455dcb
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..a133858
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ host: '0.0.0.0',
+ port: 5173,
+ },
+ preview: {
+ host: '0.0.0.0',
+ port: 4173,
+ },
+})