From 4cb038ec787cb67b47418fb584cecef66a982252 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 12 Jun 2026 16:30:07 -0700 Subject: [PATCH] Phase 3 implementation. Coordinate system implemented. Initial text box tool implemented. --- backend/app/api/v1/__init__.py | 9 + backend/app/api/v1/annotations.py | 91 + backend/app/api/v1/debug.py | 56 + backend/app/schemas/annotations.py | 107 + docker-compose.yml | 1 + frontend/.npmrc | 1 + frontend/Dockerfile | 2 +- frontend/nginx.conf | 8 +- frontend/openapi.json | 1 + frontend/package-lock.json | 462 +++- frontend/package.json | 3 + .../editor/canvas/AnnotationLayer.tsx | 181 ++ .../features/editor/pages/PageRenderer.tsx | 88 + .../src/features/editor/pages/PageStack.tsx | 32 + .../src/features/editor/pages/PdfDocument.tsx | 53 + frontend/src/features/editor/store.ts | 50 + .../features/editor/toolbar/EditorToolbar.tsx | 63 + .../editor/toolbar/TextFormatToolbar.tsx | 148 ++ .../src/features/editor/tools/TextTool.ts | 141 ++ frontend/src/features/editor/tools/index.ts | 28 + frontend/src/features/editor/useAutosave.ts | 75 + .../src/features/library/DocumentGrid.tsx | 2 +- frontend/src/features/library/useLibrary.ts | 17 +- frontend/src/lib/annotations/registry.ts | 43 + frontend/src/lib/annotations/types.ts | 28 + frontend/src/lib/coords.test.ts | 95 + frontend/src/lib/coords.ts | 74 + frontend/src/pages/EditorPage.tsx | 46 +- frontend/src/types/api.d.ts | 1501 +++++++++++++ frontend/src/types/api.ts | 1499 +++++++++++++ frontend/tsconfig.app.json | 4 +- openapi.json | 1 + openapi_pretty.json | 1893 +++++++++++++++++ shared/annotation-schema.json | 3 +- 34 files changed, 6676 insertions(+), 130 deletions(-) create mode 100644 backend/app/api/v1/annotations.py create mode 100644 backend/app/api/v1/debug.py create mode 100644 backend/app/schemas/annotations.py create mode 100644 frontend/.npmrc create mode 100644 frontend/openapi.json create mode 100644 frontend/src/features/editor/canvas/AnnotationLayer.tsx create mode 100644 frontend/src/features/editor/pages/PageRenderer.tsx create mode 100644 frontend/src/features/editor/pages/PageStack.tsx create mode 100644 frontend/src/features/editor/pages/PdfDocument.tsx create mode 100644 frontend/src/features/editor/store.ts create mode 100644 frontend/src/features/editor/toolbar/EditorToolbar.tsx create mode 100644 frontend/src/features/editor/toolbar/TextFormatToolbar.tsx create mode 100644 frontend/src/features/editor/tools/TextTool.ts create mode 100644 frontend/src/features/editor/tools/index.ts create mode 100644 frontend/src/features/editor/useAutosave.ts create mode 100644 frontend/src/lib/annotations/registry.ts create mode 100644 frontend/src/lib/annotations/types.ts create mode 100644 frontend/src/lib/coords.test.ts create mode 100644 frontend/src/lib/coords.ts create mode 100644 frontend/src/types/api.d.ts create mode 100644 frontend/src/types/api.ts create mode 100644 openapi.json create mode 100644 openapi_pretty.json diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index da6fd48..73a51d8 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -14,5 +14,14 @@ router.include_router(auth_router) # Documents router.include_router(documents_router) +# Annotations +from app.api.v1.annotations import router as annotations_router +router.include_router(annotations_router) + # Health (unauthenticated) router.include_router(health_router) + +from app.config import settings +if settings.DEBUG: + from app.api.v1.debug import router as debug_router + router.include_router(debug_router) diff --git a/backend/app/api/v1/annotations.py b/backend/app/api/v1/annotations.py new file mode 100644 index 0000000..2db3dcd --- /dev/null +++ b/backend/app/api/v1/annotations.py @@ -0,0 +1,91 @@ +from typing import Any +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.orm import Session +from datetime import datetime, timezone +import json + +from app.db import get_db +from app.models.document import Document +from app.models.annotation_state import AnnotationState +from app.auth.dependencies import get_current_user, verify_csrf +from app.schemas.annotations import AnnotationStateResponse, AnnotationStateUpdateRequest, AnnotationStateUpdateResponse + +router = APIRouter(prefix="/documents", tags=["Annotations"]) + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + +@router.get("/{document_id}/annotations", response_model=AnnotationStateResponse) +async def get_annotations( + document_id: str, + db: Session = Depends(get_db), + user_id: Any = Depends(get_current_user) +): + doc = db.scalar(select(Document).where(Document.id == document_id, Document.deleted_at.is_(None))) + if not doc: + raise HTTPException(status_code=404, detail="Document not found") + + state = db.scalar(select(AnnotationState).where(AnnotationState.document_id == document_id)) + + if not state: + return AnnotationStateResponse(data=[], updatedAt=datetime.now(timezone.utc)) + + try: + data = json.loads(state.data) + except json.JSONDecodeError: + data = [] + + return AnnotationStateResponse( + data=data, + updatedAt=datetime.fromisoformat(state.updated_at) + ) + +@router.put("/{document_id}/annotations", response_model=AnnotationStateUpdateResponse, dependencies=[Depends(verify_csrf)]) +async def update_annotations( + document_id: str, + request: AnnotationStateUpdateRequest, + db: Session = Depends(get_db), + user_id: Any = Depends(get_current_user) +): + doc = db.scalar(select(Document).where(Document.id == document_id, Document.deleted_at.is_(None))) + if not doc: + raise HTTPException(status_code=404, detail="Document not found") + + state = db.scalar(select(AnnotationState).where(AnnotationState.document_id == document_id)) + + # Check for stale tab + if state and request.baseUpdatedAt: + current_updated_at = datetime.fromisoformat(state.updated_at).replace(tzinfo=timezone.utc) + request_updated_at = request.baseUpdatedAt.replace(tzinfo=timezone.utc) + # Allow a small grace period for timezone/parsing discrepancies + if abs((current_updated_at - request_updated_at).total_seconds()) > 1.0: + raise HTTPException( + status_code=409, + detail=f"Conflict: annotations have been modified since {request.baseUpdatedAt}" + ) + + now = _now_iso() + + import json + + # serialize request.data list of models to json string + data_json = json.dumps([a.model_dump(mode="json", by_alias=True) for a in request.data]) + + if not state: + state = AnnotationState( + document_id=document_id, + data=data_json, + updated_at=now + ) + db.add(state) + else: + state.data = data_json + state.updated_at = now + + # Bump document updated_at + doc.updated_at = now + + db.commit() + + return AnnotationStateUpdateResponse(updatedAt=datetime.fromisoformat(now)) diff --git a/backend/app/api/v1/debug.py b/backend/app/api/v1/debug.py new file mode 100644 index 0000000..4d081f7 --- /dev/null +++ b/backend/app/api/v1/debug.py @@ -0,0 +1,56 @@ +import pymupdf +from fastapi import APIRouter, HTTPException +from fastapi.responses import Response +from pydantic import BaseModel + +from app.config import settings + +router = APIRouter(prefix="/debug", tags=["Debug"]) + +class VerifyCoordsRequest(BaseModel): + document_id: str + page: int + x: float + y: float + width: float + height: float + +@router.post("/verify-coords") +async def verify_coords(req: VerifyCoordsRequest): + """ + Test endpoint for cross-engine coordinate verification. + Takes a canonical rect, draws it on the PDF using PyMuPDF, + and returns the flattened PDF. + """ + filepath = settings.PDF_STORAGE_PATH / f"{req.document_id}.pdf" + if not filepath.exists(): + raise HTTPException(status_code=404, detail="Document not found") + + try: + doc = pymupdf.open(filepath) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + if req.page < 0 or req.page >= len(doc): + doc.close() + raise HTTPException(status_code=400, detail="Invalid page number") + + page = doc[req.page] + + # Canonical space is defined as relative to the unrotated CropBox. + # PyMuPDF naturally draws relative to the current rotation. + # By temporarily setting rotation to 0, we can draw directly using canonical coordinates. + original_rotation = page.rotation + if original_rotation != 0: + page.set_rotation(0) + + rect = pymupdf.Rect(req.x, req.y, req.x + req.width, req.y + req.height) + page.draw_rect(rect, color=(1, 0, 0), width=2, fill=(1, 0, 0), fill_opacity=0.3) + + if original_rotation != 0: + page.set_rotation(original_rotation) + + pdf_bytes = doc.write() + doc.close() + + return Response(content=pdf_bytes, media_type="application/pdf") diff --git a/backend/app/schemas/annotations.py b/backend/app/schemas/annotations.py new file mode 100644 index 0000000..577e459 --- /dev/null +++ b/backend/app/schemas/annotations.py @@ -0,0 +1,107 @@ +from datetime import datetime +from typing import Literal, Union, List, Tuple, Optional +from pydantic import BaseModel, Field +from uuid import UUID + +class Rect(BaseModel): + x: float + y: float + width: float + height: float + +class AnnotationBase(BaseModel): + id: UUID + page: int = Field(ge=0) + type: str + rect: Rect + rotation: float = 0.0 + z: int = 0 + createdAt: datetime + updatedAt: datetime + +class TextProps(BaseModel): + text: str + fontFamily: str = "Liberation Sans" + fontSize: float = 14 + color: str = "#000000" + align: Literal["left", "center", "right"] = "left" + bold: bool = False + italic: bool = False + lineHeight: float = 1.2 + highlightColor: Optional[str] = None + +class TextAnnotation(AnnotationBase): + type: Literal["text"] + props: TextProps + +class DrawProps(BaseModel): + paths: List[Tuple[float, float]] + strokeColor: str = "#000000" + strokeWidth: float = 2 + opacity: float = 1.0 + +class DrawAnnotation(AnnotationBase): + type: Literal["draw"] + props: DrawProps + +class SignatureDrawProps(BaseModel): + mode: Literal["draw"] + ref: str + strokeColor: str = "#000000" + +class SignatureTypeProps(BaseModel): + mode: Literal["type"] + text: str + fontFamily: str + color: str = "#000000" + +class SignatureAnnotation(AnnotationBase): + type: Literal["signature"] + props: Union[SignatureDrawProps, SignatureTypeProps] = Field(discriminator="mode") + +class ImageProps(BaseModel): + ref: str + naturalWidth: float + naturalHeight: float + +class ImageAnnotation(AnnotationBase): + type: Literal["image"] + props: ImageProps + +class HighlightProps(BaseModel): + color: str = "#FFEB3B" + opacity: float = 0.3 + +class HighlightAnnotation(AnnotationBase): + type: Literal["highlight"] + props: HighlightProps + +class ShapeProps(BaseModel): + kind: Literal["rect", "ellipse", "line", "arrow"] + strokeColor: str = "#000000" + fillColor: str = "transparent" + strokeWidth: float = 2 + +class ShapeAnnotation(AnnotationBase): + type: Literal["shape"] + props: ShapeProps + +Annotation = Union[ + TextAnnotation, + DrawAnnotation, + SignatureAnnotation, + ImageAnnotation, + HighlightAnnotation, + ShapeAnnotation +] + +class AnnotationStateResponse(BaseModel): + data: List[Annotation] + updatedAt: datetime + +class AnnotationStateUpdateRequest(BaseModel): + data: List[Annotation] + baseUpdatedAt: datetime | None = None + +class AnnotationStateUpdateResponse(BaseModel): + updatedAt: datetime diff --git a/docker-compose.yml b/docker-compose.yml index d67aa0c..4be3bb5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - PAPERJET_DATABASE_PATH=/data/db/app.sqlite - PAPERJET_PDF_STORAGE_PATH=/data/pdfs - PAPERJET_THUMBNAILS_PATH=/data/thumbnails + - PAPERJET_DEBUG=true volumes: - pdf_storage:/data/pdfs - thumbnails:/data/thumbnails diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..521a9f7 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 70b4198..e1c8fe5 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,7 +3,7 @@ FROM node:22-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci +RUN npm install --legacy-peer-deps COPY . . RUN npm run build diff --git a/frontend/nginx.conf b/frontend/nginx.conf index f84ca8b..7f1e76c 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -2,6 +2,12 @@ server { listen 80; server_name _; + include /etc/nginx/mime.types; + types { + application/javascript mjs; + application/wasm wasm; + } + # Match MAX_UPLOAD_MB — raise in both this and the outer reverse proxy client_max_body_size 200m; @@ -42,7 +48,7 @@ server { } # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + location ~* \.(js|mjs|wasm|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404; diff --git a/frontend/openapi.json b/frontend/openapi.json new file mode 100644 index 0000000..62b6e82 --- /dev/null +++ b/frontend/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"PaperJet","description":"Self-hosted PDF editor API","version":"0.1.0"},"paths":{"/api/v1/auth/status":{"get":{"tags":["auth"],"summary":"Get Auth Status","description":"Check if app requires setup and if user is logged in.","operationId":"get_auth_status_api_v1_auth_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthStatusResponse"}}}}}}},"/api/v1/auth/setup":{"post":{"tags":["auth"],"summary":"Setup Password","description":"First-run setup.","operationId":"setup_password_api_v1_auth_setup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Setup Password Api V1 Auth Setup Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/login":{"post":{"tags":["auth"],"summary":"Login","description":"Validate password and issue session cookie.","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Login Api V1 Auth Login Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","description":"Clear session cookie.","operationId":"logout_api_v1_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Logout Api V1 Auth Logout Post"}}}}}}},"/api/v1/auth/password":{"put":{"tags":["auth"],"summary":"Change Password","description":"Change an existing password.","operationId":"change_password_api_v1_auth_password_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Change Password Api V1 Auth Password Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents":{"get":{"tags":["documents"],"summary":"List Documents","description":"List non-trashed documents.","operationId":"list_documents_api_v1_documents_get","parameters":[{"name":"query","in":"query","required":false,"schema":{"type":"string","default":"","title":"Query"}},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Skip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["documents"],"summary":"Upload Document","description":"Upload a new PDF document.","operationId":"upload_document_api_v1_documents_post","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_document_api_v1_documents_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMeta"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/trash":{"get":{"tags":["documents"],"summary":"List Trash","description":"List trashed documents.","operationId":"list_trash_api_v1_documents_trash_get","parameters":[{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Skip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/{id}":{"get":{"tags":["documents"],"summary":"Get Document","operationId":"get_document_api_v1_documents__id__get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMeta"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["documents"],"summary":"Update Document","operationId":"update_document_api_v1_documents__id__patch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMeta"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["documents"],"summary":"Delete Document","description":"Soft delete by default. Hard delete if permanent=True AND already in trash.","operationId":"delete_document_api_v1_documents__id__delete","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}},{"name":"permanent","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Permanent"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/{id}/restore":{"post":{"tags":["documents"],"summary":"Restore Document","operationId":"restore_document_api_v1_documents__id__restore_post","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMeta"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/bulk-delete":{"post":{"tags":["documents"],"summary":"Bulk Delete","operationId":"bulk_delete_api_v1_documents_bulk_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/bulk-restore":{"post":{"tags":["documents"],"summary":"Bulk Restore","operationId":"bulk_restore_api_v1_documents_bulk_restore_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRestoreRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/trash/empty":{"post":{"tags":["documents"],"summary":"Empty Trash","operationId":"empty_trash_api_v1_documents_trash_empty_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/documents/{id}/file":{"get":{"tags":["documents"],"summary":"Get Document File","operationId":"get_document_file_api_v1_documents__id__file_get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/{id}/thumbnail":{"get":{"tags":["documents"],"summary":"Get Document Thumbnail","operationId":"get_document_thumbnail_api_v1_documents__id__thumbnail_get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/{document_id}/annotations":{"get":{"tags":["Annotations"],"summary":"Get Annotations","operationId":"get_annotations_api_v1_documents__document_id__annotations_get","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationStateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Annotations"],"summary":"Update Annotations","operationId":"update_annotations_api_v1_documents__document_id__annotations_put","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationStateUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationStateUpdateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/health":{"get":{"tags":["health"],"summary":"Health Check","description":"Return service health status and version.","operationId":"health_check_api_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Check Api V1 Health Get"}}}}}}},"/api/v1/debug/verify-coords":{"post":{"tags":["Debug"],"summary":"Verify Coords","description":"Test endpoint for cross-engine coordinate verification.\nTakes a canonical rect, draws it on the PDF using PyMuPDF,\nand returns the flattened PDF.","operationId":"verify_coords_api_v1_debug_verify_coords_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyCoordsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AnnotationStateResponse":{"properties":{"data":{"items":{"anyOf":[{"$ref":"#/components/schemas/TextAnnotation"},{"$ref":"#/components/schemas/DrawAnnotation"},{"$ref":"#/components/schemas/SignatureAnnotation"},{"$ref":"#/components/schemas/ImageAnnotation"},{"$ref":"#/components/schemas/HighlightAnnotation"},{"$ref":"#/components/schemas/ShapeAnnotation"}]},"type":"array","title":"Data"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"}},"type":"object","required":["data","updatedAt"],"title":"AnnotationStateResponse"},"AnnotationStateUpdateRequest":{"properties":{"data":{"items":{"anyOf":[{"$ref":"#/components/schemas/TextAnnotation"},{"$ref":"#/components/schemas/DrawAnnotation"},{"$ref":"#/components/schemas/SignatureAnnotation"},{"$ref":"#/components/schemas/ImageAnnotation"},{"$ref":"#/components/schemas/HighlightAnnotation"},{"$ref":"#/components/schemas/ShapeAnnotation"}]},"type":"array","title":"Data"},"baseUpdatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Baseupdatedat"}},"type":"object","required":["data"],"title":"AnnotationStateUpdateRequest"},"AnnotationStateUpdateResponse":{"properties":{"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"}},"type":"object","required":["updatedAt"],"title":"AnnotationStateUpdateResponse"},"AuthStatusResponse":{"properties":{"setupRequired":{"type":"boolean","title":"Setuprequired"},"loggedIn":{"type":"boolean","title":"Loggedin"}},"type":"object","required":["setupRequired","loggedIn"],"title":"AuthStatusResponse"},"Body_upload_document_api_v1_documents_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_document_api_v1_documents_post"},"BulkDeleteRequest":{"properties":{"ids":{"items":{"type":"string"},"type":"array","title":"Ids"}},"type":"object","required":["ids"],"title":"BulkDeleteRequest"},"BulkRestoreRequest":{"properties":{"ids":{"items":{"type":"string"},"type":"array","title":"Ids"}},"type":"object","required":["ids"],"title":"BulkRestoreRequest"},"ChangePasswordRequest":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","minLength":8,"title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"ChangePasswordRequest"},"DocumentListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/DocumentMeta"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["items","total"],"title":"DocumentListResponse"},"DocumentMeta":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"filename":{"type":"string","title":"Filename"},"mime_type":{"type":"string","title":"Mime Type"},"size_bytes":{"type":"integer","title":"Size Bytes"},"in_trash":{"type":"boolean","title":"In Trash"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deleted At"}},"type":"object","required":["id","title","filename","mime_type","size_bytes","in_trash","created_at","updated_at","deleted_at"],"title":"DocumentMeta"},"DocumentUpdateRequest":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"in_trash":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"In Trash"}},"type":"object","title":"DocumentUpdateRequest"},"DrawAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"draw","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/DrawProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"DrawAnnotation"},"DrawProps":{"properties":{"paths":{"items":{"prefixItems":[{"type":"number"},{"type":"number"}],"type":"array","maxItems":2,"minItems":2},"type":"array","title":"Paths"},"strokeColor":{"type":"string","title":"Strokecolor","default":"#000000"},"strokeWidth":{"type":"number","title":"Strokewidth","default":2},"opacity":{"type":"number","title":"Opacity","default":1.0}},"type":"object","required":["paths"],"title":"DrawProps"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HighlightAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"highlight","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/HighlightProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"HighlightAnnotation"},"HighlightProps":{"properties":{"color":{"type":"string","title":"Color","default":"#FFEB3B"},"opacity":{"type":"number","title":"Opacity","default":0.3}},"type":"object","title":"HighlightProps"},"ImageAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"image","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/ImageProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"ImageAnnotation"},"ImageProps":{"properties":{"ref":{"type":"string","title":"Ref"},"naturalWidth":{"type":"number","title":"Naturalwidth"},"naturalHeight":{"type":"number","title":"Naturalheight"}},"type":"object","required":["ref","naturalWidth","naturalHeight"],"title":"ImageProps"},"LoginRequest":{"properties":{"password":{"type":"string","title":"Password"}},"type":"object","required":["password"],"title":"LoginRequest"},"Rect":{"properties":{"x":{"type":"number","title":"X"},"y":{"type":"number","title":"Y"},"width":{"type":"number","title":"Width"},"height":{"type":"number","title":"Height"}},"type":"object","required":["x","y","width","height"],"title":"Rect"},"SetupRequest":{"properties":{"password":{"type":"string","minLength":8,"title":"Password"}},"type":"object","required":["password"],"title":"SetupRequest"},"ShapeAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"shape","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/ShapeProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"ShapeAnnotation"},"ShapeProps":{"properties":{"kind":{"type":"string","enum":["rect","ellipse","line","arrow"],"title":"Kind"},"strokeColor":{"type":"string","title":"Strokecolor","default":"#000000"},"fillColor":{"type":"string","title":"Fillcolor","default":"transparent"},"strokeWidth":{"type":"number","title":"Strokewidth","default":2}},"type":"object","required":["kind"],"title":"ShapeProps"},"SignatureAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"signature","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"oneOf":[{"$ref":"#/components/schemas/SignatureDrawProps"},{"$ref":"#/components/schemas/SignatureTypeProps"}],"title":"Props","discriminator":{"propertyName":"mode","mapping":{"draw":"#/components/schemas/SignatureDrawProps","type":"#/components/schemas/SignatureTypeProps"}}}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"SignatureAnnotation"},"SignatureDrawProps":{"properties":{"mode":{"type":"string","const":"draw","title":"Mode"},"ref":{"type":"string","title":"Ref"},"strokeColor":{"type":"string","title":"Strokecolor","default":"#000000"}},"type":"object","required":["mode","ref"],"title":"SignatureDrawProps"},"SignatureTypeProps":{"properties":{"mode":{"type":"string","const":"type","title":"Mode"},"text":{"type":"string","title":"Text"},"fontFamily":{"type":"string","title":"Fontfamily"},"color":{"type":"string","title":"Color","default":"#000000"}},"type":"object","required":["mode","text","fontFamily"],"title":"SignatureTypeProps"},"TextAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"text","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/TextProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"TextAnnotation"},"TextProps":{"properties":{"text":{"type":"string","title":"Text"},"fontFamily":{"type":"string","title":"Fontfamily","default":"Liberation Sans"},"fontSize":{"type":"number","title":"Fontsize","default":14},"color":{"type":"string","title":"Color","default":"#000000"},"align":{"type":"string","enum":["left","center","right"],"title":"Align","default":"left"},"bold":{"type":"boolean","title":"Bold","default":false},"italic":{"type":"boolean","title":"Italic","default":false},"lineHeight":{"type":"number","title":"Lineheight","default":1.2}},"type":"object","required":["text"],"title":"TextProps"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyCoordsRequest":{"properties":{"document_id":{"type":"string","title":"Document Id"},"page":{"type":"integer","title":"Page"},"x":{"type":"number","title":"X"},"y":{"type":"number","title":"Y"},"width":{"type":"number","title":"Width"},"height":{"type":"number","title":"Height"}},"type":"object","required":["document_id","page","x","y","width","height"],"title":"VerifyCoordsRequest"}}}} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2f3f411..0671781 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@fontsource/outfit": "^5.2.8", "@fontsource/plus-jakarta-sans": "^5.2.8", "@tailwindcss/vite": "^4.3.0", + "@types/uuid": "^10.0.0", "date-fns": "^4.4.0", "fabric": "^7.4.0", "pdfjs-dist": "^6.0.227", @@ -18,6 +19,7 @@ "react-dom": "^19.2.6", "react-router-dom": "^7.17.0", "tailwindcss": "^4.3.0", + "uuid": "^14.0.0", "zustand": "^5.0.14" }, "devDependencies": { @@ -34,6 +36,7 @@ "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", "jsdom": "^29.1.1", + "openapi-typescript": "^7.13.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12", @@ -129,7 +132,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -450,7 +452,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -499,7 +500,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1061,11 +1061,95 @@ "version": "0.133.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.15", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.15.tgz", + "integrity": "sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.1.1", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -1073,6 +1157,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1089,6 +1174,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1105,6 +1191,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1121,6 +1208,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1137,6 +1225,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1153,6 +1242,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1169,6 +1259,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1185,6 +1276,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1201,6 +1293,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1217,6 +1310,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1233,6 +1327,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1249,6 +1344,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1265,6 +1361,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1280,6 +1377,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1291,6 +1389,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1301,6 +1400,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1314,6 +1414,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1330,6 +1431,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1343,6 +1445,7 @@ "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/@standard-schema/spec": { @@ -1609,27 +1712,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -1695,13 +1777,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1752,9 +1827,8 @@ "version": "24.13.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -1763,9 +1837,8 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1776,11 +1849,16 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.61.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", @@ -1826,7 +1904,6 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -2170,7 +2247,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2192,8 +2268,8 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 14" } @@ -2215,28 +2291,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "Python-2.0" }, "node_modules/aria-query": { "version": "5.3.0", @@ -2357,7 +2427,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2418,6 +2487,21 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2428,6 +2512,13 @@ "node": ">=18" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -2435,6 +2526,13 @@ "license": "ISC", "optional": true }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2591,6 +2689,49 @@ "@csstools/css-tokenizer": "^3.0.4" } }, + "node_modules/cssstyle/node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/cssstyle/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -2602,7 +2743,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/data-urls": { @@ -2706,13 +2847,6 @@ "node": ">=8" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.371", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", @@ -2792,7 +2926,6 @@ "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -3210,6 +3343,7 @@ "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" @@ -3285,6 +3419,7 @@ "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, @@ -3392,8 +3527,8 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -3466,6 +3601,19 @@ "node": ">=8" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3526,6 +3674,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3533,6 +3691,19 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", @@ -3930,16 +4101,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4023,6 +4184,7 @@ "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, "funding": [ { "type": "github", @@ -4125,6 +4287,27 @@ "wrappy": "1" } }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4175,6 +4358,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -4231,14 +4432,15 @@ "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.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4246,10 +4448,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -4312,21 +4525,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -4369,7 +4567,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4379,7 +4576,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4387,13 +4583,6 @@ "react": "^19.2.7" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, "node_modules/react-router": { "version": "7.17.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", @@ -4475,6 +4664,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.133.0", @@ -4707,6 +4897,19 @@ "node": ">=0.10.0" } }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -4784,6 +4987,7 @@ "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", @@ -4898,13 +5102,25 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4951,7 +5167,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -4995,6 +5211,13 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5002,12 +5225,25 @@ "license": "MIT", "optional": true }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vite": { "version": "8.0.16", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -5328,6 +5564,23 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5347,7 +5600,6 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index f091777..366c312 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@fontsource/outfit": "^5.2.8", "@fontsource/plus-jakarta-sans": "^5.2.8", "@tailwindcss/vite": "^4.3.0", + "@types/uuid": "^10.0.0", "date-fns": "^4.4.0", "fabric": "^7.4.0", "pdfjs-dist": "^6.0.227", @@ -23,6 +24,7 @@ "react-dom": "^19.2.6", "react-router-dom": "^7.17.0", "tailwindcss": "^4.3.0", + "uuid": "^14.0.0", "zustand": "^5.0.14" }, "devDependencies": { @@ -39,6 +41,7 @@ "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", "jsdom": "^29.1.1", + "openapi-typescript": "^7.13.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12", diff --git a/frontend/src/features/editor/canvas/AnnotationLayer.tsx b/frontend/src/features/editor/canvas/AnnotationLayer.tsx new file mode 100644 index 0000000..2f47e87 --- /dev/null +++ b/frontend/src/features/editor/canvas/AnnotationLayer.tsx @@ -0,0 +1,181 @@ +import { useEffect, useRef } from 'react'; +import * as fabric from 'fabric'; +import { useEditorStore } from '../store'; +import { getTool } from '../../../lib/annotations/registry'; +import type { ViewportParams } from '../../../lib/coords'; +import { screenToPdf } from '../../../lib/coords'; +import { TextFormatToolbar } from '../toolbar/TextFormatToolbar'; + +interface AnnotationLayerProps { + pageNumber: number; + width: number; + height: number; + viewportParams: ViewportParams; +} + +export function AnnotationLayer({ pageNumber, width, height, viewportParams }: AnnotationLayerProps) { + const canvasRef = useRef(null); + const fabricRef = useRef(null); + + const activeToolName = useEditorStore(state => state.activeTool); + + const annotations = useEditorStore(state => state.annotations); + + // Initial setup and event binding + useEffect(() => { + if (!canvasRef.current) return; + + const canvas = new fabric.Canvas(canvasRef.current, { + width, + height, + selection: activeToolName === 'select', + }); + + // Sync fabric modifications back to Zustand + canvas.on('object:modified', (e) => { + const obj = e.target as any; + if (obj && obj.id) { + // Find existing annotation + const ann = useEditorStore.getState().annotations.find(a => a.id === obj.id); + if (ann) { + const pt = screenToPdf({ x: obj.left, y: obj.top }, viewportParams); + + if (ann.type === 'text') { + const scaleX = obj.scaleX || 1; + const scaleY = obj.scaleY || 1; + + // Box width/height in PDF space + const newWidth = (obj.width * scaleX) / (viewportParams.scale * (viewportParams.dpr || 1)); + const newHeight = (obj.height * scaleY) / (viewportParams.scale * (viewportParams.dpr || 1)); + + const textProps = ann.props as any; + const newFontSize = (textProps.fontSize || 14) * scaleY; + + useEditorStore.getState().updateAnnotation(obj.id, { + rect: { ...ann.rect, x: pt.x, y: pt.y, width: newWidth, height: newHeight }, + props: { ...ann.props, fontSize: newFontSize } + }); + } else { + useEditorStore.getState().updateAnnotation(obj.id, { + rect: { ...ann.rect, x: pt.x, y: pt.y } + }); + } + } + } + }); + + const updateSelection = (_e?: any) => { + const activeObj = canvas.getActiveObject() as any; + if (activeObj && activeObj.id) { + useEditorStore.getState().setSelection(activeObj.id); + } else { + useEditorStore.getState().setSelection(null); + } + }; + + canvas.on('selection:created', updateSelection); + canvas.on('selection:updated', updateSelection); + canvas.on('selection:cleared', updateSelection); + + // Keyboard shortcut for delete + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + const activeObj = canvas.getActiveObject() as any; + // Don't delete if we are actively editing text + if (activeObj && activeObj.isEditing === true) { + return; + } + if (activeObj && activeObj.id) { + useEditorStore.getState().deleteAnnotation(activeObj.id); + } + } + }; + + document.addEventListener('keydown', handleKeyDown); + + fabricRef.current = canvas; + + return () => { + document.removeEventListener('keydown', handleKeyDown); + canvas.dispose(); + fabricRef.current = null; + }; + }, []); // Run once on mount + + // Handle dimensions and re-rendering annotations + useEffect(() => { + if (!fabricRef.current) return; + const canvas = fabricRef.current; + canvas.setDimensions({ width, height }); + + // Simple naive sync: clear and re-render everything + // We keep track of the currently active object ID to restore it + const activeObj = canvas.getActiveObject() as any; + const activeId = activeObj?.id; + + canvas.clear(); + + annotations.forEach(ann => { + // Only render annotations for THIS page + if (ann.page === pageNumber) { + const tool = getTool(ann.type); + if (tool && tool.renderToFabric) { + tool.renderToFabric(ann, canvas, viewportParams); + } + } + }); + + if (activeId) { + const newActive = canvas.getObjects().find((o: any) => o.id === activeId); + if (newActive) canvas.setActiveObject(newActive); + } + }, [width, height, annotations, pageNumber]); // We ignore viewportParams identity for now to avoid loops + + // Handle tool activation/deactivation and event binding + useEffect(() => { + const canvas = fabricRef.current; + if (!canvas) return; + + // First deactivate any previous tool logic + // This is a bit tricky if we don't remember the previous tool, + // but we can just clear event listeners. + canvas.off('mouse:down'); + canvas.off('mouse:move'); + canvas.off('mouse:up'); + + canvas.selection = activeToolName === 'select'; + + const tool = getTool(activeToolName); + if (tool) { + if (tool.onActivate) tool.onActivate(canvas); + + if (tool.onPointerDown) { + canvas.on('mouse:down', (e) => tool.onPointerDown!(e, canvas, viewportParams, pageNumber)); + } + if (tool.onPointerMove) { + canvas.on('mouse:move', (e) => tool.onPointerMove!(e, canvas, viewportParams, pageNumber)); + } + if (tool.onPointerUp) { + canvas.on('mouse:up', (e) => tool.onPointerUp!(e, canvas, viewportParams, pageNumber)); + } + } + + return () => { + if (tool && tool.onDeactivate) { + tool.onDeactivate(canvas); + } + }; + }, [activeToolName]); + + const selection = useEditorStore(state => state.selection); + const isSelectedOnThisPage = annotations.find(a => a.id === selection && a.page === pageNumber); + + return ( +
+ + {isSelectedOnThisPage && ( + + )} +
+ ); +} diff --git a/frontend/src/features/editor/pages/PageRenderer.tsx b/frontend/src/features/editor/pages/PageRenderer.tsx new file mode 100644 index 0000000..1a2a27d --- /dev/null +++ b/frontend/src/features/editor/pages/PageRenderer.tsx @@ -0,0 +1,88 @@ +import { useEffect, useRef, useState } from 'react'; +import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; +import { AnnotationLayer } from '../canvas/AnnotationLayer'; + +interface PageRendererProps { + pdfDoc: PDFDocumentProxy; + pageNumber: number; + scale: number; + dpr: number; +} + +export function PageRenderer({ pdfDoc, pageNumber, scale, dpr }: PageRendererProps) { + const canvasRef = useRef(null); + const [page, setPage] = useState(null); + + useEffect(() => { + let active = true; + pdfDoc.getPage(pageNumber).then(p => { + if (active) setPage(p); + }); + return () => { active = false; }; + }, [pdfDoc, pageNumber]); + + useEffect(() => { + if (!page || !canvasRef.current) return; + + const viewport = page.getViewport({ scale: scale * dpr }); + + const canvas = canvasRef.current; + const context = canvas.getContext('2d'); + if (!context) return; + + canvas.width = viewport.width; + canvas.height = viewport.height; + + canvas.style.width = `${viewport.width / dpr}px`; + canvas.style.height = `${viewport.height / dpr}px`; + + const renderContext = { + canvas: canvas, + viewport: viewport, + }; + + let renderTask = page.render(renderContext); + + return () => { + renderTask.cancel(); + }; + }, [page, scale, dpr]); + + if (!page) { + return ( +
+ Loading page {pageNumber}... +
+ ); + } + + const baseViewport = page ? page.getViewport({ scale: 1, rotation: 0 }) : null; + + return ( +
+ + + {baseViewport && ( + + )} + +
+ {pageNumber} +
+
+ ); +} diff --git a/frontend/src/features/editor/pages/PageStack.tsx b/frontend/src/features/editor/pages/PageStack.tsx new file mode 100644 index 0000000..0dbcc4f --- /dev/null +++ b/frontend/src/features/editor/pages/PageStack.tsx @@ -0,0 +1,32 @@ +import type { PDFDocumentProxy } from 'pdfjs-dist'; +import { PageRenderer } from './PageRenderer'; +import { useEditorStore } from '../store'; + +interface PageStackProps { + pdfDoc: PDFDocumentProxy; +} + +export function PageStack({ pdfDoc }: PageStackProps) { + const numPages = pdfDoc.numPages; + const pages = Array.from({ length: numPages }, (_, i) => i + 1); + + const zoom = useEditorStore(state => state.zoom); + + // For now, render all pages vertically. Virtualization comes in Phase 7. + const scale = zoom * 2.0; + const dpr = window.devicePixelRatio || 1; + + return ( +
+ {pages.map(pageNum => ( + + ))} +
+ ); +} diff --git a/frontend/src/features/editor/pages/PdfDocument.tsx b/frontend/src/features/editor/pages/PdfDocument.tsx new file mode 100644 index 0000000..5caeb75 --- /dev/null +++ b/frontend/src/features/editor/pages/PdfDocument.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from 'react'; +import * as pdfjsLib from 'pdfjs-dist'; +import { PageStack } from './PageStack'; + +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.mjs', + import.meta.url +).toString(); + +export function PdfDocument({ documentId }: { documentId: string }) { + const [pdfDoc, setPdfDoc] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let active = true; + const url = `/api/v1/documents/${documentId}/file`; + + const loadingTask = pdfjsLib.getDocument({ url }); + + loadingTask.promise.then((doc) => { + if (active) setPdfDoc(doc); + }).catch(err => { + console.error('Failed to load PDF', err); + if (active) setError(err.message); + }); + + return () => { + active = false; + loadingTask.destroy(); + }; + }, [documentId]); + + if (error) { + return ( +
+
+ Error loading PDF: {error} +
+
+ ); + } + + if (!pdfDoc) { + return ( +
+
+
Loading document...
+
+ ); + } + + return ; +} diff --git a/frontend/src/features/editor/store.ts b/frontend/src/features/editor/store.ts new file mode 100644 index 0000000..664dfe3 --- /dev/null +++ b/frontend/src/features/editor/store.ts @@ -0,0 +1,50 @@ +import { create } from 'zustand'; +import type { Annotation } from '../../lib/annotations/types'; + +export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'; + +interface EditorState { + documentId: string | null; + annotations: Annotation[]; + activeTool: string; + selection: string | null; + zoom: number; + saveStatus: SaveStatus; + + // Actions + setDocumentId: (id: string | null) => void; + setAnnotations: (annotations: Annotation[]) => void; + addAnnotation: (annotation: Annotation) => void; + updateAnnotation: (id: string, updates: Partial) => void; + deleteAnnotation: (id: string) => void; + setActiveTool: (tool: string) => void; + setSelection: (id: string | null) => void; + setZoom: (zoom: number) => void; + setSaveStatus: (status: SaveStatus) => void; +} + +export const useEditorStore = create((set) => ({ + documentId: null, + annotations: [], + activeTool: 'select', + selection: null, + zoom: 1, + saveStatus: 'idle', + + setDocumentId: (id) => set({ documentId: id }), + setAnnotations: (annotations) => set({ annotations }), + addAnnotation: (annotation) => set((state) => ({ + annotations: [...state.annotations, annotation] + })), + updateAnnotation: (id, updates) => set((state) => ({ + annotations: state.annotations.map(a => a.id === id ? { ...a, ...updates } as Annotation : a) + })), + deleteAnnotation: (id) => set((state) => ({ + annotations: state.annotations.filter(a => a.id !== id), + selection: state.selection === id ? null : state.selection + })), + setActiveTool: (tool) => set({ activeTool: tool }), + setSelection: (id) => set({ selection: id }), + setZoom: (zoom) => set({ zoom }), + setSaveStatus: (status) => set({ saveStatus: status }), +})); diff --git a/frontend/src/features/editor/toolbar/EditorToolbar.tsx b/frontend/src/features/editor/toolbar/EditorToolbar.tsx new file mode 100644 index 0000000..5ea1bbe --- /dev/null +++ b/frontend/src/features/editor/toolbar/EditorToolbar.tsx @@ -0,0 +1,63 @@ +import { useEditorStore } from '../store'; + +export function EditorToolbar() { + const { activeTool, setActiveTool, saveStatus, zoom, setZoom } = useEditorStore(); + + const handleZoomIn = () => setZoom(Math.min(zoom + 0.25, 3)); + const handleZoomOut = () => setZoom(Math.max(zoom - 0.25, 0.5)); + + return ( +
+ + + + +
+ +
+ + + {Math.round(zoom * 100)}% + + +
+ +
+ +
+ {saveStatus === 'saving' && 'Saving...'} + {saveStatus === 'saved' && 'Saved'} + {saveStatus === 'error' && Error saving} + {saveStatus === 'idle' && ''} +
+
+ ); +} diff --git a/frontend/src/features/editor/toolbar/TextFormatToolbar.tsx b/frontend/src/features/editor/toolbar/TextFormatToolbar.tsx new file mode 100644 index 0000000..8681cb3 --- /dev/null +++ b/frontend/src/features/editor/toolbar/TextFormatToolbar.tsx @@ -0,0 +1,148 @@ +import { useEditorStore } from '../store'; +import type { TextAnnotation } from '../../../lib/annotations/types'; +import type { ViewportParams } from '../../../lib/coords'; +import { pdfRectToScreen } from '../../../lib/coords'; +import { v4 as uuidv4 } from 'uuid'; + +interface TextFormatToolbarProps { + annotationId: string; + viewportParams: ViewportParams; +} + +const FONTS = ['Liberation Sans', 'Outfit', 'Plus Jakarta Sans']; +const SIZES = [8, 10, 12, 14, 16, 18, 24, 36, 48, 72]; +const COLORS = ['#000000', '#EF4444', '#3B82F6', '#10B981', '#F59E0B', '#ffffff']; +const HIGHLIGHTS = ['transparent', '#FEF08A', '#BBF7D0', '#BFDBFE', '#FBCFE8', '#000000']; + +export function TextFormatToolbar({ annotationId, viewportParams }: TextFormatToolbarProps) { + const { annotations, updateAnnotation, deleteAnnotation, addAnnotation } = useEditorStore(); + + const annotation = annotations.find(a => a.id === annotationId); + if (!annotation || annotation.type !== 'text') return null; + + const textAnn = annotation as TextAnnotation; + const props = textAnn.props; + + // Calculate screen position + const pt = pdfRectToScreen(textAnn.rect, viewportParams); + // Place it just above the text box + const top = pt.y - 48; // 48px above + const left = pt.x; + + const updateProps = (newProps: Partial) => { + updateAnnotation(annotationId, { props: { ...props, ...newProps } }); + }; + + const handleDuplicate = () => { + const newId = uuidv4(); + addAnnotation({ + ...textAnn, + id: newId, + rect: { + ...textAnn.rect, + x: textAnn.rect.x + 20, + y: textAnn.rect.y + 20 + } + }); + // Setting selection to new annotation requires it to be mounted, which is fine, + // but we can just leave selection on the old one for now. + }; + + return ( +
+ {/* Font Family */} + + +
+ + {/* Font Size */} + + +
+ + {/* Bold / Italic */} + + + +
+ + {/* Text Color */} +
+ {COLORS.map(c => ( +
+ +
+ + {/* Highlight Color */} +
+ {HIGHLIGHTS.map(c => ( +
+ +
+ + {/* Duplicate */} + + + {/* Delete */} + +
+ ); +} diff --git a/frontend/src/features/editor/tools/TextTool.ts b/frontend/src/features/editor/tools/TextTool.ts new file mode 100644 index 0000000..4c54c23 --- /dev/null +++ b/frontend/src/features/editor/tools/TextTool.ts @@ -0,0 +1,141 @@ +import * as fabric from 'fabric'; +import { v4 as uuidv4 } from 'uuid'; +import { useEditorStore } from '../store'; +import type { ToolHandler } from '../../../lib/annotations/registry'; +import type { Annotation, TextAnnotation } from '../../../lib/annotations/types'; +import { screenToPdf, pdfRectToScreen } from '../../../lib/coords'; +import type { ViewportParams } from '../../../lib/coords'; + +export const TextTool: ToolHandler = { + name: 'text', + + onActivate: (canvas: fabric.Canvas) => { + canvas.defaultCursor = 'text'; + canvas.selection = false; + }, + + onDeactivate: (canvas: fabric.Canvas) => { + canvas.defaultCursor = 'default'; + }, + + onPointerDown: (e: any, canvas: fabric.Canvas, vp: ViewportParams, pageNumber: number) => { + // If we clicked on an existing object, don't create a new one + if (e.target) return; + + // Create the Fabric object first so the user can immediately type + const pointer = e.scenePoint || canvas.getScenePoint(e.e); + const textbox = new fabric.Textbox('', { + left: pointer.x, + top: pointer.y, + width: 150 * vp.scale, + fontFamily: 'Liberation Sans', + fontSize: 14 * vp.scale * (vp.dpr || 1), // scaled loosely for screen display + fontWeight: 'normal', + fontStyle: 'normal', + fill: '#000000', + backgroundColor: undefined, + originX: 'left', + originY: 'top', + transparentCorners: false, + cornerColor: '#3b82f6', + cornerStrokeColor: '#3b82f6', + borderColor: '#3b82f6', + cornerSize: 8, + padding: 5, + }); + + // Hide all corners and vertical resizers, leaving only middle-left and middle-right for width + textbox.setControlsVisibility({ + tl: false, + tr: false, + bl: false, + br: false, + mt: false, + mb: false, + mtr: false // hide rotation handle for now + }); + + canvas.add(textbox); + canvas.setActiveObject(textbox); + textbox.enterEditing(); + + textbox.on('editing:exited', () => { + if (!textbox.text || textbox.text.trim() === '') { + canvas.remove(textbox); + return; + } + + const pt = screenToPdf({ x: textbox.left!, y: textbox.top! }, vp); + + const newAnn: TextAnnotation = { + id: uuidv4(), + page: pageNumber, + type: 'text', + rect: { + x: pt.x, + y: pt.y, + width: textbox.width! / (vp.scale * (vp.dpr || 1)), + height: textbox.height! / (vp.scale * (vp.dpr || 1)) + }, + rotation: 0, + z: 0, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + props: { + text: textbox.text, + fontFamily: 'Liberation Sans', + fontSize: 14, + color: '#000000', + align: 'left', + bold: false, + italic: false, + lineHeight: 1.2, + highlightColor: null + } + }; + + // We attach the ID to the fabric object so we can link it + (textbox as any).id = newAnn.id; + + useEditorStore.getState().addAnnotation(newAnn); + }); + }, + + renderToFabric: (annotation: Annotation, canvas: fabric.Canvas, vp: ViewportParams) => { + const textAnn = annotation as TextAnnotation; + const pt = pdfRectToScreen(textAnn.rect, vp); + + const textbox = new fabric.Textbox(textAnn.props.text, { + left: pt.x, + top: pt.y, + width: textAnn.rect.width * vp.scale * (vp.dpr || 1), + fontFamily: textAnn.props.fontFamily, + fontSize: textAnn.props.fontSize * vp.scale * (vp.dpr || 1), + fontWeight: textAnn.props.bold ? 'bold' : 'normal', + fontStyle: textAnn.props.italic ? 'italic' : 'normal', + fill: textAnn.props.color, + backgroundColor: textAnn.props.highlightColor || undefined, + originX: 'left', + originY: 'top', + transparentCorners: false, + cornerColor: '#3b82f6', + cornerStrokeColor: '#3b82f6', + borderColor: '#3b82f6', + cornerSize: 8, + padding: 5, + }); + + textbox.setControlsVisibility({ + tl: false, + tr: false, + bl: false, + br: false, + mt: false, + mb: false, + mtr: false + }); + + (textbox as any).id = annotation.id; + canvas.add(textbox); + } +}; diff --git a/frontend/src/features/editor/tools/index.ts b/frontend/src/features/editor/tools/index.ts new file mode 100644 index 0000000..139442d --- /dev/null +++ b/frontend/src/features/editor/tools/index.ts @@ -0,0 +1,28 @@ +import { registerTool } from '../../../lib/annotations/registry'; +import { TextTool } from './TextTool'; + +// Register all tools here +registerTool(TextTool); + +// SelectTool implementation +registerTool({ + name: 'select', + onActivate: (canvas) => { + canvas.defaultCursor = 'default'; + canvas.selection = true; + + // Allow interacting with objects + canvas.forEachObject(obj => { + obj.set('selectable', true); + obj.set('evented', true); + }); + }, + onDeactivate: (canvas) => { + // Lock all objects + canvas.forEachObject(obj => { + obj.set('selectable', false); + obj.set('evented', false); + }); + canvas.discardActiveObject(); + } +}); diff --git a/frontend/src/features/editor/useAutosave.ts b/frontend/src/features/editor/useAutosave.ts new file mode 100644 index 0000000..5f060d6 --- /dev/null +++ b/frontend/src/features/editor/useAutosave.ts @@ -0,0 +1,75 @@ +import { useEffect, useRef } from 'react'; +import { useEditorStore } from './store'; +import type { Annotation } from '../../lib/annotations/types'; + +export function useAutosave() { + const { documentId, annotations, setSaveStatus, setAnnotations } = useEditorStore(); + const timeoutRef = useRef(null); + const isFirstLoad = useRef(true); + + // Initial load + useEffect(() => { + if (!documentId) return; + + let active = true; + + async function load() { + try { + const res = await fetch(`/api/v1/documents/${documentId}/annotations`); + if (!res.ok) throw new Error('Failed to load annotations'); + const data = await res.json(); + + if (active) { + setAnnotations(data.data as Annotation[]); + setSaveStatus('saved'); + isFirstLoad.current = false; + } + } catch (err) { + console.error('Error loading annotations:', err); + if (active) setSaveStatus('error'); + } + } + + load(); + + return () => { active = false; }; + }, [documentId, setAnnotations, setSaveStatus]); + + // Debounced save + useEffect(() => { + // Don't save on the initial load! + if (isFirstLoad.current) return; + if (!documentId) return; + + setSaveStatus('saving'); + + if (timeoutRef.current) { + window.clearTimeout(timeoutRef.current); + } + + timeoutRef.current = window.setTimeout(async () => { + try { + const res = await fetch(`/api/v1/documents/${documentId}/annotations`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ + data: annotations + }) + }); + + if (!res.ok) throw new Error('Failed to save'); + setSaveStatus('saved'); + } catch (err) { + console.error('Error saving annotations:', err); + setSaveStatus('error'); + } + }, 500); // 500ms debounce + + return () => { + if (timeoutRef.current) window.clearTimeout(timeoutRef.current); + }; + }, [annotations, documentId, setSaveStatus]); +} diff --git a/frontend/src/features/library/DocumentGrid.tsx b/frontend/src/features/library/DocumentGrid.tsx index efa7e6a..7d7604b 100644 --- a/frontend/src/features/library/DocumentGrid.tsx +++ b/frontend/src/features/library/DocumentGrid.tsx @@ -6,7 +6,7 @@ import { formatDistanceToNow } from 'date-fns' import { ContextMenu } from './ContextMenu' export const DocumentGrid = ({ isTrash }: { isTrash: boolean }) => { - const { documents, selectedIds, toggleSelection, isLoading, error, deleteDocument, restoreDocument, updateDocument } = useLibrary() + const { documents, selectedIds, isLoading, error, deleteDocument, restoreDocument, updateDocument } = useLibrary() const navigate = useNavigate() const [contextMenu, setContextMenu] = useState<{ x: number, y: number, docId: string } | null>(null) diff --git a/frontend/src/features/library/useLibrary.ts b/frontend/src/features/library/useLibrary.ts index 4634747..f429d95 100644 --- a/frontend/src/features/library/useLibrary.ts +++ b/frontend/src/features/library/useLibrary.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { libraryApi } from './api' -import type { DocumentMeta } from './types' +import type { DocumentMeta, DocumentUpdateRequest } from './types' interface LibraryState { documents: DocumentMeta[] @@ -25,6 +25,7 @@ interface LibraryState { bulkRestore: () => Promise emptyTrash: () => Promise setSearchQuery: (query: string) => void + updateDocument: (id: string, data: DocumentUpdateRequest) => Promise } export const useLibrary = create((set, get) => ({ @@ -176,5 +177,19 @@ export const useLibrary = create((set, get) => ({ set({ error: err.error?.message || err.message, isLoading: false }) throw err } + }, + + updateDocument: async (id: string, data: DocumentUpdateRequest) => { + set({ isLoading: true, error: null }) + try { + const updatedDoc = await libraryApi.updateDocument(id, data) + set((state) => ({ + documents: state.documents.map(d => d.id === id ? updatedDoc : d), + isLoading: false + })) + } catch (err: any) { + set({ error: err.error?.message || err.message, isLoading: false }) + throw err + } } })) diff --git a/frontend/src/lib/annotations/registry.ts b/frontend/src/lib/annotations/registry.ts new file mode 100644 index 0000000..e97565d --- /dev/null +++ b/frontend/src/lib/annotations/registry.ts @@ -0,0 +1,43 @@ +import type { Canvas } from 'fabric'; +import type { Annotation } from './types'; +import type { ViewportParams } from '../coords'; + +/** + * Interface that all annotation tools must implement. + */ +export interface ToolHandler { + /** Uniquely identifies the tool, usually matching the annotation type. */ + name: string; + + /** Called when the tool is selected. Use this to change cursors, etc. */ + onActivate?: (canvas: Canvas) => void; + + /** Called when the tool is deselected. */ + onDeactivate?: (canvas: Canvas) => void; + + /** Called when the user presses down on the canvas. */ + onPointerDown?: (e: any, canvas: Canvas, viewportParams: ViewportParams, pageNumber: number) => void; + + /** Called when the user drags the pointer. */ + onPointerMove?: (e: any, canvas: Canvas, viewportParams: ViewportParams, pageNumber: number) => void; + + /** Called when the user releases the pointer. */ + onPointerUp?: (e: any, canvas: Canvas, viewportParams: ViewportParams, pageNumber: number) => void; + + /** Renders a canonical Annotation onto the given Fabric Canvas. */ + renderToFabric?: (annotation: Annotation, canvas: Canvas, viewportParams: ViewportParams) => void; +} + +const registry = new Map(); + +export function registerTool(handler: ToolHandler) { + registry.set(handler.name, handler); +} + +export function getTool(name: string): ToolHandler | undefined { + return registry.get(name); +} + +export function getAllTools(): ToolHandler[] { + return Array.from(registry.values()); +} diff --git a/frontend/src/lib/annotations/types.ts b/frontend/src/lib/annotations/types.ts new file mode 100644 index 0000000..4559b82 --- /dev/null +++ b/frontend/src/lib/annotations/types.ts @@ -0,0 +1,28 @@ +import type { components } from '../../types/api'; + +export type TextProps = components['schemas']['TextProps'] & { highlightColor?: string | null }; + +export type TextAnnotation = Omit & { + props: TextProps; +}; +export type DrawAnnotation = components['schemas']['DrawAnnotation']; +export type SignatureAnnotation = components['schemas']['SignatureAnnotation']; +export type ImageAnnotation = components['schemas']['ImageAnnotation']; +export type HighlightAnnotation = components['schemas']['HighlightAnnotation']; +export type ShapeAnnotation = components['schemas']['ShapeAnnotation']; + +export type Annotation = + | TextAnnotation + | DrawAnnotation + | SignatureAnnotation + | ImageAnnotation + | HighlightAnnotation + | ShapeAnnotation; + +export type Rect = components['schemas']['Rect']; +export type DrawProps = components['schemas']['DrawProps']; +export type SignatureDrawProps = components['schemas']['SignatureDrawProps']; +export type SignatureTypeProps = components['schemas']['SignatureTypeProps']; +export type ImageProps = components['schemas']['ImageProps']; +export type HighlightProps = components['schemas']['HighlightProps']; +export type ShapeProps = components['schemas']['ShapeProps']; diff --git a/frontend/src/lib/coords.test.ts b/frontend/src/lib/coords.test.ts new file mode 100644 index 0000000..0d4f37a --- /dev/null +++ b/frontend/src/lib/coords.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest'; +import { + pdfToScreen, + screenToPdf, + pdfRectToScreen, + screenRectToPdf, +} from './coords'; +import type { + ViewportParams, + PdfPoint, + PdfRect +} from './coords'; + +describe('Coordinate Transforms', () => { + const W = 612; // 8.5 inches at 72dpi + const H = 792; // 11 inches at 72dpi + + const rotations = [0, 90, 180, 270]; + const zooms = [0.5, 1.0, 2.0]; + const dprs = [1, 2]; + + const testPoints: PdfPoint[] = [ + { x: 0, y: 0 }, + { x: W, y: 0 }, + { x: 0, y: H }, + { x: W, y: H }, + { x: W / 2, y: H / 2 }, + { x: 100, y: 150 }, + ]; + + const testRects: PdfRect[] = [ + { x: 0, y: 0, width: 0, height: 0 }, // zero-width + { x: 10, y: 20, width: 100, height: 200 }, + { x: W - 50, y: H - 50, width: 50, height: 50 }, // boundary + ]; + + it('should round-trip points accurately', () => { + for (const rotation of rotations) { + for (const scale of zooms) { + for (const dpr of dprs) { + const vp: ViewportParams = { scale, rotation, canonicalWidth: W, canonicalHeight: H, dpr }; + + for (const p of testPoints) { + const screen = pdfToScreen(p, vp); + const backToPdf = screenToPdf(screen, vp); + + expect(backToPdf.x).toBeCloseTo(p.x, 2); + expect(backToPdf.y).toBeCloseTo(p.y, 2); + } + } + } + } + }); + + it('should round-trip rects accurately', () => { + for (const rotation of rotations) { + for (const scale of zooms) { + for (const dpr of dprs) { + const vp: ViewportParams = { scale, rotation, canonicalWidth: W, canonicalHeight: H, dpr }; + + for (const r of testRects) { + const screenR = pdfRectToScreen(r, vp); + const backToPdf = screenRectToPdf(screenR, vp); + + expect(backToPdf.x).toBeCloseTo(r.x, 2); + expect(backToPdf.y).toBeCloseTo(r.y, 2); + expect(backToPdf.width).toBeCloseTo(r.width, 2); + expect(backToPdf.height).toBeCloseTo(r.height, 2); + } + } + } + } + }); + + it('should handle CropBox offset equivalently (since canonical space is relative to CropBox top-left)', () => { + const vp: ViewportParams = { scale: 1, rotation: 0, canonicalWidth: 612, canonicalHeight: 792 }; + const p = { x: 306, y: 396 }; + const s = pdfToScreen(p, vp); + expect(s.x).toBeCloseTo(306, 2); + expect(s.y).toBeCloseTo(396, 2); + }); + + it('should correctly rotate points (90 deg)', () => { + const vp: ViewportParams = { scale: 1, rotation: 90, canonicalWidth: W, canonicalHeight: H }; + const tl = { x: 0, y: 0 }; + const tlS = pdfToScreen(tl, vp); + expect(tlS.x).toBeCloseTo(H, 2); + expect(tlS.y).toBeCloseTo(0, 2); + + const bl = { x: 0, y: H }; + const blS = pdfToScreen(bl, vp); + expect(blS.x).toBeCloseTo(0, 2); + expect(blS.y).toBeCloseTo(0, 2); + }); +}); diff --git a/frontend/src/lib/coords.ts b/frontend/src/lib/coords.ts new file mode 100644 index 0000000..701d530 --- /dev/null +++ b/frontend/src/lib/coords.ts @@ -0,0 +1,74 @@ +export interface PdfPoint { x: number; y: number } +export interface ScreenPoint { x: number; y: number } + +export interface PdfRect { x: number; y: number; width: number; height: number } +export interface ScreenRect { x: number; y: number; width: number; height: number } + +export interface ViewportParams { + scale: number; // CSS Zoom level (1.0 = 100%) + rotation: number; // 0, 90, 180, 270 (clockwise) + canonicalWidth: number; // Unrotated CropBox width in PDF points + canonicalHeight: number;// Unrotated CropBox height in PDF points + dpr?: number; // Device Pixel Ratio (defaults to 1) +} + +function getEffectiveScale(vp: ViewportParams): number { + return vp.scale * (vp.dpr || 1); +} + +export function pdfToScreen(p: PdfPoint, vp: ViewportParams): ScreenPoint { + const s = getEffectiveScale(vp); + const { rotation, canonicalWidth: W, canonicalHeight: H } = vp; + const { x, y } = p; + + const rot = ((rotation % 360) + 360) % 360; + + switch (rot) { + case 0: return { x: x * s, y: y * s }; + case 90: return { x: (H - y) * s, y: x * s }; + case 180: return { x: (W - x) * s, y: (H - y) * s }; + case 270: return { x: y * s, y: (W - x) * s }; + default: throw new Error(`Invalid rotation: ${rotation}`); + } +} + +export function screenToPdf(p: ScreenPoint, vp: ViewportParams): PdfPoint { + const s = getEffectiveScale(vp); + const { rotation, canonicalWidth: W, canonicalHeight: H } = vp; + const sx = p.x / s; + const sy = p.y / s; + + const rot = ((rotation % 360) + 360) % 360; + + switch (rot) { + case 0: return { x: sx, y: sy }; + case 90: return { x: sy, y: H - sx }; + case 180: return { x: W - sx, y: H - sy }; + case 270: return { x: W - sy, y: sx }; + default: throw new Error(`Invalid rotation: ${rotation}`); + } +} + +export function pdfRectToScreen(r: PdfRect, vp: ViewportParams): ScreenRect { + const p1 = pdfToScreen({ x: r.x, y: r.y }, vp); + const p2 = pdfToScreen({ x: r.x + r.width, y: r.y + r.height }, vp); + + return { + x: Math.min(p1.x, p2.x), + y: Math.min(p1.y, p2.y), + width: Math.abs(p2.x - p1.x), + height: Math.abs(p2.y - p1.y) + }; +} + +export function screenRectToPdf(r: ScreenRect, vp: ViewportParams): PdfRect { + const p1 = screenToPdf({ x: r.x, y: r.y }, vp); + const p2 = screenToPdf({ x: r.x + r.width, y: r.y + r.height }, vp); + + return { + x: Math.min(p1.x, p2.x), + y: Math.min(p1.y, p2.y), + width: Math.abs(p2.x - p1.x), + height: Math.abs(p2.y - p1.y) + }; +} diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index aee16ef..102af3e 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -1,24 +1,38 @@ -import { useParams } from 'react-router-dom' +import { useParams, Link } from 'react-router-dom' +import { useEffect } from 'react' +import { PdfDocument } from '../features/editor/pages/PdfDocument' +import { EditorToolbar } from '../features/editor/toolbar/EditorToolbar' +import { useEditorStore } from '../features/editor/store' +import { useAutosave } from '../features/editor/useAutosave' +import '../features/editor/tools' /** * Editor page — PDF canvas workspace. - * Full implementation begins in Phase 2 (rendering) and Phase 3 (annotations). */ export function EditorPage() { const { id } = useParams<{ id: string }>() + const { setDocumentId } = useEditorStore() + useAutosave() + + useEffect(() => { + if (id) { + setDocumentId(id) + } + }, [id, setDocumentId]) + + if (!id) return
Invalid document ID
return ( -
+
{/* Editor toolbar */} -
+
- ← Back - + Document {id} @@ -30,20 +44,18 @@ export function EditorPage() {
- {/* Canvas area */} -
-
- PDF viewer will render here (Phase 2) -
-
+ {/* Main workspace area */} +
+ + +
) } diff --git a/frontend/src/types/api.d.ts b/frontend/src/types/api.d.ts new file mode 100644 index 0000000..49716a4 --- /dev/null +++ b/frontend/src/types/api.d.ts @@ -0,0 +1,1501 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/v1/auth/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Auth Status + * @description Check if app requires setup and if user is logged in. + */ + get: operations["get_auth_status_api_v1_auth_status_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/auth/setup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Setup Password + * @description First-run setup. + */ + post: operations["setup_password_api_v1_auth_setup_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Login + * @description Validate password and issue session cookie. + */ + post: operations["login_api_v1_auth_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Logout + * @description Clear session cookie. + */ + post: operations["logout_api_v1_auth_logout_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/auth/password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Change Password + * @description Change an existing password. + */ + put: operations["change_password_api_v1_auth_password_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Documents + * @description List non-trashed documents. + */ + get: operations["list_documents_api_v1_documents_get"]; + put?: never; + /** + * Upload Document + * @description Upload a new PDF document. + */ + post: operations["upload_document_api_v1_documents_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/trash": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Trash + * @description List trashed documents. + */ + get: operations["list_trash_api_v1_documents_trash_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Document */ + get: operations["get_document_api_v1_documents__id__get"]; + put?: never; + post?: never; + /** + * Delete Document + * @description Soft delete by default. Hard delete if permanent=True AND already in trash. + */ + delete: operations["delete_document_api_v1_documents__id__delete"]; + options?: never; + head?: never; + /** Update Document */ + patch: operations["update_document_api_v1_documents__id__patch"]; + trace?: never; + }; + "/api/v1/documents/{id}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Restore Document */ + post: operations["restore_document_api_v1_documents__id__restore_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/bulk-delete": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Bulk Delete */ + post: operations["bulk_delete_api_v1_documents_bulk_delete_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/bulk-restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Bulk Restore */ + post: operations["bulk_restore_api_v1_documents_bulk_restore_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/trash/empty": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Empty Trash */ + post: operations["empty_trash_api_v1_documents_trash_empty_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/{id}/file": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Document File */ + get: operations["get_document_file_api_v1_documents__id__file_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/{id}/thumbnail": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Document Thumbnail */ + get: operations["get_document_thumbnail_api_v1_documents__id__thumbnail_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/{document_id}/annotations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Annotations */ + get: operations["get_annotations_api_v1_documents__document_id__annotations_get"]; + /** Update Annotations */ + put: operations["update_annotations_api_v1_documents__document_id__annotations_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health Check + * @description Return service health status and version. + */ + get: operations["health_check_api_v1_health_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/debug/verify-coords": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Verify Coords + * @description Test endpoint for cross-engine coordinate verification. + * Takes a canonical rect, draws it on the PDF using PyMuPDF, + * and returns the flattened PDF. + */ + post: operations["verify_coords_api_v1_debug_verify_coords_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** AnnotationStateResponse */ + AnnotationStateResponse: { + /** Data */ + data: (components["schemas"]["TextAnnotation"] | components["schemas"]["DrawAnnotation"] | components["schemas"]["SignatureAnnotation"] | components["schemas"]["ImageAnnotation"] | components["schemas"]["HighlightAnnotation"] | components["schemas"]["ShapeAnnotation"])[]; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + }; + /** AnnotationStateUpdateRequest */ + AnnotationStateUpdateRequest: { + /** Data */ + data: (components["schemas"]["TextAnnotation"] | components["schemas"]["DrawAnnotation"] | components["schemas"]["SignatureAnnotation"] | components["schemas"]["ImageAnnotation"] | components["schemas"]["HighlightAnnotation"] | components["schemas"]["ShapeAnnotation"])[]; + /** Baseupdatedat */ + baseUpdatedAt?: string | null; + }; + /** AnnotationStateUpdateResponse */ + AnnotationStateUpdateResponse: { + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + }; + /** AuthStatusResponse */ + AuthStatusResponse: { + /** Setuprequired */ + setupRequired: boolean; + /** Loggedin */ + loggedIn: boolean; + }; + /** Body_upload_document_api_v1_documents_post */ + Body_upload_document_api_v1_documents_post: { + /** File */ + file: string; + }; + /** BulkDeleteRequest */ + BulkDeleteRequest: { + /** Ids */ + ids: string[]; + }; + /** BulkRestoreRequest */ + BulkRestoreRequest: { + /** Ids */ + ids: string[]; + }; + /** ChangePasswordRequest */ + ChangePasswordRequest: { + /** Current Password */ + current_password: string; + /** New Password */ + new_password: string; + }; + /** DocumentListResponse */ + DocumentListResponse: { + /** Items */ + items: components["schemas"]["DocumentMeta"][]; + /** Total */ + total: number; + }; + /** DocumentMeta */ + DocumentMeta: { + /** Id */ + id: string; + /** Title */ + title: string; + /** Filename */ + filename: string; + /** Mime Type */ + mime_type: string; + /** Size Bytes */ + size_bytes: number; + /** In Trash */ + in_trash: boolean; + /** Created At */ + created_at: string; + /** Updated At */ + updated_at: string; + /** Deleted At */ + deleted_at: string | null; + }; + /** DocumentUpdateRequest */ + DocumentUpdateRequest: { + /** Title */ + title?: string | null; + /** In Trash */ + in_trash?: boolean | null; + }; + /** DrawAnnotation */ + DrawAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "draw"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["DrawProps"]; + }; + /** DrawProps */ + DrawProps: { + /** Paths */ + paths: [ + number, + number + ][]; + /** + * Strokecolor + * @default #000000 + */ + strokeColor: string; + /** + * Strokewidth + * @default 2 + */ + strokeWidth: number; + /** + * Opacity + * @default 1 + */ + opacity: number; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** HighlightAnnotation */ + HighlightAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "highlight"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["HighlightProps"]; + }; + /** HighlightProps */ + HighlightProps: { + /** + * Color + * @default #FFEB3B + */ + color: string; + /** + * Opacity + * @default 0.3 + */ + opacity: number; + }; + /** ImageAnnotation */ + ImageAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "image"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["ImageProps"]; + }; + /** ImageProps */ + ImageProps: { + /** Ref */ + ref: string; + /** Naturalwidth */ + naturalWidth: number; + /** Naturalheight */ + naturalHeight: number; + }; + /** LoginRequest */ + LoginRequest: { + /** Password */ + password: string; + }; + /** Rect */ + Rect: { + /** X */ + x: number; + /** Y */ + y: number; + /** Width */ + width: number; + /** Height */ + height: number; + }; + /** SetupRequest */ + SetupRequest: { + /** Password */ + password: string; + }; + /** ShapeAnnotation */ + ShapeAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "shape"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["ShapeProps"]; + }; + /** ShapeProps */ + ShapeProps: { + /** + * Kind + * @enum {string} + */ + kind: "rect" | "ellipse" | "line" | "arrow"; + /** + * Strokecolor + * @default #000000 + */ + strokeColor: string; + /** + * Fillcolor + * @default transparent + */ + fillColor: string; + /** + * Strokewidth + * @default 2 + */ + strokeWidth: number; + }; + /** SignatureAnnotation */ + SignatureAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "signature"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + /** Props */ + props: components["schemas"]["SignatureDrawProps"] | components["schemas"]["SignatureTypeProps"]; + }; + /** SignatureDrawProps */ + SignatureDrawProps: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + mode: "draw"; + /** Ref */ + ref: string; + /** + * Strokecolor + * @default #000000 + */ + strokeColor: string; + }; + /** SignatureTypeProps */ + SignatureTypeProps: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + mode: "type"; + /** Text */ + text: string; + /** Fontfamily */ + fontFamily: string; + /** + * Color + * @default #000000 + */ + color: string; + }; + /** TextAnnotation */ + TextAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "text"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["TextProps"]; + }; + /** TextProps */ + TextProps: { + /** Text */ + text: string; + /** + * Fontfamily + * @default Liberation Sans + */ + fontFamily: string; + /** + * Fontsize + * @default 14 + */ + fontSize: number; + /** + * Color + * @default #000000 + */ + color: string; + /** + * Align + * @default left + * @enum {string} + */ + align: "left" | "center" | "right"; + /** + * Bold + * @default false + */ + bold: boolean; + /** + * Italic + * @default false + */ + italic: boolean; + /** + * Lineheight + * @default 1.2 + */ + lineHeight: number; + /** Highlightcolor */ + highlightColor?: string | null; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + /** Input */ + input?: unknown; + /** Context */ + ctx?: Record; + }; + /** VerifyCoordsRequest */ + VerifyCoordsRequest: { + /** Document Id */ + document_id: string; + /** Page */ + page: number; + /** X */ + x: number; + /** Y */ + y: number; + /** Width */ + width: number; + /** Height */ + height: number; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + get_auth_status_api_v1_auth_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthStatusResponse"]; + }; + }; + }; + }; + setup_password_api_v1_auth_setup_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetupRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + login_api_v1_auth_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + logout_api_v1_auth_logout_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + }; + }; + change_password_api_v1_auth_password_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ChangePasswordRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_documents_api_v1_documents_get: { + parameters: { + query?: { + query?: string; + skip?: number; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + upload_document_api_v1_documents_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["Body_upload_document_api_v1_documents_post"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentMeta"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_trash_api_v1_documents_trash_get: { + parameters: { + query?: { + skip?: number; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_document_api_v1_documents__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentMeta"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_document_api_v1_documents__id__delete: { + parameters: { + query?: { + permanent?: boolean; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_document_api_v1_documents__id__patch: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DocumentUpdateRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentMeta"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + restore_document_api_v1_documents__id__restore_post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentMeta"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + bulk_delete_api_v1_documents_bulk_delete_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BulkDeleteRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + bulk_restore_api_v1_documents_bulk_restore_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BulkRestoreRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + empty_trash_api_v1_documents_trash_empty_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + }; + }; + get_document_file_api_v1_documents__id__file_get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_document_thumbnail_api_v1_documents__id__thumbnail_get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_annotations_api_v1_documents__document_id__annotations_get: { + parameters: { + query?: never; + header?: never; + path: { + document_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnnotationStateResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_annotations_api_v1_documents__document_id__annotations_put: { + parameters: { + query?: never; + header?: never; + path: { + document_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AnnotationStateUpdateRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnnotationStateUpdateResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + health_check_api_v1_health_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + }; + }; + verify_coords_api_v1_debug_verify_coords_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["VerifyCoordsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 0000000..630a6de --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,1499 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/v1/auth/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Auth Status + * @description Check if app requires setup and if user is logged in. + */ + get: operations["get_auth_status_api_v1_auth_status_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/auth/setup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Setup Password + * @description First-run setup. + */ + post: operations["setup_password_api_v1_auth_setup_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Login + * @description Validate password and issue session cookie. + */ + post: operations["login_api_v1_auth_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Logout + * @description Clear session cookie. + */ + post: operations["logout_api_v1_auth_logout_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/auth/password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Change Password + * @description Change an existing password. + */ + put: operations["change_password_api_v1_auth_password_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Documents + * @description List non-trashed documents. + */ + get: operations["list_documents_api_v1_documents_get"]; + put?: never; + /** + * Upload Document + * @description Upload a new PDF document. + */ + post: operations["upload_document_api_v1_documents_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/trash": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Trash + * @description List trashed documents. + */ + get: operations["list_trash_api_v1_documents_trash_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Document */ + get: operations["get_document_api_v1_documents__id__get"]; + put?: never; + post?: never; + /** + * Delete Document + * @description Soft delete by default. Hard delete if permanent=True AND already in trash. + */ + delete: operations["delete_document_api_v1_documents__id__delete"]; + options?: never; + head?: never; + /** Update Document */ + patch: operations["update_document_api_v1_documents__id__patch"]; + trace?: never; + }; + "/api/v1/documents/{id}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Restore Document */ + post: operations["restore_document_api_v1_documents__id__restore_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/bulk-delete": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Bulk Delete */ + post: operations["bulk_delete_api_v1_documents_bulk_delete_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/bulk-restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Bulk Restore */ + post: operations["bulk_restore_api_v1_documents_bulk_restore_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/trash/empty": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Empty Trash */ + post: operations["empty_trash_api_v1_documents_trash_empty_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/{id}/file": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Document File */ + get: operations["get_document_file_api_v1_documents__id__file_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/{id}/thumbnail": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Document Thumbnail */ + get: operations["get_document_thumbnail_api_v1_documents__id__thumbnail_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/documents/{document_id}/annotations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Annotations */ + get: operations["get_annotations_api_v1_documents__document_id__annotations_get"]; + /** Update Annotations */ + put: operations["update_annotations_api_v1_documents__document_id__annotations_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health Check + * @description Return service health status and version. + */ + get: operations["health_check_api_v1_health_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/debug/verify-coords": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Verify Coords + * @description Test endpoint for cross-engine coordinate verification. + * Takes a canonical rect, draws it on the PDF using PyMuPDF, + * and returns the flattened PDF. + */ + post: operations["verify_coords_api_v1_debug_verify_coords_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** AnnotationStateResponse */ + AnnotationStateResponse: { + /** Data */ + data: (components["schemas"]["TextAnnotation"] | components["schemas"]["DrawAnnotation"] | components["schemas"]["SignatureAnnotation"] | components["schemas"]["ImageAnnotation"] | components["schemas"]["HighlightAnnotation"] | components["schemas"]["ShapeAnnotation"])[]; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + }; + /** AnnotationStateUpdateRequest */ + AnnotationStateUpdateRequest: { + /** Data */ + data: (components["schemas"]["TextAnnotation"] | components["schemas"]["DrawAnnotation"] | components["schemas"]["SignatureAnnotation"] | components["schemas"]["ImageAnnotation"] | components["schemas"]["HighlightAnnotation"] | components["schemas"]["ShapeAnnotation"])[]; + /** Baseupdatedat */ + baseUpdatedAt?: string | null; + }; + /** AnnotationStateUpdateResponse */ + AnnotationStateUpdateResponse: { + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + }; + /** AuthStatusResponse */ + AuthStatusResponse: { + /** Setuprequired */ + setupRequired: boolean; + /** Loggedin */ + loggedIn: boolean; + }; + /** Body_upload_document_api_v1_documents_post */ + Body_upload_document_api_v1_documents_post: { + /** File */ + file: string; + }; + /** BulkDeleteRequest */ + BulkDeleteRequest: { + /** Ids */ + ids: string[]; + }; + /** BulkRestoreRequest */ + BulkRestoreRequest: { + /** Ids */ + ids: string[]; + }; + /** ChangePasswordRequest */ + ChangePasswordRequest: { + /** Current Password */ + current_password: string; + /** New Password */ + new_password: string; + }; + /** DocumentListResponse */ + DocumentListResponse: { + /** Items */ + items: components["schemas"]["DocumentMeta"][]; + /** Total */ + total: number; + }; + /** DocumentMeta */ + DocumentMeta: { + /** Id */ + id: string; + /** Title */ + title: string; + /** Filename */ + filename: string; + /** Mime Type */ + mime_type: string; + /** Size Bytes */ + size_bytes: number; + /** In Trash */ + in_trash: boolean; + /** Created At */ + created_at: string; + /** Updated At */ + updated_at: string; + /** Deleted At */ + deleted_at: string | null; + }; + /** DocumentUpdateRequest */ + DocumentUpdateRequest: { + /** Title */ + title?: string | null; + /** In Trash */ + in_trash?: boolean | null; + }; + /** DrawAnnotation */ + DrawAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "draw"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["DrawProps"]; + }; + /** DrawProps */ + DrawProps: { + /** Paths */ + paths: [ + number, + number + ][]; + /** + * Strokecolor + * @default #000000 + */ + strokeColor: string; + /** + * Strokewidth + * @default 2 + */ + strokeWidth: number; + /** + * Opacity + * @default 1 + */ + opacity: number; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** HighlightAnnotation */ + HighlightAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "highlight"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["HighlightProps"]; + }; + /** HighlightProps */ + HighlightProps: { + /** + * Color + * @default #FFEB3B + */ + color: string; + /** + * Opacity + * @default 0.3 + */ + opacity: number; + }; + /** ImageAnnotation */ + ImageAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "image"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["ImageProps"]; + }; + /** ImageProps */ + ImageProps: { + /** Ref */ + ref: string; + /** Naturalwidth */ + naturalWidth: number; + /** Naturalheight */ + naturalHeight: number; + }; + /** LoginRequest */ + LoginRequest: { + /** Password */ + password: string; + }; + /** Rect */ + Rect: { + /** X */ + x: number; + /** Y */ + y: number; + /** Width */ + width: number; + /** Height */ + height: number; + }; + /** SetupRequest */ + SetupRequest: { + /** Password */ + password: string; + }; + /** ShapeAnnotation */ + ShapeAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "shape"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["ShapeProps"]; + }; + /** ShapeProps */ + ShapeProps: { + /** + * Kind + * @enum {string} + */ + kind: "rect" | "ellipse" | "line" | "arrow"; + /** + * Strokecolor + * @default #000000 + */ + strokeColor: string; + /** + * Fillcolor + * @default transparent + */ + fillColor: string; + /** + * Strokewidth + * @default 2 + */ + strokeWidth: number; + }; + /** SignatureAnnotation */ + SignatureAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "signature"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + /** Props */ + props: components["schemas"]["SignatureDrawProps"] | components["schemas"]["SignatureTypeProps"]; + }; + /** SignatureDrawProps */ + SignatureDrawProps: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + mode: "draw"; + /** Ref */ + ref: string; + /** + * Strokecolor + * @default #000000 + */ + strokeColor: string; + }; + /** SignatureTypeProps */ + SignatureTypeProps: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + mode: "type"; + /** Text */ + text: string; + /** Fontfamily */ + fontFamily: string; + /** + * Color + * @default #000000 + */ + color: string; + }; + /** TextAnnotation */ + TextAnnotation: { + /** + * Id + * Format: uuid + */ + id: string; + /** Page */ + page: number; + /** + * Type + * @constant + */ + type: "text"; + rect: components["schemas"]["Rect"]; + /** + * Rotation + * @default 0 + */ + rotation: number; + /** + * Z + * @default 0 + */ + z: number; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** + * Updatedat + * Format: date-time + */ + updatedAt: string; + props: components["schemas"]["TextProps"]; + }; + /** TextProps */ + TextProps: { + /** Text */ + text: string; + /** + * Fontfamily + * @default Liberation Sans + */ + fontFamily: string; + /** + * Fontsize + * @default 14 + */ + fontSize: number; + /** + * Color + * @default #000000 + */ + color: string; + /** + * Align + * @default left + * @enum {string} + */ + align: "left" | "center" | "right"; + /** + * Bold + * @default false + */ + bold: boolean; + /** + * Italic + * @default false + */ + italic: boolean; + /** + * Lineheight + * @default 1.2 + */ + lineHeight: number; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + /** Input */ + input?: unknown; + /** Context */ + ctx?: Record; + }; + /** VerifyCoordsRequest */ + VerifyCoordsRequest: { + /** Document Id */ + document_id: string; + /** Page */ + page: number; + /** X */ + x: number; + /** Y */ + y: number; + /** Width */ + width: number; + /** Height */ + height: number; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + get_auth_status_api_v1_auth_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthStatusResponse"]; + }; + }; + }; + }; + setup_password_api_v1_auth_setup_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetupRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + login_api_v1_auth_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + logout_api_v1_auth_logout_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + }; + }; + change_password_api_v1_auth_password_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ChangePasswordRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_documents_api_v1_documents_get: { + parameters: { + query?: { + query?: string; + skip?: number; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + upload_document_api_v1_documents_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["Body_upload_document_api_v1_documents_post"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentMeta"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_trash_api_v1_documents_trash_get: { + parameters: { + query?: { + skip?: number; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_document_api_v1_documents__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentMeta"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_document_api_v1_documents__id__delete: { + parameters: { + query?: { + permanent?: boolean; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_document_api_v1_documents__id__patch: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DocumentUpdateRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentMeta"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + restore_document_api_v1_documents__id__restore_post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentMeta"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + bulk_delete_api_v1_documents_bulk_delete_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BulkDeleteRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + bulk_restore_api_v1_documents_bulk_restore_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BulkRestoreRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + empty_trash_api_v1_documents_trash_empty_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + }; + }; + get_document_file_api_v1_documents__id__file_get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_document_thumbnail_api_v1_documents__id__thumbnail_get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_annotations_api_v1_documents__document_id__annotations_get: { + parameters: { + query?: never; + header?: never; + path: { + document_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnnotationStateResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_annotations_api_v1_documents__document_id__annotations_put: { + parameters: { + query?: never; + header?: never; + path: { + document_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AnnotationStateUpdateRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnnotationStateUpdateResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + health_check_api_v1_health_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + }; + }; + verify_coords_api_v1_debug_verify_coords_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["VerifyCoordsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 6dd34cf..9a07158 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -23,11 +23,9 @@ "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "ignoreDeprecations": "6.0", - "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": ["./src/*"] } }, "include": ["src"] diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..62b6e82 --- /dev/null +++ b/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"PaperJet","description":"Self-hosted PDF editor API","version":"0.1.0"},"paths":{"/api/v1/auth/status":{"get":{"tags":["auth"],"summary":"Get Auth Status","description":"Check if app requires setup and if user is logged in.","operationId":"get_auth_status_api_v1_auth_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthStatusResponse"}}}}}}},"/api/v1/auth/setup":{"post":{"tags":["auth"],"summary":"Setup Password","description":"First-run setup.","operationId":"setup_password_api_v1_auth_setup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Setup Password Api V1 Auth Setup Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/login":{"post":{"tags":["auth"],"summary":"Login","description":"Validate password and issue session cookie.","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Login Api V1 Auth Login Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","description":"Clear session cookie.","operationId":"logout_api_v1_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Logout Api V1 Auth Logout Post"}}}}}}},"/api/v1/auth/password":{"put":{"tags":["auth"],"summary":"Change Password","description":"Change an existing password.","operationId":"change_password_api_v1_auth_password_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Change Password Api V1 Auth Password Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents":{"get":{"tags":["documents"],"summary":"List Documents","description":"List non-trashed documents.","operationId":"list_documents_api_v1_documents_get","parameters":[{"name":"query","in":"query","required":false,"schema":{"type":"string","default":"","title":"Query"}},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Skip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["documents"],"summary":"Upload Document","description":"Upload a new PDF document.","operationId":"upload_document_api_v1_documents_post","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_document_api_v1_documents_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMeta"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/trash":{"get":{"tags":["documents"],"summary":"List Trash","description":"List trashed documents.","operationId":"list_trash_api_v1_documents_trash_get","parameters":[{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Skip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/{id}":{"get":{"tags":["documents"],"summary":"Get Document","operationId":"get_document_api_v1_documents__id__get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMeta"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["documents"],"summary":"Update Document","operationId":"update_document_api_v1_documents__id__patch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMeta"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["documents"],"summary":"Delete Document","description":"Soft delete by default. Hard delete if permanent=True AND already in trash.","operationId":"delete_document_api_v1_documents__id__delete","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}},{"name":"permanent","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Permanent"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/{id}/restore":{"post":{"tags":["documents"],"summary":"Restore Document","operationId":"restore_document_api_v1_documents__id__restore_post","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMeta"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/bulk-delete":{"post":{"tags":["documents"],"summary":"Bulk Delete","operationId":"bulk_delete_api_v1_documents_bulk_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/bulk-restore":{"post":{"tags":["documents"],"summary":"Bulk Restore","operationId":"bulk_restore_api_v1_documents_bulk_restore_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRestoreRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/trash/empty":{"post":{"tags":["documents"],"summary":"Empty Trash","operationId":"empty_trash_api_v1_documents_trash_empty_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/documents/{id}/file":{"get":{"tags":["documents"],"summary":"Get Document File","operationId":"get_document_file_api_v1_documents__id__file_get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/{id}/thumbnail":{"get":{"tags":["documents"],"summary":"Get Document Thumbnail","operationId":"get_document_thumbnail_api_v1_documents__id__thumbnail_get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/documents/{document_id}/annotations":{"get":{"tags":["Annotations"],"summary":"Get Annotations","operationId":"get_annotations_api_v1_documents__document_id__annotations_get","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationStateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Annotations"],"summary":"Update Annotations","operationId":"update_annotations_api_v1_documents__document_id__annotations_put","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationStateUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationStateUpdateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/health":{"get":{"tags":["health"],"summary":"Health Check","description":"Return service health status and version.","operationId":"health_check_api_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Check Api V1 Health Get"}}}}}}},"/api/v1/debug/verify-coords":{"post":{"tags":["Debug"],"summary":"Verify Coords","description":"Test endpoint for cross-engine coordinate verification.\nTakes a canonical rect, draws it on the PDF using PyMuPDF,\nand returns the flattened PDF.","operationId":"verify_coords_api_v1_debug_verify_coords_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyCoordsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AnnotationStateResponse":{"properties":{"data":{"items":{"anyOf":[{"$ref":"#/components/schemas/TextAnnotation"},{"$ref":"#/components/schemas/DrawAnnotation"},{"$ref":"#/components/schemas/SignatureAnnotation"},{"$ref":"#/components/schemas/ImageAnnotation"},{"$ref":"#/components/schemas/HighlightAnnotation"},{"$ref":"#/components/schemas/ShapeAnnotation"}]},"type":"array","title":"Data"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"}},"type":"object","required":["data","updatedAt"],"title":"AnnotationStateResponse"},"AnnotationStateUpdateRequest":{"properties":{"data":{"items":{"anyOf":[{"$ref":"#/components/schemas/TextAnnotation"},{"$ref":"#/components/schemas/DrawAnnotation"},{"$ref":"#/components/schemas/SignatureAnnotation"},{"$ref":"#/components/schemas/ImageAnnotation"},{"$ref":"#/components/schemas/HighlightAnnotation"},{"$ref":"#/components/schemas/ShapeAnnotation"}]},"type":"array","title":"Data"},"baseUpdatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Baseupdatedat"}},"type":"object","required":["data"],"title":"AnnotationStateUpdateRequest"},"AnnotationStateUpdateResponse":{"properties":{"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"}},"type":"object","required":["updatedAt"],"title":"AnnotationStateUpdateResponse"},"AuthStatusResponse":{"properties":{"setupRequired":{"type":"boolean","title":"Setuprequired"},"loggedIn":{"type":"boolean","title":"Loggedin"}},"type":"object","required":["setupRequired","loggedIn"],"title":"AuthStatusResponse"},"Body_upload_document_api_v1_documents_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_document_api_v1_documents_post"},"BulkDeleteRequest":{"properties":{"ids":{"items":{"type":"string"},"type":"array","title":"Ids"}},"type":"object","required":["ids"],"title":"BulkDeleteRequest"},"BulkRestoreRequest":{"properties":{"ids":{"items":{"type":"string"},"type":"array","title":"Ids"}},"type":"object","required":["ids"],"title":"BulkRestoreRequest"},"ChangePasswordRequest":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","minLength":8,"title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"ChangePasswordRequest"},"DocumentListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/DocumentMeta"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["items","total"],"title":"DocumentListResponse"},"DocumentMeta":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"filename":{"type":"string","title":"Filename"},"mime_type":{"type":"string","title":"Mime Type"},"size_bytes":{"type":"integer","title":"Size Bytes"},"in_trash":{"type":"boolean","title":"In Trash"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deleted At"}},"type":"object","required":["id","title","filename","mime_type","size_bytes","in_trash","created_at","updated_at","deleted_at"],"title":"DocumentMeta"},"DocumentUpdateRequest":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"in_trash":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"In Trash"}},"type":"object","title":"DocumentUpdateRequest"},"DrawAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"draw","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/DrawProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"DrawAnnotation"},"DrawProps":{"properties":{"paths":{"items":{"prefixItems":[{"type":"number"},{"type":"number"}],"type":"array","maxItems":2,"minItems":2},"type":"array","title":"Paths"},"strokeColor":{"type":"string","title":"Strokecolor","default":"#000000"},"strokeWidth":{"type":"number","title":"Strokewidth","default":2},"opacity":{"type":"number","title":"Opacity","default":1.0}},"type":"object","required":["paths"],"title":"DrawProps"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HighlightAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"highlight","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/HighlightProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"HighlightAnnotation"},"HighlightProps":{"properties":{"color":{"type":"string","title":"Color","default":"#FFEB3B"},"opacity":{"type":"number","title":"Opacity","default":0.3}},"type":"object","title":"HighlightProps"},"ImageAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"image","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/ImageProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"ImageAnnotation"},"ImageProps":{"properties":{"ref":{"type":"string","title":"Ref"},"naturalWidth":{"type":"number","title":"Naturalwidth"},"naturalHeight":{"type":"number","title":"Naturalheight"}},"type":"object","required":["ref","naturalWidth","naturalHeight"],"title":"ImageProps"},"LoginRequest":{"properties":{"password":{"type":"string","title":"Password"}},"type":"object","required":["password"],"title":"LoginRequest"},"Rect":{"properties":{"x":{"type":"number","title":"X"},"y":{"type":"number","title":"Y"},"width":{"type":"number","title":"Width"},"height":{"type":"number","title":"Height"}},"type":"object","required":["x","y","width","height"],"title":"Rect"},"SetupRequest":{"properties":{"password":{"type":"string","minLength":8,"title":"Password"}},"type":"object","required":["password"],"title":"SetupRequest"},"ShapeAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"shape","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/ShapeProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"ShapeAnnotation"},"ShapeProps":{"properties":{"kind":{"type":"string","enum":["rect","ellipse","line","arrow"],"title":"Kind"},"strokeColor":{"type":"string","title":"Strokecolor","default":"#000000"},"fillColor":{"type":"string","title":"Fillcolor","default":"transparent"},"strokeWidth":{"type":"number","title":"Strokewidth","default":2}},"type":"object","required":["kind"],"title":"ShapeProps"},"SignatureAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"signature","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"oneOf":[{"$ref":"#/components/schemas/SignatureDrawProps"},{"$ref":"#/components/schemas/SignatureTypeProps"}],"title":"Props","discriminator":{"propertyName":"mode","mapping":{"draw":"#/components/schemas/SignatureDrawProps","type":"#/components/schemas/SignatureTypeProps"}}}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"SignatureAnnotation"},"SignatureDrawProps":{"properties":{"mode":{"type":"string","const":"draw","title":"Mode"},"ref":{"type":"string","title":"Ref"},"strokeColor":{"type":"string","title":"Strokecolor","default":"#000000"}},"type":"object","required":["mode","ref"],"title":"SignatureDrawProps"},"SignatureTypeProps":{"properties":{"mode":{"type":"string","const":"type","title":"Mode"},"text":{"type":"string","title":"Text"},"fontFamily":{"type":"string","title":"Fontfamily"},"color":{"type":"string","title":"Color","default":"#000000"}},"type":"object","required":["mode","text","fontFamily"],"title":"SignatureTypeProps"},"TextAnnotation":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"page":{"type":"integer","minimum":0.0,"title":"Page"},"type":{"type":"string","const":"text","title":"Type"},"rect":{"$ref":"#/components/schemas/Rect"},"rotation":{"type":"number","title":"Rotation","default":0.0},"z":{"type":"integer","title":"Z","default":0},"createdAt":{"type":"string","format":"date-time","title":"Createdat"},"updatedAt":{"type":"string","format":"date-time","title":"Updatedat"},"props":{"$ref":"#/components/schemas/TextProps"}},"type":"object","required":["id","page","type","rect","createdAt","updatedAt","props"],"title":"TextAnnotation"},"TextProps":{"properties":{"text":{"type":"string","title":"Text"},"fontFamily":{"type":"string","title":"Fontfamily","default":"Liberation Sans"},"fontSize":{"type":"number","title":"Fontsize","default":14},"color":{"type":"string","title":"Color","default":"#000000"},"align":{"type":"string","enum":["left","center","right"],"title":"Align","default":"left"},"bold":{"type":"boolean","title":"Bold","default":false},"italic":{"type":"boolean","title":"Italic","default":false},"lineHeight":{"type":"number","title":"Lineheight","default":1.2}},"type":"object","required":["text"],"title":"TextProps"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyCoordsRequest":{"properties":{"document_id":{"type":"string","title":"Document Id"},"page":{"type":"integer","title":"Page"},"x":{"type":"number","title":"X"},"y":{"type":"number","title":"Y"},"width":{"type":"number","title":"Width"},"height":{"type":"number","title":"Height"}},"type":"object","required":["document_id","page","x","y","width","height"],"title":"VerifyCoordsRequest"}}}} \ No newline at end of file diff --git a/openapi_pretty.json b/openapi_pretty.json new file mode 100644 index 0000000..896226d --- /dev/null +++ b/openapi_pretty.json @@ -0,0 +1,1893 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "PaperJet", + "description": "Self-hosted PDF editor API", + "version": "0.1.0" + }, + "paths": { + "/api/v1/auth/status": { + "get": { + "tags": [ + "auth" + ], + "summary": "Get Auth Status", + "description": "Check if app requires setup and if user is logged in.", + "operationId": "get_auth_status_api_v1_auth_status_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthStatusResponse" + } + } + } + } + } + } + }, + "/api/v1/auth/setup": { + "post": { + "tags": [ + "auth" + ], + "summary": "Setup Password", + "description": "First-run setup.", + "operationId": "setup_password_api_v1_auth_setup_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetupRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Setup Password Api V1 Auth Setup Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "tags": [ + "auth" + ], + "summary": "Login", + "description": "Validate password and issue session cookie.", + "operationId": "login_api_v1_auth_login_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Login Api V1 Auth Login Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/logout": { + "post": { + "tags": [ + "auth" + ], + "summary": "Logout", + "description": "Clear session cookie.", + "operationId": "logout_api_v1_auth_logout_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Logout Api V1 Auth Logout Post" + } + } + } + } + } + } + }, + "/api/v1/auth/password": { + "put": { + "tags": [ + "auth" + ], + "summary": "Change Password", + "description": "Change an existing password.", + "operationId": "change_password_api_v1_auth_password_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangePasswordRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Change Password Api V1 Auth Password Put" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents": { + "get": { + "tags": [ + "documents" + ], + "summary": "List Documents", + "description": "List non-trashed documents.", + "operationId": "list_documents_api_v1_documents_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "", + "title": "Query" + } + }, + { + "name": "skip", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Skip" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 50, + "title": "Limit" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentListResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "documents" + ], + "summary": "Upload Document", + "description": "Upload a new PDF document.", + "operationId": "upload_document_api_v1_documents_post", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_document_api_v1_documents_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentMeta" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents/trash": { + "get": { + "tags": [ + "documents" + ], + "summary": "List Trash", + "description": "List trashed documents.", + "operationId": "list_trash_api_v1_documents_trash_get", + "parameters": [ + { + "name": "skip", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Skip" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 50, + "title": "Limit" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentListResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents/{id}": { + "get": { + "tags": [ + "documents" + ], + "summary": "Get Document", + "operationId": "get_document_api_v1_documents__id__get", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentMeta" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "documents" + ], + "summary": "Update Document", + "operationId": "update_document_api_v1_documents__id__patch", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentMeta" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "documents" + ], + "summary": "Delete Document", + "description": "Soft delete by default. Hard delete if permanent=True AND already in trash.", + "operationId": "delete_document_api_v1_documents__id__delete", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Id" + } + }, + { + "name": "permanent", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Permanent" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents/{id}/restore": { + "post": { + "tags": [ + "documents" + ], + "summary": "Restore Document", + "operationId": "restore_document_api_v1_documents__id__restore_post", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentMeta" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents/bulk-delete": { + "post": { + "tags": [ + "documents" + ], + "summary": "Bulk Delete", + "operationId": "bulk_delete_api_v1_documents_bulk_delete_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkDeleteRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents/bulk-restore": { + "post": { + "tags": [ + "documents" + ], + "summary": "Bulk Restore", + "operationId": "bulk_restore_api_v1_documents_bulk_restore_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkRestoreRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents/trash/empty": { + "post": { + "tags": [ + "documents" + ], + "summary": "Empty Trash", + "operationId": "empty_trash_api_v1_documents_trash_empty_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/documents/{id}/file": { + "get": { + "tags": [ + "documents" + ], + "summary": "Get Document File", + "operationId": "get_document_file_api_v1_documents__id__file_get", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents/{id}/thumbnail": { + "get": { + "tags": [ + "documents" + ], + "summary": "Get Document Thumbnail", + "operationId": "get_document_thumbnail_api_v1_documents__id__thumbnail_get", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/documents/{document_id}/annotations": { + "get": { + "tags": [ + "Annotations" + ], + "summary": "Get Annotations", + "operationId": "get_annotations_api_v1_documents__document_id__annotations_get", + "parameters": [ + { + "name": "document_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Document Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationStateResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "put": { + "tags": [ + "Annotations" + ], + "summary": "Update Annotations", + "operationId": "update_annotations_api_v1_documents__document_id__annotations_put", + "parameters": [ + { + "name": "document_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Document Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationStateUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationStateUpdateResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/health": { + "get": { + "tags": [ + "health" + ], + "summary": "Health Check", + "description": "Return service health status and version.", + "operationId": "health_check_api_v1_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Health Check Api V1 Health Get" + } + } + } + } + } + } + }, + "/api/v1/debug/verify-coords": { + "post": { + "tags": [ + "Debug" + ], + "summary": "Verify Coords", + "description": "Test endpoint for cross-engine coordinate verification.\nTakes a canonical rect, draws it on the PDF using PyMuPDF,\nand returns the flattened PDF.", + "operationId": "verify_coords_api_v1_debug_verify_coords_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCoordsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AnnotationStateResponse": { + "properties": { + "data": { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextAnnotation" + }, + { + "$ref": "#/components/schemas/DrawAnnotation" + }, + { + "$ref": "#/components/schemas/SignatureAnnotation" + }, + { + "$ref": "#/components/schemas/ImageAnnotation" + }, + { + "$ref": "#/components/schemas/HighlightAnnotation" + }, + { + "$ref": "#/components/schemas/ShapeAnnotation" + } + ] + }, + "type": "array", + "title": "Data" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + } + }, + "type": "object", + "required": [ + "data", + "updatedAt" + ], + "title": "AnnotationStateResponse" + }, + "AnnotationStateUpdateRequest": { + "properties": { + "data": { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextAnnotation" + }, + { + "$ref": "#/components/schemas/DrawAnnotation" + }, + { + "$ref": "#/components/schemas/SignatureAnnotation" + }, + { + "$ref": "#/components/schemas/ImageAnnotation" + }, + { + "$ref": "#/components/schemas/HighlightAnnotation" + }, + { + "$ref": "#/components/schemas/ShapeAnnotation" + } + ] + }, + "type": "array", + "title": "Data" + }, + "baseUpdatedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Baseupdatedat" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "AnnotationStateUpdateRequest" + }, + "AnnotationStateUpdateResponse": { + "properties": { + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + } + }, + "type": "object", + "required": [ + "updatedAt" + ], + "title": "AnnotationStateUpdateResponse" + }, + "AuthStatusResponse": { + "properties": { + "setupRequired": { + "type": "boolean", + "title": "Setuprequired" + }, + "loggedIn": { + "type": "boolean", + "title": "Loggedin" + } + }, + "type": "object", + "required": [ + "setupRequired", + "loggedIn" + ], + "title": "AuthStatusResponse" + }, + "Body_upload_document_api_v1_documents_post": { + "properties": { + "file": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "File" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_upload_document_api_v1_documents_post" + }, + "BulkDeleteRequest": { + "properties": { + "ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Ids" + } + }, + "type": "object", + "required": [ + "ids" + ], + "title": "BulkDeleteRequest" + }, + "BulkRestoreRequest": { + "properties": { + "ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Ids" + } + }, + "type": "object", + "required": [ + "ids" + ], + "title": "BulkRestoreRequest" + }, + "ChangePasswordRequest": { + "properties": { + "current_password": { + "type": "string", + "title": "Current Password" + }, + "new_password": { + "type": "string", + "minLength": 8, + "title": "New Password" + } + }, + "type": "object", + "required": [ + "current_password", + "new_password" + ], + "title": "ChangePasswordRequest" + }, + "DocumentListResponse": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/DocumentMeta" + }, + "type": "array", + "title": "Items" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "items", + "total" + ], + "title": "DocumentListResponse" + }, + "DocumentMeta": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "filename": { + "type": "string", + "title": "Filename" + }, + "mime_type": { + "type": "string", + "title": "Mime Type" + }, + "size_bytes": { + "type": "integer", + "title": "Size Bytes" + }, + "in_trash": { + "type": "boolean", + "title": "In Trash" + }, + "created_at": { + "type": "string", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + }, + "deleted_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deleted At" + } + }, + "type": "object", + "required": [ + "id", + "title", + "filename", + "mime_type", + "size_bytes", + "in_trash", + "created_at", + "updated_at", + "deleted_at" + ], + "title": "DocumentMeta" + }, + "DocumentUpdateRequest": { + "properties": { + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "in_trash": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "In Trash" + } + }, + "type": "object", + "title": "DocumentUpdateRequest" + }, + "DrawAnnotation": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "page": { + "type": "integer", + "minimum": 0.0, + "title": "Page" + }, + "type": { + "type": "string", + "const": "draw", + "title": "Type" + }, + "rect": { + "$ref": "#/components/schemas/Rect" + }, + "rotation": { + "type": "number", + "title": "Rotation", + "default": 0.0 + }, + "z": { + "type": "integer", + "title": "Z", + "default": 0 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + }, + "props": { + "$ref": "#/components/schemas/DrawProps" + } + }, + "type": "object", + "required": [ + "id", + "page", + "type", + "rect", + "createdAt", + "updatedAt", + "props" + ], + "title": "DrawAnnotation" + }, + "DrawProps": { + "properties": { + "paths": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Paths" + }, + "strokeColor": { + "type": "string", + "title": "Strokecolor", + "default": "#000000" + }, + "strokeWidth": { + "type": "number", + "title": "Strokewidth", + "default": 2 + }, + "opacity": { + "type": "number", + "title": "Opacity", + "default": 1.0 + } + }, + "type": "object", + "required": [ + "paths" + ], + "title": "DrawProps" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HighlightAnnotation": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "page": { + "type": "integer", + "minimum": 0.0, + "title": "Page" + }, + "type": { + "type": "string", + "const": "highlight", + "title": "Type" + }, + "rect": { + "$ref": "#/components/schemas/Rect" + }, + "rotation": { + "type": "number", + "title": "Rotation", + "default": 0.0 + }, + "z": { + "type": "integer", + "title": "Z", + "default": 0 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + }, + "props": { + "$ref": "#/components/schemas/HighlightProps" + } + }, + "type": "object", + "required": [ + "id", + "page", + "type", + "rect", + "createdAt", + "updatedAt", + "props" + ], + "title": "HighlightAnnotation" + }, + "HighlightProps": { + "properties": { + "color": { + "type": "string", + "title": "Color", + "default": "#FFEB3B" + }, + "opacity": { + "type": "number", + "title": "Opacity", + "default": 0.3 + } + }, + "type": "object", + "title": "HighlightProps" + }, + "ImageAnnotation": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "page": { + "type": "integer", + "minimum": 0.0, + "title": "Page" + }, + "type": { + "type": "string", + "const": "image", + "title": "Type" + }, + "rect": { + "$ref": "#/components/schemas/Rect" + }, + "rotation": { + "type": "number", + "title": "Rotation", + "default": 0.0 + }, + "z": { + "type": "integer", + "title": "Z", + "default": 0 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + }, + "props": { + "$ref": "#/components/schemas/ImageProps" + } + }, + "type": "object", + "required": [ + "id", + "page", + "type", + "rect", + "createdAt", + "updatedAt", + "props" + ], + "title": "ImageAnnotation" + }, + "ImageProps": { + "properties": { + "ref": { + "type": "string", + "title": "Ref" + }, + "naturalWidth": { + "type": "number", + "title": "Naturalwidth" + }, + "naturalHeight": { + "type": "number", + "title": "Naturalheight" + } + }, + "type": "object", + "required": [ + "ref", + "naturalWidth", + "naturalHeight" + ], + "title": "ImageProps" + }, + "LoginRequest": { + "properties": { + "password": { + "type": "string", + "title": "Password" + } + }, + "type": "object", + "required": [ + "password" + ], + "title": "LoginRequest" + }, + "Rect": { + "properties": { + "x": { + "type": "number", + "title": "X" + }, + "y": { + "type": "number", + "title": "Y" + }, + "width": { + "type": "number", + "title": "Width" + }, + "height": { + "type": "number", + "title": "Height" + } + }, + "type": "object", + "required": [ + "x", + "y", + "width", + "height" + ], + "title": "Rect" + }, + "SetupRequest": { + "properties": { + "password": { + "type": "string", + "minLength": 8, + "title": "Password" + } + }, + "type": "object", + "required": [ + "password" + ], + "title": "SetupRequest" + }, + "ShapeAnnotation": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "page": { + "type": "integer", + "minimum": 0.0, + "title": "Page" + }, + "type": { + "type": "string", + "const": "shape", + "title": "Type" + }, + "rect": { + "$ref": "#/components/schemas/Rect" + }, + "rotation": { + "type": "number", + "title": "Rotation", + "default": 0.0 + }, + "z": { + "type": "integer", + "title": "Z", + "default": 0 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + }, + "props": { + "$ref": "#/components/schemas/ShapeProps" + } + }, + "type": "object", + "required": [ + "id", + "page", + "type", + "rect", + "createdAt", + "updatedAt", + "props" + ], + "title": "ShapeAnnotation" + }, + "ShapeProps": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "rect", + "ellipse", + "line", + "arrow" + ], + "title": "Kind" + }, + "strokeColor": { + "type": "string", + "title": "Strokecolor", + "default": "#000000" + }, + "fillColor": { + "type": "string", + "title": "Fillcolor", + "default": "transparent" + }, + "strokeWidth": { + "type": "number", + "title": "Strokewidth", + "default": 2 + } + }, + "type": "object", + "required": [ + "kind" + ], + "title": "ShapeProps" + }, + "SignatureAnnotation": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "page": { + "type": "integer", + "minimum": 0.0, + "title": "Page" + }, + "type": { + "type": "string", + "const": "signature", + "title": "Type" + }, + "rect": { + "$ref": "#/components/schemas/Rect" + }, + "rotation": { + "type": "number", + "title": "Rotation", + "default": 0.0 + }, + "z": { + "type": "integer", + "title": "Z", + "default": 0 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + }, + "props": { + "oneOf": [ + { + "$ref": "#/components/schemas/SignatureDrawProps" + }, + { + "$ref": "#/components/schemas/SignatureTypeProps" + } + ], + "title": "Props", + "discriminator": { + "propertyName": "mode", + "mapping": { + "draw": "#/components/schemas/SignatureDrawProps", + "type": "#/components/schemas/SignatureTypeProps" + } + } + } + }, + "type": "object", + "required": [ + "id", + "page", + "type", + "rect", + "createdAt", + "updatedAt", + "props" + ], + "title": "SignatureAnnotation" + }, + "SignatureDrawProps": { + "properties": { + "mode": { + "type": "string", + "const": "draw", + "title": "Mode" + }, + "ref": { + "type": "string", + "title": "Ref" + }, + "strokeColor": { + "type": "string", + "title": "Strokecolor", + "default": "#000000" + } + }, + "type": "object", + "required": [ + "mode", + "ref" + ], + "title": "SignatureDrawProps" + }, + "SignatureTypeProps": { + "properties": { + "mode": { + "type": "string", + "const": "type", + "title": "Mode" + }, + "text": { + "type": "string", + "title": "Text" + }, + "fontFamily": { + "type": "string", + "title": "Fontfamily" + }, + "color": { + "type": "string", + "title": "Color", + "default": "#000000" + } + }, + "type": "object", + "required": [ + "mode", + "text", + "fontFamily" + ], + "title": "SignatureTypeProps" + }, + "TextAnnotation": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "page": { + "type": "integer", + "minimum": 0.0, + "title": "Page" + }, + "type": { + "type": "string", + "const": "text", + "title": "Type" + }, + "rect": { + "$ref": "#/components/schemas/Rect" + }, + "rotation": { + "type": "number", + "title": "Rotation", + "default": 0.0 + }, + "z": { + "type": "integer", + "title": "Z", + "default": 0 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + }, + "props": { + "$ref": "#/components/schemas/TextProps" + } + }, + "type": "object", + "required": [ + "id", + "page", + "type", + "rect", + "createdAt", + "updatedAt", + "props" + ], + "title": "TextAnnotation" + }, + "TextProps": { + "properties": { + "text": { + "type": "string", + "title": "Text" + }, + "fontFamily": { + "type": "string", + "title": "Fontfamily", + "default": "Liberation Sans" + }, + "fontSize": { + "type": "number", + "title": "Fontsize", + "default": 14 + }, + "color": { + "type": "string", + "title": "Color", + "default": "#000000" + }, + "align": { + "type": "string", + "enum": [ + "left", + "center", + "right" + ], + "title": "Align", + "default": "left" + }, + "bold": { + "type": "boolean", + "title": "Bold", + "default": false + }, + "italic": { + "type": "boolean", + "title": "Italic", + "default": false + }, + "lineHeight": { + "type": "number", + "title": "Lineheight", + "default": 1.2 + } + }, + "type": "object", + "required": [ + "text" + ], + "title": "TextProps" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "VerifyCoordsRequest": { + "properties": { + "document_id": { + "type": "string", + "title": "Document Id" + }, + "page": { + "type": "integer", + "title": "Page" + }, + "x": { + "type": "number", + "title": "X" + }, + "y": { + "type": "number", + "title": "Y" + }, + "width": { + "type": "number", + "title": "Width" + }, + "height": { + "type": "number", + "title": "Height" + } + }, + "type": "object", + "required": [ + "document_id", + "page", + "x", + "y", + "width", + "height" + ], + "title": "VerifyCoordsRequest" + } + } + } +} diff --git a/shared/annotation-schema.json b/shared/annotation-schema.json index 11023b5..accf8c2 100644 --- a/shared/annotation-schema.json +++ b/shared/annotation-schema.json @@ -39,7 +39,8 @@ "align": { "type": "string", "enum": ["left", "center", "right"], "default": "left" }, "bold": { "type": "boolean", "default": false }, "italic": { "type": "boolean", "default": false }, - "lineHeight": { "type": "number", "default": 1.2 } + "lineHeight": { "type": "number", "default": 1.2 }, + "highlightColor": { "type": ["string", "null"] } }, "required": ["text"] },