Phase 3 implementation. Coordinate system implemented. Initial text box tool implemented.
This commit is contained in:
parent
c159ad4f37
commit
4cb038ec78
34 changed files with 6676 additions and 130 deletions
|
|
@ -14,5 +14,14 @@ router.include_router(auth_router)
|
||||||
# Documents
|
# Documents
|
||||||
router.include_router(documents_router)
|
router.include_router(documents_router)
|
||||||
|
|
||||||
|
# Annotations
|
||||||
|
from app.api.v1.annotations import router as annotations_router
|
||||||
|
router.include_router(annotations_router)
|
||||||
|
|
||||||
# Health (unauthenticated)
|
# Health (unauthenticated)
|
||||||
router.include_router(health_router)
|
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)
|
||||||
|
|
|
||||||
91
backend/app/api/v1/annotations.py
Normal file
91
backend/app/api/v1/annotations.py
Normal file
|
|
@ -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))
|
||||||
56
backend/app/api/v1/debug.py
Normal file
56
backend/app/api/v1/debug.py
Normal file
|
|
@ -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")
|
||||||
107
backend/app/schemas/annotations.py
Normal file
107
backend/app/schemas/annotations.py
Normal file
|
|
@ -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
|
||||||
|
|
@ -10,6 +10,7 @@ services:
|
||||||
- PAPERJET_DATABASE_PATH=/data/db/app.sqlite
|
- PAPERJET_DATABASE_PATH=/data/db/app.sqlite
|
||||||
- PAPERJET_PDF_STORAGE_PATH=/data/pdfs
|
- PAPERJET_PDF_STORAGE_PATH=/data/pdfs
|
||||||
- PAPERJET_THUMBNAILS_PATH=/data/thumbnails
|
- PAPERJET_THUMBNAILS_PATH=/data/thumbnails
|
||||||
|
- PAPERJET_DEBUG=true
|
||||||
volumes:
|
volumes:
|
||||||
- pdf_storage:/data/pdfs
|
- pdf_storage:/data/pdfs
|
||||||
- thumbnails:/data/thumbnails
|
- thumbnails:/data/thumbnails
|
||||||
|
|
|
||||||
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
legacy-peer-deps=true
|
||||||
|
|
@ -3,7 +3,7 @@ FROM node:22-alpine AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm install --legacy-peer-deps
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@ server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
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
|
# Match MAX_UPLOAD_MB — raise in both this and the outer reverse proxy
|
||||||
client_max_body_size 200m;
|
client_max_body_size 200m;
|
||||||
|
|
||||||
|
|
@ -42,7 +48,7 @@ server {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cache static assets
|
# 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;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
|
|
|
||||||
1
frontend/openapi.json
Normal file
1
frontend/openapi.json
Normal file
File diff suppressed because one or more lines are too long
462
frontend/package-lock.json
generated
462
frontend/package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
||||||
"@fontsource/outfit": "^5.2.8",
|
"@fontsource/outfit": "^5.2.8",
|
||||||
"@fontsource/plus-jakarta-sans": "^5.2.8",
|
"@fontsource/plus-jakarta-sans": "^5.2.8",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"fabric": "^7.4.0",
|
"fabric": "^7.4.0",
|
||||||
"pdfjs-dist": "^6.0.227",
|
"pdfjs-dist": "^6.0.227",
|
||||||
|
|
@ -18,6 +19,7 @@
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-router-dom": "^7.17.0",
|
"react-router-dom": "^7.17.0",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
|
"uuid": "^14.0.0",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -34,6 +36,7 @@
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
"openapi-typescript": "^7.13.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
"vite": "^8.0.12",
|
"vite": "^8.0.12",
|
||||||
|
|
@ -129,7 +132,6 @@
|
||||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.7",
|
"@babel/code-frame": "^7.29.7",
|
||||||
"@babel/generator": "^7.29.7",
|
"@babel/generator": "^7.29.7",
|
||||||
|
|
@ -450,7 +452,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
|
|
@ -499,7 +500,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1061,11 +1061,95 @@
|
||||||
"version": "0.133.0",
|
"version": "0.133.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"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": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||||
|
|
@ -1073,6 +1157,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1089,6 +1174,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1105,6 +1191,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1121,6 +1208,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1137,6 +1225,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1153,6 +1242,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1169,6 +1259,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1185,6 +1276,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1201,6 +1293,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1217,6 +1310,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1233,6 +1327,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1249,6 +1344,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1265,6 +1361,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1280,6 +1377,7 @@
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1291,6 +1389,7 @@
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1301,6 +1400,7 @@
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1314,6 +1414,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1330,6 +1431,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -1343,6 +1445,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
|
|
@ -1609,27 +1712,6 @@
|
||||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
"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": {
|
"node_modules/@testing-library/jest-dom": {
|
||||||
"version": "6.9.1",
|
"version": "6.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
|
||||||
|
|
@ -1695,13 +1777,6 @@
|
||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
|
@ -1752,9 +1827,8 @@
|
||||||
"version": "24.13.2",
|
"version": "24.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||||||
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1763,9 +1837,8 @@
|
||||||
"version": "19.2.17",
|
"version": "19.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
|
|
@ -1776,11 +1849,16 @@
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.61.0",
|
"version": "8.61.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
|
"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==",
|
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.61.0",
|
"@typescript-eslint/scope-manager": "8.61.0",
|
||||||
"@typescript-eslint/types": "8.61.0",
|
"@typescript-eslint/types": "8.61.0",
|
||||||
|
|
@ -2170,7 +2247,6 @@
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -2192,8 +2268,8 @@
|
||||||
"version": "7.1.4",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
|
|
@ -2215,28 +2291,22 @@
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "5.0.1",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-styles": {
|
"node_modules/argparse": {
|
||||||
"version": "5.2.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "Python-2.0"
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
|
|
@ -2357,7 +2427,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001782",
|
||||||
|
|
@ -2418,6 +2487,21 @@
|
||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"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": {
|
"node_modules/chai": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
|
|
@ -2428,6 +2512,13 @@
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/chownr": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
|
@ -2435,6 +2526,13 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true
|
"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": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"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"
|
"@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": {
|
"node_modules/cssstyle/node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "10.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
|
@ -2602,7 +2743,7 @@
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-urls": {
|
"node_modules/data-urls": {
|
||||||
|
|
@ -2706,13 +2847,6 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.371",
|
"version": "1.5.371",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz",
|
||||||
|
|
@ -2792,7 +2926,6 @@
|
||||||
"integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==",
|
"integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
|
|
@ -3210,6 +3343,7 @@
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
|
|
@ -3285,6 +3419,7 @@
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|
@ -3392,8 +3527,8 @@
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"agent-base": "^7.1.2",
|
"agent-base": "^7.1.2",
|
||||||
"debug": "4"
|
"debug": "4"
|
||||||
|
|
@ -3466,6 +3601,19 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
|
@ -3526,6 +3674,16 @@
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|
@ -3533,6 +3691,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/jsdom": {
|
||||||
"version": "29.1.1",
|
"version": "29.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||||
|
|
@ -3930,16 +4101,6 @@
|
||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
|
@ -4023,6 +4184,7 @@
|
||||||
"version": "3.3.12",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -4125,6 +4287,27 @@
|
||||||
"wrappy": "1"
|
"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": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|
@ -4175,6 +4358,24 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/parse5": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||||
|
|
@ -4231,14 +4432,15 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -4246,10 +4448,21 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -4312,21 +4525,6 @@
|
||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -4379,7 +4576,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
|
|
@ -4387,13 +4583,6 @@
|
||||||
"react": "^19.2.7"
|
"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": {
|
"node_modules/react-router": {
|
||||||
"version": "7.17.0",
|
"version": "7.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
|
||||||
|
|
@ -4475,6 +4664,7 @@
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.133.0",
|
"@oxc-project/types": "=0.133.0",
|
||||||
|
|
@ -4707,6 +4897,19 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/symbol-tree": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
|
@ -4784,6 +4987,7 @@
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -4898,13 +5102,25 @@
|
||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -4951,7 +5167,7 @@
|
||||||
"version": "7.18.2",
|
"version": "7.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
|
|
@ -4995,6 +5211,13 @@
|
||||||
"punycode": "^2.1.0"
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|
@ -5002,12 +5225,25 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"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": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.16",
|
"version": "8.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
|
|
@ -5328,6 +5564,23 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|
@ -5347,7 +5600,6 @@
|
||||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"@fontsource/outfit": "^5.2.8",
|
"@fontsource/outfit": "^5.2.8",
|
||||||
"@fontsource/plus-jakarta-sans": "^5.2.8",
|
"@fontsource/plus-jakarta-sans": "^5.2.8",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"fabric": "^7.4.0",
|
"fabric": "^7.4.0",
|
||||||
"pdfjs-dist": "^6.0.227",
|
"pdfjs-dist": "^6.0.227",
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-router-dom": "^7.17.0",
|
"react-router-dom": "^7.17.0",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
|
"uuid": "^14.0.0",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -39,6 +41,7 @@
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
"openapi-typescript": "^7.13.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
"vite": "^8.0.12",
|
"vite": "^8.0.12",
|
||||||
|
|
|
||||||
181
frontend/src/features/editor/canvas/AnnotationLayer.tsx
Normal file
181
frontend/src/features/editor/canvas/AnnotationLayer.tsx
Normal file
|
|
@ -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<HTMLCanvasElement>(null);
|
||||||
|
const fabricRef = useRef<fabric.Canvas | null>(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 (
|
||||||
|
<div className="absolute top-0 left-0" style={{ width, height }}>
|
||||||
|
<canvas ref={canvasRef} />
|
||||||
|
{isSelectedOnThisPage && (
|
||||||
|
<TextFormatToolbar annotationId={selection!} viewportParams={viewportParams} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
frontend/src/features/editor/pages/PageRenderer.tsx
Normal file
88
frontend/src/features/editor/pages/PageRenderer.tsx
Normal file
|
|
@ -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<HTMLCanvasElement>(null);
|
||||||
|
const [page, setPage] = useState<PDFPageProxy | null>(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 (
|
||||||
|
<div
|
||||||
|
className="bg-white shadow-sm flex items-center justify-center text-neutral-400 border border-neutral-200"
|
||||||
|
style={{ width: 612 * scale, height: 792 * scale }}
|
||||||
|
>
|
||||||
|
Loading page {pageNumber}...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseViewport = page ? page.getViewport({ scale: 1, rotation: 0 }) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative shadow-xl bg-white border border-neutral-200 group">
|
||||||
|
<canvas ref={canvasRef} className="block" />
|
||||||
|
|
||||||
|
{baseViewport && (
|
||||||
|
<AnnotationLayer
|
||||||
|
pageNumber={pageNumber}
|
||||||
|
width={baseViewport.width * scale}
|
||||||
|
height={baseViewport.height * scale}
|
||||||
|
viewportParams={{
|
||||||
|
scale,
|
||||||
|
rotation: page.rotate,
|
||||||
|
canonicalWidth: baseViewport.width,
|
||||||
|
canonicalHeight: baseViewport.height,
|
||||||
|
dpr
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="absolute -left-10 top-2 text-xs font-bold text-neutral-400 bg-white/80 rounded-md px-1.5 py-0.5 shadow-sm border border-neutral-200">
|
||||||
|
{pageNumber}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
frontend/src/features/editor/pages/PageStack.tsx
Normal file
32
frontend/src/features/editor/pages/PageStack.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="flex flex-col items-center gap-8 py-8 w-full max-w-full">
|
||||||
|
{pages.map(pageNum => (
|
||||||
|
<PageRenderer
|
||||||
|
key={pageNum}
|
||||||
|
pdfDoc={pdfDoc}
|
||||||
|
pageNumber={pageNum}
|
||||||
|
scale={scale}
|
||||||
|
dpr={dpr}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
frontend/src/features/editor/pages/PdfDocument.tsx
Normal file
53
frontend/src/features/editor/pages/PdfDocument.tsx
Normal file
|
|
@ -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<pdfjsLib.PDFDocumentProxy | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
|
<div className="text-red-500 bg-red-50 px-4 py-3 rounded-lg border border-red-200">
|
||||||
|
Error loading PDF: {error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pdfDoc) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full w-full gap-4">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accent-500"></div>
|
||||||
|
<div className="text-neutral-500 font-medium animate-pulse">Loading document...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PageStack pdfDoc={pdfDoc} />;
|
||||||
|
}
|
||||||
50
frontend/src/features/editor/store.ts
Normal file
50
frontend/src/features/editor/store.ts
Normal file
|
|
@ -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<Annotation>) => 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<EditorState>((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 }),
|
||||||
|
}));
|
||||||
63
frontend/src/features/editor/toolbar/EditorToolbar.tsx
Normal file
63
frontend/src/features/editor/toolbar/EditorToolbar.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="fixed top-4 left-1/2 -translate-x-1/2 bg-white rounded-lg shadow-xl border border-neutral-200 px-2 py-2 flex items-center gap-2 z-50">
|
||||||
|
<button
|
||||||
|
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
activeTool === 'select'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'text-neutral-600 hover:bg-neutral-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTool('select')}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
activeTool === 'text'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'text-neutral-600 hover:bg-neutral-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTool('text')}
|
||||||
|
>
|
||||||
|
Add Text
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-neutral-200 mx-2" />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={handleZoomOut}
|
||||||
|
className="px-2 py-1 text-sm font-medium text-neutral-600 hover:bg-neutral-100 rounded"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-medium text-neutral-600 w-12 text-center">
|
||||||
|
{Math.round(zoom * 100)}%
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleZoomIn}
|
||||||
|
className="px-2 py-1 text-sm font-medium text-neutral-600 hover:bg-neutral-100 rounded"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-neutral-200 mx-2" />
|
||||||
|
|
||||||
|
<div className="text-xs font-medium text-neutral-400 min-w-16 text-center">
|
||||||
|
{saveStatus === 'saving' && 'Saving...'}
|
||||||
|
{saveStatus === 'saved' && 'Saved'}
|
||||||
|
{saveStatus === 'error' && <span className="text-red-500">Error saving</span>}
|
||||||
|
{saveStatus === 'idle' && ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
frontend/src/features/editor/toolbar/TextFormatToolbar.tsx
Normal file
148
frontend/src/features/editor/toolbar/TextFormatToolbar.tsx
Normal file
|
|
@ -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<TextAnnotation['props']>) => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="absolute z-50 bg-white rounded-md shadow-lg border border-neutral-200 flex items-center p-1 gap-1"
|
||||||
|
style={{ top: `${Math.max(0, top)}px`, left: `${left}px` }}
|
||||||
|
>
|
||||||
|
{/* Font Family */}
|
||||||
|
<select
|
||||||
|
value={props.fontFamily}
|
||||||
|
onChange={(e) => updateProps({ fontFamily: e.target.value })}
|
||||||
|
className="text-xs bg-neutral-100 hover:bg-neutral-200 rounded px-2 py-1 outline-none cursor-pointer"
|
||||||
|
>
|
||||||
|
{FONTS.map(f => <option key={f} value={f}>{f}</option>)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div className="w-px h-4 bg-neutral-200 mx-1" />
|
||||||
|
|
||||||
|
{/* Font Size */}
|
||||||
|
<select
|
||||||
|
value={props.fontSize}
|
||||||
|
onChange={(e) => updateProps({ fontSize: Number(e.target.value) })}
|
||||||
|
className="text-xs bg-neutral-100 hover:bg-neutral-200 rounded px-2 py-1 outline-none cursor-pointer"
|
||||||
|
>
|
||||||
|
{SIZES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div className="w-px h-4 bg-neutral-200 mx-1" />
|
||||||
|
|
||||||
|
{/* Bold / Italic */}
|
||||||
|
<button
|
||||||
|
onClick={() => updateProps({ bold: !props.bold })}
|
||||||
|
className={`w-6 h-6 flex items-center justify-center rounded text-sm font-bold ${props.bold ? 'bg-blue-100 text-blue-700' : 'hover:bg-neutral-100 text-neutral-700'}`}
|
||||||
|
title="Bold"
|
||||||
|
>
|
||||||
|
B
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateProps({ italic: !props.italic })}
|
||||||
|
className={`w-6 h-6 flex items-center justify-center rounded text-sm italic font-serif ${props.italic ? 'bg-blue-100 text-blue-700' : 'hover:bg-neutral-100 text-neutral-700'}`}
|
||||||
|
title="Italic"
|
||||||
|
>
|
||||||
|
I
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-4 bg-neutral-200 mx-1" />
|
||||||
|
|
||||||
|
{/* Text Color */}
|
||||||
|
<div className="flex gap-0.5 items-center">
|
||||||
|
{COLORS.map(c => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
onClick={() => updateProps({ color: c })}
|
||||||
|
className={`w-4 h-4 rounded-full border ${props.color === c ? 'border-blue-500 scale-110' : 'border-neutral-300 hover:scale-110'}`}
|
||||||
|
style={{ backgroundColor: c }}
|
||||||
|
title="Text Color"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-px h-4 bg-neutral-200 mx-1" />
|
||||||
|
|
||||||
|
{/* Highlight Color */}
|
||||||
|
<div className="flex gap-0.5 items-center">
|
||||||
|
{HIGHLIGHTS.map(c => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
onClick={() => updateProps({ highlightColor: c === 'transparent' ? null : c })}
|
||||||
|
className={`w-4 h-4 rounded-sm border ${props.highlightColor === c || (c === 'transparent' && !props.highlightColor) ? 'border-blue-500 scale-110' : 'border-neutral-300 hover:scale-110'}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: c === 'transparent' ? '#ffffff' : c,
|
||||||
|
backgroundImage: c === 'transparent' ? 'linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc)' : 'none',
|
||||||
|
backgroundSize: '4px 4px',
|
||||||
|
backgroundPosition: '0 0, 2px 2px'
|
||||||
|
}}
|
||||||
|
title="Highlight Color"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-px h-4 bg-neutral-200 mx-1" />
|
||||||
|
|
||||||
|
{/* Duplicate */}
|
||||||
|
<button
|
||||||
|
onClick={handleDuplicate}
|
||||||
|
className="text-xs px-2 py-1 rounded text-neutral-600 hover:bg-neutral-100 font-medium"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Delete */}
|
||||||
|
<button
|
||||||
|
onClick={() => deleteAnnotation(annotationId)}
|
||||||
|
className="text-xs px-2 py-1 rounded text-red-600 hover:bg-red-50 font-medium ml-1"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
frontend/src/features/editor/tools/TextTool.ts
Normal file
141
frontend/src/features/editor/tools/TextTool.ts
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
28
frontend/src/features/editor/tools/index.ts
Normal file
28
frontend/src/features/editor/tools/index.ts
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
75
frontend/src/features/editor/useAutosave.ts
Normal file
75
frontend/src/features/editor/useAutosave.ts
Normal file
|
|
@ -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<number | null>(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]);
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import { formatDistanceToNow } from 'date-fns'
|
||||||
import { ContextMenu } from './ContextMenu'
|
import { ContextMenu } from './ContextMenu'
|
||||||
|
|
||||||
export const DocumentGrid = ({ isTrash }: { isTrash: boolean }) => {
|
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 navigate = useNavigate()
|
||||||
|
|
||||||
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, docId: string } | null>(null)
|
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, docId: string } | null>(null)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { libraryApi } from './api'
|
import { libraryApi } from './api'
|
||||||
import type { DocumentMeta } from './types'
|
import type { DocumentMeta, DocumentUpdateRequest } from './types'
|
||||||
|
|
||||||
interface LibraryState {
|
interface LibraryState {
|
||||||
documents: DocumentMeta[]
|
documents: DocumentMeta[]
|
||||||
|
|
@ -25,6 +25,7 @@ interface LibraryState {
|
||||||
bulkRestore: () => Promise<void>
|
bulkRestore: () => Promise<void>
|
||||||
emptyTrash: () => Promise<void>
|
emptyTrash: () => Promise<void>
|
||||||
setSearchQuery: (query: string) => void
|
setSearchQuery: (query: string) => void
|
||||||
|
updateDocument: (id: string, data: DocumentUpdateRequest) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLibrary = create<LibraryState>((set, get) => ({
|
export const useLibrary = create<LibraryState>((set, get) => ({
|
||||||
|
|
@ -176,5 +177,19 @@ export const useLibrary = create<LibraryState>((set, get) => ({
|
||||||
set({ error: err.error?.message || err.message, isLoading: false })
|
set({ error: err.error?.message || err.message, isLoading: false })
|
||||||
throw err
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
43
frontend/src/lib/annotations/registry.ts
Normal file
43
frontend/src/lib/annotations/registry.ts
Normal file
|
|
@ -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<string, ToolHandler>();
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
28
frontend/src/lib/annotations/types.ts
Normal file
28
frontend/src/lib/annotations/types.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import type { components } from '../../types/api';
|
||||||
|
|
||||||
|
export type TextProps = components['schemas']['TextProps'] & { highlightColor?: string | null };
|
||||||
|
|
||||||
|
export type TextAnnotation = Omit<components['schemas']['TextAnnotation'], 'props'> & {
|
||||||
|
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'];
|
||||||
95
frontend/src/lib/coords.test.ts
Normal file
95
frontend/src/lib/coords.test.ts
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
74
frontend/src/lib/coords.ts
Normal file
74
frontend/src/lib/coords.ts
Normal file
|
|
@ -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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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.
|
* Editor page — PDF canvas workspace.
|
||||||
* Full implementation begins in Phase 2 (rendering) and Phase 3 (annotations).
|
|
||||||
*/
|
*/
|
||||||
export function EditorPage() {
|
export function EditorPage() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
|
const { setDocumentId } = useEditorStore()
|
||||||
|
useAutosave()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
setDocumentId(id)
|
||||||
|
}
|
||||||
|
}, [id, setDocumentId])
|
||||||
|
|
||||||
|
if (!id) return <div>Invalid document ID</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col bg-neutral-100">
|
<div className="flex h-screen flex-col bg-neutral-100 overflow-hidden">
|
||||||
{/* Editor toolbar */}
|
{/* Editor toolbar */}
|
||||||
<header className="flex items-center justify-between border-b border-neutral-200 bg-white px-4 py-2">
|
<header className="flex-none flex items-center justify-between border-b border-neutral-200 bg-white px-4 py-2 z-10 shadow-sm">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<a
|
<Link
|
||||||
href="/"
|
to="/"
|
||||||
className="rounded-md p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100
|
className="rounded-md p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-700"
|
||||||
hover:text-neutral-700"
|
|
||||||
>
|
>
|
||||||
← Back
|
← Back
|
||||||
</a>
|
</Link>
|
||||||
<span className="text-sm font-medium text-neutral-700">
|
<span className="text-sm font-medium text-neutral-700">
|
||||||
Document {id}
|
Document {id}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -30,20 +44,18 @@ export function EditorPage() {
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-lg bg-accent-500 px-3 py-1.5 text-sm font-medium text-white
|
className="rounded-lg bg-accent-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-accent-600"
|
||||||
transition-colors hover:bg-accent-600"
|
|
||||||
>
|
>
|
||||||
Export PDF
|
Export PDF
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Canvas area */}
|
{/* Main workspace area */}
|
||||||
<main className="flex flex-1 items-center justify-center overflow-auto">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<div className="text-sm text-neutral-400">
|
<EditorToolbar />
|
||||||
PDF viewer will render here (Phase 2)
|
<PdfDocument documentId={id} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1501
frontend/src/types/api.d.ts
vendored
Normal file
1501
frontend/src/types/api.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
1499
frontend/src/types/api.ts
Normal file
1499
frontend/src/types/api.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -23,11 +23,9 @@
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"ignoreDeprecations": "6.0",
|
|
||||||
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|
|
||||||
1
openapi.json
Normal file
1
openapi.json
Normal file
File diff suppressed because one or more lines are too long
1893
openapi_pretty.json
Normal file
1893
openapi_pretty.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -39,7 +39,8 @@
|
||||||
"align": { "type": "string", "enum": ["left", "center", "right"], "default": "left" },
|
"align": { "type": "string", "enum": ["left", "center", "right"], "default": "left" },
|
||||||
"bold": { "type": "boolean", "default": false },
|
"bold": { "type": "boolean", "default": false },
|
||||||
"italic": { "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"]
|
"required": ["text"]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue