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 -[![Python](https://img.shields.io/badge/python-grey?style=for-the-badge&logo=python)](https://python.org) +[![React](https://img.shields.io/badge/react-grey?style=for-the-badge&logo=react)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/typescript-grey?style=for-the-badge&logo=typescript)](https://typescriptlang.org) [![FastAPI](https://img.shields.io/badge/fastapi-grey?style=for-the-badge&logo=fastapi)](https://fastapi.tiangolo.com) [![PostgreSQL](https://img.shields.io/badge/postgresql-grey?style=for-the-badge&logo=postgresql)](https://postgresql.org) [![Docker](https://img.shields.io/badge/docker-grey?style=for-the-badge&logo=docker)](https://docker.com) -[![JWT](https://img.shields.io/badge/JWT-grey?style=for-the-badge&logo=jsonwebtokens)](https://www.jwt.io) -[![Pydantic](https://img.shields.io/badge/Pydantic-grey?style=for-the-badge&logo=pydantic)](https://pydantic.dev) +[![Nginx](https://img.shields.io/badge/nginx-grey?style=for-the-badge&logo=nginx)](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 ( +
+
+ +

{t('opening')}

+
+
+ ) + } + + if (sessionStatus === 'anonymous') { + return ( +
+
+
+ + + + + {authError ?

{authError}

: null} + + + + + +
+
+
+ ) + } + + 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('words', { count: wordCount })} + {t('minRead', { count: readingTime })} + {draft.tags.length ? t('tagCount', { count: draft.tags.length }) : t('noTags')} + {draft.summary ? t('summaryChars', { count: draft.summary.length }) : t('noSummary')} +
+
+ + + +
+
+
+ ) : ( +
+

{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 @@ +Vite 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, + }, +})