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 ( +
+
+ +

{t('opening')}

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

{authError}

: null} + + + + + +
+
+
+ ) + } + + return ( +
+ + +
+
+
+
+ + + + + + + + + + + +
+ + {toolbarOpen ? ( +
+ + + + + + + + + +
+ ) : null} +
+ +
+ + {selectedNote ? ( + <> + + + + + ) : null} + + + + applyTags(tagInput)} + onChange={(event) => setTagInput(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault() + applyTags(tagInput) + } + }} + /> + setSearchQuery(event.target.value)} + /> + + + +
+
+ +
+
+ {selectedNote ? formatDate(selectedNote.edit_time, locale) : t('noNoteSelected')} + {saveState === 'saving' ? t('saving') : saveState === 'dirty' ? t('unsaved') : notice} + {selectedNote ? ( + + {draft.is_pinned ? t('pinned') : draft.is_favorite ? t('favorite') : draft.is_archived ? t('archived') : t('draft')} + + ) : null} +
+ + {selectedNote ? ( +
setToolbarOpen(false)} + onDragEnter={(event) => { + event.preventDefault() + dragDepthRef.current += 1 + setDragActive(true) + }} + onDragOver={(event) => { + event.preventDefault() + event.dataTransfer.dropEffect = 'copy' + }} + onDragLeave={(event) => { + event.preventDefault() + dragDepthRef.current = Math.max(0, dragDepthRef.current - 1) + if (dragDepthRef.current === 0) { + setDragActive(false) + } + }} + onDrop={(event) => { + event.preventDefault() + dragDepthRef.current = 0 + setDragActive(false) + + const file = Array.from(event.dataTransfer.files).find((item) => + item.type.startsWith('image/'), + ) + + if (file) { + void uploadImageFile(file) + } + }} + > + {dragActive ? ( +
+ {t('dropImage')} + {t('imageFormats')} +
+ ) : null} + + + setDraft((current) => ({ ...current, title: event.target.value })) + } + /> + + {draft.tags.length ? ( +
+ {draft.tags.map((tag) => ( + + ))} +
+ ) : null} + +
+ +
+ +
+
+ {t('words', { count: wordCount })} + {t('minRead', { count: readingTime })} + {draft.tags.length ? t('tagCount', { count: draft.tags.length }) : t('noTags')} + {draft.summary ? t('summaryChars', { count: draft.summary.length }) : t('noSummary')} +
+
+ + + +
+
+
+ ) : ( +
+

{t('noNoteSelected')}

+

{t('createFirst')}

+ +
+ )} +
+
+ + void handleUploadImage()} + /> + +
+ {toasts.map((toast) => ( +
+ {toast.text} +
+ ))} +
+ + {commandOpen ? ( +
setCommandOpen(false)}> +
event.stopPropagation()}> +
+ + setCommandQuery(event.target.value)} + /> +
+ +
+ {filteredCommands.map((command) => { + const Icon = command.icon + return ( + + ) + })} + {!filteredCommands.length ? ( +

{t('noCommands')}

+ ) : null} +
+
+
+ ) : null} +
+ ) +} + +export default App diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 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 @@ +Vite diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts new file mode 100644 index 0000000..91358dd --- /dev/null +++ b/frontend/src/i18n.ts @@ -0,0 +1,60 @@ +export type Locale = 'en' | 'ru' + +const translations = { + en: { + notes: 'Notes', pinned: 'Pinned', favorites: 'Favorites', archive: 'Archive', + loading: 'Loading…', emptyNote: 'Empty note', noShelfNotes: 'No notes in this shelf.', + editorPlaceholder: 'Start writing your note…', title: 'Title', heading: 'Heading', text: 'Text', bodyText: 'Body text', + blockQuote: 'Block quote', enterLink: 'Enter a link', link: 'Link', checklist: 'Checklist', table: 'Table', + purpleText: 'Purple text', resetColor: 'Reset color', tags: 'Tags', search: 'Search', + lightTheme: 'Light theme', darkTheme: 'Dark theme', switchLight: 'Switch to light theme', switchDark: 'Switch to dark theme', + signOut: 'Sign out', signingOut: 'Signing out…', noNoteSelected: 'No note selected', saving: 'Saving…', unsaved: 'Unsaved', + favorite: 'Favorite', archived: 'Archived', draft: 'Draft', dropImage: 'Drop image to insert', imageFormats: 'PNG, JPG, WEBP or GIF', + words: '{count} words', minRead: '{count} min read', tagCount: '{count} tags', noTags: 'No tags', + summaryChars: '{count} summary chars', noSummary: 'No summary', duplicate: 'Duplicate', save: 'Save', delete: 'Delete', deleting: 'Deleting…', + createFirst: 'Create a note to start writing.', newNote: 'New note', searchCommands: 'Search commands', noCommands: 'No commands found.', + opening: 'Opening notes…', cloudNotes: 'Cloud Notes', authHeading: 'Dark, focused notes with visual formatting.', + authCopy: 'Apple Notes inspired workspace with secure sign in, formatting buttons, note library and autosave.', + login: 'Login', password: 'Password', openWorkspace: 'Open workspace', createAccount: 'Create account', + createInstead: 'Create account instead', existingAccount: 'I already have an account', language: 'Language', + largeHeading: 'Large heading', sectionHeading: 'Section heading', subsection: 'Subsection', quote: 'Quote', bulletList: 'Bullet list', + numberedList: 'Numbered list', codeBlock: 'Code block', divider: 'Divider', image: 'Image', + defaultParagraph: 'Default paragraph block', primaryTitle: 'Primary section title', secondaryHeading: 'Secondary heading block', + compactHeading: 'Compact heading block', emphasizedQuote: 'Block quote with emphasis', unorderedList: 'Unordered list', + orderedSequence: 'Ordered sequence', taskList: 'Trackable task list', preformattedBlock: 'Monospaced preformatted block', + sectionBreak: 'Visual section break', grid: '3 by 3 grid', uploadDevice: 'Upload from device', + }, + ru: { + notes: 'Заметки', pinned: 'Закреплённые', favorites: 'Избранное', archive: 'Архив', + loading: 'Загрузка…', emptyNote: 'Пустая заметка', noShelfNotes: 'В этом разделе нет заметок.', + editorPlaceholder: 'Начните писать заметку…', title: 'Название', heading: 'Заголовок', text: 'Текст', bodyText: 'Основной текст', + blockQuote: 'Блок цитаты', enterLink: 'Введите ссылку', link: 'Ссылка', checklist: 'Чек-лист', table: 'Таблица', + purpleText: 'Фиолетовый текст', resetColor: 'Сбросить цвет', tags: 'Теги', search: 'Поиск', + lightTheme: 'Светлая тема', darkTheme: 'Тёмная тема', switchLight: 'Включить светлую тему', switchDark: 'Включить тёмную тему', + signOut: 'Выйти', signingOut: 'Выход…', noNoteSelected: 'Заметка не выбрана', saving: 'Сохранение…', unsaved: 'Не сохранено', + favorite: 'Избранное', archived: 'В архиве', draft: 'Черновик', dropImage: 'Перетащите изображение сюда', imageFormats: 'PNG, JPG, WEBP или GIF', + words: 'Слов: {count}', minRead: 'Чтение: {count} мин', tagCount: 'Тегов: {count}', noTags: 'Нет тегов', + summaryChars: 'Символов в описании: {count}', noSummary: 'Нет описания', duplicate: 'Дублировать', save: 'Сохранить', delete: 'Удалить', deleting: 'Удаление…', + createFirst: 'Создайте заметку, чтобы начать.', newNote: 'Новая заметка', searchCommands: 'Поиск команд', noCommands: 'Команды не найдены.', + opening: 'Открываем заметки…', cloudNotes: 'Облачные заметки', authHeading: 'Сосредоточьтесь на заметках и форматировании.', + authCopy: 'Рабочее пространство в стиле Apple Notes с безопасным входом, форматированием, библиотекой заметок и автосохранением.', + login: 'Логин', password: 'Пароль', openWorkspace: 'Открыть заметки', createAccount: 'Создать аккаунт', + createInstead: 'Создать аккаунт', existingAccount: 'У меня уже есть аккаунт', language: 'Язык', + largeHeading: 'Большой заголовок', sectionHeading: 'Заголовок раздела', subsection: 'Подзаголовок', quote: 'Цитата', bulletList: 'Маркированный список', + numberedList: 'Нумерованный список', codeBlock: 'Блок кода', divider: 'Разделитель', image: 'Изображение', + defaultParagraph: 'Обычный абзац', primaryTitle: 'Главный заголовок раздела', secondaryHeading: 'Вторичный заголовок', + compactHeading: 'Компактный заголовок', emphasizedQuote: 'Выделенный блок цитаты', unorderedList: 'Неупорядоченный список', + orderedSequence: 'Упорядоченная последовательность', taskList: 'Список задач', preformattedBlock: 'Моноширинный блок кода', + sectionBreak: 'Визуальный разделитель', grid: 'Сетка 3 на 3', uploadDevice: 'Загрузить с устройства', + }, +} as const + +export type TranslationKey = keyof typeof translations.en + +export function translate(locale: Locale, key: TranslationKey, values?: Record) { + let value: string = translations[locale][key] + for (const [name, replacement] of Object.entries(values ?? {})) { + value = value.replace(`{${name}}`, String(replacement)) + } + return value +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..7806ab2 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,56 @@ +@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,600;9..144,700&display=swap'); + +:root { + font-family: var(--font-sans); + line-height: 1.5; + font-weight: 400; + color: var(--ink); + background: + radial-gradient(circle at top left, rgba(231, 135, 62, 0.16), transparent 28%), + radial-gradient(circle at right 20%, rgba(27, 112, 93, 0.1), transparent 24%), + linear-gradient(180deg, #f8efe0 0%, #f5ecd9 40%, #f7f1e7 100%); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + --font-sans: 'Manrope', 'Segoe UI', sans-serif; + --font-display: 'Fraunces', Georgia, serif; + --ink: #111827; + --ink-soft: #60574a; + --accent: #e7873e; + --accent-deep: #9c531c; + --forest: #1b705d; + --danger: #9b3336; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-width: 320px; +} + +button, +input, +textarea { + font: inherit; +} + +button { + -webkit-tap-highlight-color: transparent; +} + +#root { + min-height: 100vh; +} + +a { + color: inherit; +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..4515614 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,108 @@ +import type { AuthPayload, Note, NotePayload, UploadResponse, User } from '../types' + +const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '/api').replace( + /\/$/, + '', +) + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(`${API_BASE_URL}${path}`, { + credentials: 'include', + headers: { + Accept: 'application/json', + ...(init?.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }), + ...init?.headers, + }, + ...init, + }) + + if (!response.ok) { + let message = 'Request failed.' + + try { + const data = (await response.json()) as { detail?: string } + if (data.detail) { + message = data.detail + } + } catch { + message = response.statusText || message + } + + throw new Error(message) + } + + if (response.status === 204) { + return undefined as T + } + + return (await response.json()) as T +} + +export function register(payload: AuthPayload) { + return request('/users/register', { + method: 'POST', + body: JSON.stringify(payload), + }) +} + +export function login(payload: AuthPayload) { + return request<{ message: string }>('/users/login', { + method: 'POST', + body: JSON.stringify(payload), + }) +} + +export function logout() { + return request<{ message: string }>('/users/logout', { + method: 'POST', + }) +} + +export function getCurrentUser() { + return request('/users/me') +} + +export function updateUserTheme(theme: 'light' | 'dark') { + return request('/users/me/theme', { + method: 'PATCH', + body: JSON.stringify({ theme }), + }) +} + +export function getNotes() { + return request('/notes') +} + +export function createNote(payload: NotePayload) { + return request('/notes', { + method: 'POST', + body: JSON.stringify(payload), + }) +} + +export function getNote(noteId: number) { + return request(`/notes/${noteId}`) +} + +export function updateNote(noteId: number, payload: NotePayload) { + return request(`/notes/${noteId}`, { + method: 'PUT', + body: JSON.stringify(payload), + }) +} + +export function deleteNote(noteId: number) { + return request<{ message: string }>(`/notes/${noteId}`, { + method: 'DELETE', + }) +} + +export function uploadAttachment(file: File) { + const formData = new FormData() + formData.append('file', file) + + return request('/attachments', { + method: 'POST', + body: formData, + }) +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..04876e7 --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,38 @@ +export interface User { + id: number + login: string + theme: string +} + +export interface Note { + id: number + title: string + text: string | null + summary: string | null + tags: string[] + is_pinned: boolean + is_favorite: boolean + is_archived: boolean + created_time: string + edit_time: string + creator_id: number +} + +export interface AuthPayload { + login: string + password: string +} + +export interface NotePayload { + title: string + text: string | null + summary: string | null + tags: string[] + is_pinned: boolean + is_favorite: boolean + is_archived: boolean +} + +export interface UploadResponse { + url: string +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..a133858 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173, + }, + preview: { + host: '0.0.0.0', + port: 4173, + }, +}) 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 -[![Python](https://img.shields.io/badge/python-grey?style=for-the-badge&logo=python)](https://python.org) +[![React](https://img.shields.io/badge/react-grey?style=for-the-badge&logo=react)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/typescript-grey?style=for-the-badge&logo=typescript)](https://typescriptlang.org) [![FastAPI](https://img.shields.io/badge/fastapi-grey?style=for-the-badge&logo=fastapi)](https://fastapi.tiangolo.com) [![PostgreSQL](https://img.shields.io/badge/postgresql-grey?style=for-the-badge&logo=postgresql)](https://postgresql.org) [![Docker](https://img.shields.io/badge/docker-grey?style=for-the-badge&logo=docker)](https://docker.com) -[![JWT](https://img.shields.io/badge/JWT-grey?style=for-the-badge&logo=jsonwebtokens)](https://www.jwt.io) -[![Pydantic](https://img.shields.io/badge/Pydantic-grey?style=for-the-badge&logo=pydantic)](https://pydantic.dev) +[![Nginx](https://img.shields.io/badge/nginx-grey?style=for-the-badge&logo=nginx)](https://nginx.org) -- **Backend Framework:** [FastAPI](https://tiangolo.com) (Asynchronous, High performance) -- **Database Engine:** [PostgreSQL](https://postgresql.org) -- **ORM:** [SQLAlchemy 2.0](https://sqlalchemy.org) (Async extension with `asyncpg`) -- **Data Validation:** [Pydantic v2](https://pydantic.dev) -- **Security:** JWT (JSON Web Tokens) inside secure **HttpOnly Cookies** + `bcrypt` password hashing -- **Environment:** [Docker](https://docker.com) / [OrbStack](https://orbstack.dev) for full containerization +- **Frontend:** React 19, TypeScript, Vite, TipTap, and Lucide icons +- **Backend:** FastAPI, Pydantic, and async SQLAlchemy 2.0 +- **Database:** PostgreSQL 16 +- **Authentication:** JWT in HttpOnly cookies with bcrypt password hashing +- **Web server:** Nginx serving the production frontend and proxying `/api` +- **Deployment:** Docker Compose with health checks and persistent volumes ## 🚀 Quick Start (Docker) -Make sure you have **Docker** or **OrbStack** running on your system. You don't need to install Python or PostgreSQL locally. +Docker or OrbStack is the only requirement. Python, Node.js, Nginx, and PostgreSQL do not need to be installed on the host. 1. **Clone the repository:** + ```bash git clone https://github.com/reallyShould/Cloud_Notes_api.git cd Cloud_Notes_api ``` -2. **Launch the environment:** - This command automatically creates the database, runs migrations (tables initialization), and starts the backend server: +2. **Start the complete application with one command:** + ```bash - docker compose up --build + docker compose up -d --build ``` -3. **Explore the API:** - Once the terminal prints `Uvicorn running on http://0.0.0.0`, open your browser at: - - **Interactive API Docs (Swagger UI):** `http://localhost:8000/docs` +3. **Open Cloud Notes:** + + - On the Docker host: `http://localhost:8080` + - From another device on the home network: `http://SERVER_IP:8080` + - API documentation: `http://SERVER_IP:8080/api/docs` + +The default published port is **8080**. PostgreSQL and FastAPI are not published on the host and are reachable only through the private Docker network. All browser requests use the same Nginx origin, so no domain is required. + +--- + +## ⚙️ Optional Configuration + +The application works without an `.env` file. For a permanent home-server installation, copy the example before the first start: + +```bash +cp .env.example .env +``` + +Recommended settings: + +```dotenv +APP_PORT=8080 +POSTGRES_USER=cloud_notes +POSTGRES_PASSWORD=use-a-long-random-password +POSTGRES_DB=cloud_notes +JWT_SECRET_KEY=use-a-random-secret-at-least-32-bytes-long +COOKIE_SECURE=false +REGISTRATION_ENABLED=true +SQL_ECHO=false +``` + +- `APP_PORT` changes the single host port exposed by the stack. +- `JWT_SECRET_KEY` keeps existing login sessions valid after backend recreation. Without it, a secure temporary key is generated at every backend start. +- `COOKIE_SECURE=false` is required for plain HTTP access by local IP. Set it to `true` only when the application is served through HTTPS. +- Set `REGISTRATION_ENABLED=false` after creating the required accounts if public registration is not needed. +- Database credentials must be selected before the PostgreSQL volume is initialized. Changing them later does not modify an existing database volume. + +Apply configuration changes with: + +```bash +docker compose up -d --build +``` + +## 🔌 Published Ports + +| Service | Container port | Host port | Exposure | +|---|---:|---:|---| +| Nginx frontend | 80 | `8080` by default | Home network | +| FastAPI backend | 8000 | Not published | Docker network only | +| PostgreSQL | 5432 | Not published | Docker network only | + +If `APP_PORT=9090` is set, the application will be available at `http://SERVER_IP:9090` and no other host ports will be opened by this Compose project. + +--- + +## 💾 Persistent Data and Backups + +Docker volumes preserve both database records and uploaded images: + +- `pgdata` stores PostgreSQL data. +- `uploads` stores note attachments. + +On upgrade, files from the legacy `backend/uploads` directory are copied into the attachment volume automatically. Existing attachment links that used `localhost:8000` are normalized by the API. + +Stop containers without deleting data: + +```bash +docker compose down +``` + +Do not use `docker compose down -v` unless all notes and uploaded images may be permanently deleted. + +Create a database backup: + +```bash +docker compose exec -T db pg_dump -U admin main_db > cloud-notes-backup.sql +``` + +When custom PostgreSQL values are configured, replace `admin` and `main_db` in the backup command. + +## 🩺 Operations + +Check container health and published ports: + +```bash +docker compose ps +``` + +Follow application logs: + +```bash +docker compose logs -f frontend server db +``` + +Restart the stack: + +```bash +docker compose restart +``` + +Update after pulling new code: + +```bash +git pull +docker compose up -d --build +``` --- @@ -48,26 +152,29 @@ Make sure you have **Docker** or **OrbStack** running on your system. You don't Cloud_Notes_api/ ├── backend/ │ ├── app/ -│ │ ├── api/ # Modular API Routers (Separation of Concerns) -│ │ │ ├── notes.py # Notes CRUD logic (Markdown supported) -│ │ │ ├── system.py # Health checks and monitoring -│ │ │ └── users.py # Registration & Authentication -│ │ ├── database.py # Async SQLAlchemy engine & session setup -│ │ ├── dependencies.py # Secure request filters (Token decoding & User fetch) -│ │ ├── main.py # Application entrypoint & Lifespan setup -│ │ ├── models.py # SQLAlchemy DB models (User, Note) -│ │ ├── schemas.py # Pydantic validation DTOs -│ │ └── utils.py # Hashing & JWT utility functions -│ ├── Dockerfile -│ └── requirements.txt +│ │ ├── api/ # Notes, users, attachments, and health routes +│ │ ├── database.py # Async SQLAlchemy engine and sessions +│ │ ├── dependencies.py # Authentication dependencies +│ │ ├── main.py # FastAPI application and startup lifecycle +│ │ ├── models.py # User, note, and attachment models +│ │ ├── schemas.py # Request and response validation +│ │ └── utils.py # Password hashing and JWT helpers +│ └── Dockerfile +├── frontend/ +│ ├── src/ # React application, styles, API client, and localization +│ ├── Dockerfile # Vite build and Nginx runtime +│ └── nginx.conf # Static frontend and `/api` reverse proxy ├── docker-compose.yml +├── .env.example └── README.md ``` ---- - -## 🔒 Security Features +## 🔒 Security Notes -- **XSS Protection:** Access tokens are strictly transmitted via **HttpOnly** cookies, preventing malicious JavaScript from stealing sessions. -- **CSRF Mitigation:** Cookie management utilizes `samesite="lax"` policy configuration. -- **Credential Safety:** Passwords are encrypted with a slow `bcrypt` hashing context. No raw credentials ever enter the database. +- Only the Nginx application port is published by default. +- Authentication cookies are HttpOnly and use `SameSite=Lax`. +- Login attempts are limited per client address. +- Uploaded images are size-limited, streamed to disk, and checked by extension, MIME type, and file signature. +- SQL parameter logging is disabled by default. +- The backend container runs as an unprivileged user. +- Plain HTTP does not encrypt credentials or note contents. Use the application only on a trusted LAN, or place it behind HTTPS, Tailscale, or another VPN when remote access is required. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..74ef339 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,12 @@ +__pycache__ +*.py[cod] +.pytest_cache +.mypy_cache +.ruff_cache +.venv +venv +uploads +.env +.env.* +.git +*.log diff --git a/backend/Dockerfile b/backend/Dockerfile index f10f57e..6628b5e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -11,6 +11,12 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY . /code/ +RUN useradd --create-home --uid 10001 appuser \ + && mkdir -p /data/uploads \ + && chown -R appuser:appuser /code /data/uploads + +USER appuser + EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"] diff --git a/backend/app/api/attachments.py b/backend/app/api/attachments.py index 4cd9949..46bf1d1 100644 --- a/backend/app/api/attachments.py +++ b/backend/app/api/attachments.py @@ -1,80 +1,106 @@ +import os +import uuid +from pathlib import Path + from fastapi import APIRouter, Depends, HTTPException, UploadFile, status from fastapi.responses import FileResponse - -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession -import os, uuid - -from ..dependencies import get_current_user from ..database import get_db -from ..models import User, Attachment +from ..dependencies import get_current_user +from ..models import Attachment, User ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"} +ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"} MAX_FILE_SIZE = 25 * 1024 * 1024 +CHUNK_SIZE = 1024 * 1024 +UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "uploads")) attachments_router = APIRouter(prefix="/attachments", tags=["Attachments"]) +def has_valid_image_signature(extension: str, header: bytes) -> bool: + if extension in {".jpg", ".jpeg"}: + return header.startswith(b"\xff\xd8\xff") + if extension == ".png": + return header.startswith(b"\x89PNG\r\n\x1a\n") + if extension == ".gif": + return header.startswith((b"GIF87a", b"GIF89a")) + if extension == ".webp": + return len(header) >= 12 and header[:4] == b"RIFF" and header[8:12] == b"WEBP" + return False + + @attachments_router.post("") -async def upload_attachments( - file: UploadFile, - db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user) +async def upload_attachment( + file: UploadFile, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), ): - extension = os.path.splitext(file.filename)[1].lower() + original_name = file.filename or "upload" + extension = Path(original_name).suffix.lower() - if extension not in ALLOWED_EXTENSIONS: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Unsupported file type. Allowed types: {', '.join(ALLOWED_EXTENSIONS)}" - ) - - if file.size > MAX_FILE_SIZE: - raise HTTPException( - status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, - detail="File is too large. Maximum allowed size is 25 MB." - ) + if extension not in ALLOWED_EXTENSIONS or file.content_type not in ALLOWED_CONTENT_TYPES: + raise HTTPException(status_code=400, detail="Unsupported image type") + UPLOAD_DIR.mkdir(parents=True, exist_ok=True) file_uuid = uuid.uuid4() - saved_name = f"{file_uuid}{extension}" - - file_path = f"uploads/{saved_name}" - file_bytes = await file.read() - - with open(file_path, "wb") as buffer: - buffer.write(file_bytes) - - new_attachment = Attachment( - id=str(file_uuid), - original_name=file.filename, - creator_id=current_user.id - ) + file_path = UPLOAD_DIR / f"{file_uuid}{extension}" + total_size = 0 + header = b"" + + try: + with file_path.open("wb") as destination: + while chunk := await file.read(CHUNK_SIZE): + if not header: + header = chunk[:16] + total_size += len(chunk) + if total_size > MAX_FILE_SIZE: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail="File is too large. Maximum allowed size is 25 MB.", + ) + destination.write(chunk) + + if not has_valid_image_signature(extension, header): + raise HTTPException(status_code=400, detail="File content is not a valid image") + + attachment = Attachment( + id=str(file_uuid), + original_name=original_name, + creator_id=current_user.id, + ) + db.add(attachment) + await db.commit() + except Exception: + file_path.unlink(missing_ok=True) + raise + finally: + await file.close() - db.add(new_attachment) - await db.commit() + return {"url": f"/api/attachments/download/{file_uuid}"} - return {"url": f"http://localhost:8000/attachments/download/{file_uuid}"} @attachments_router.get("/download/{file_uuid}") async def download_attachment( - file_uuid: str, + file_uuid: uuid.UUID, db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user) + current_user: User = Depends(get_current_user), ): - query = select(Attachment).where(Attachment.id == file_uuid) - result = await db.execute(query) + attachment_id = str(file_uuid) + result = await db.execute( + select(Attachment).where( + Attachment.id == attachment_id, + Attachment.creator_id == current_user.id, + ) + ) attachment = result.scalar_one_or_none() - - if attachment is None or attachment.creator_id != current_user.id: + if attachment is None: raise HTTPException(status_code=404, detail="File not found") - files = [f for f in os.listdir("uploads") if f.startswith(file_uuid)] - if not files: + files = list(UPLOAD_DIR.glob(f"{attachment_id}.*")) + if len(files) != 1: raise HTTPException(status_code=404, detail="File on disk not found") - file_name = files[0] - file_path = f"uploads/{file_name}" - - return FileResponse(file_path) - + return FileResponse(files[0], filename=attachment.original_name) diff --git a/backend/app/api/notes.py b/backend/app/api/notes.py index 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 / {