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