Merge branch 'V2' into settingsPage

Resolved conflicts by:
- Using V2's @app path aliases for imports
- Keeping settingsPage's admin settings features and legal policy links
- Keeping settingsPage's first login password change feature
- Combining imports where both branches added functionality
This commit is contained in:
Anthony Stirling
2025-10-28 11:37:12 +00:00
765 changed files with 4302 additions and 2620 deletions
+37 -30
View File
@@ -31,10 +31,15 @@ jobs:
project: ${{ steps.changes.outputs.project }}
openapi: ${{ steps.changes.outputs.openapi }}
steps:
- uses: actions/checkout@v4.3.0
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Check for file changes
uses: dorny/paths-filter@v3.0.2
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
with:
filters: .github/config/.files.yaml
@@ -51,19 +56,19 @@ jobs:
spring-security: [true, false]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: ${{ matrix.jdk-version }}
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4.4.2
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
@@ -89,7 +94,7 @@ jobs:
done
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
path: |
@@ -106,26 +111,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@v2.13.0
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 17
uses: actions/setup-java@v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@v4.4.2
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Generate OpenAPI documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
env:
DISABLE_ADDITIONAL_FEATURES: true
- name: Upload OpenAPI Documentation
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: openapi-docs
path: ./SwaggerDoc.json
@@ -134,19 +139,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@v2.12.2
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Node.js
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: '20'
node-version: '22'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Type-check frontend
run: cd frontend && npm run prebuild && npm run typecheck:all
- name: Lint frontend
run: cd frontend && npm run lint
- name: Build frontend
@@ -154,7 +161,7 @@ jobs:
- name: Run frontend tests
run: cd frontend && npm run test -- --run
- name: Upload frontend build artifacts
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: frontend-build
path: frontend/dist/
@@ -166,13 +173,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 17
uses: actions/setup-java@v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
distribution: "temurin"
@@ -180,7 +187,7 @@ jobs:
run: ./gradlew clean checkLicense
- name: FAILED - check the licenses for compatibility
if: failure()
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
@@ -207,15 +214,15 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Java 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
distribution: "temurin"
@@ -225,11 +232,11 @@ jobs:
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.37.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.12"
cache: 'pip' # caching pip dependencies
@@ -256,21 +263,21 @@ jobs:
docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
distribution: "temurin"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
@@ -355,6 +355,8 @@ public class ApplicationProperties {
private String tessdataDir;
private Boolean enableAlphaFunctionality;
private Boolean enableAnalytics;
private Boolean enablePosthog;
private Boolean enableScarf;
private Datasource datasource;
private Boolean disableSanitize;
private int maxDPI;
@@ -368,6 +370,18 @@ public class ApplicationProperties {
public boolean isAnalyticsEnabled() {
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
}
public boolean isPosthogEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnablePosthog() == null || this.getEnablePosthog());
}
public boolean isScarfEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnableScarf() == null || this.getEnableScarf());
}
}
@Data
@@ -56,7 +56,7 @@ public class PostHogService {
}
private void captureSystemInfo() {
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
if (!applicationProperties.getSystem().isPosthogEnabled()) {
return;
}
try {
@@ -67,7 +67,7 @@ public class PostHogService {
}
public void captureEvent(String eventName, Map<String, Object> properties) {
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
if (!applicationProperties.getSystem().isPosthogEnabled()) {
return;
}
@@ -325,6 +325,14 @@ public class PostHogService {
properties,
"system_enableAnalytics",
applicationProperties.getSystem().isAnalyticsEnabled());
addIfNotEmpty(
properties,
"system_enablePosthog",
applicationProperties.getSystem().isPosthogEnabled());
addIfNotEmpty(
properties,
"system_enableScarf",
applicationProperties.getSystem().isScarfEnabled());
// Capture UI properties
addIfNotEmpty(
@@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import io.swagger.v3.oas.annotations.Hidden;
@@ -31,7 +32,7 @@ public class SettingsController {
@AutoJobPostMapping("/update-enable-analytics")
@Hidden
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
public ResponseEntity<String> updateApiKey(@RequestParam Boolean enabled) throws IOException {
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
.body(
@@ -82,6 +82,8 @@ public class ConfigController {
applicationProperties.getSystem().getEnableAlphaFunctionality());
configData.put(
"enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
configData.put("enablePosthog", applicationProperties.getSystem().getEnablePosthog());
configData.put("enableScarf", applicationProperties.getSystem().getEnableScarf());
// Premium/Enterprise settings
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
@@ -107,8 +107,8 @@ mail:
from: '' # sender email address
legal:
termsAndConditions: https://www.stirlingpdf.com/terms # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
termsAndConditions: https://www.stirling.com/legal/terms-of-service # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirling.com/legal/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
@@ -121,7 +121,9 @@ system:
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
enableAnalytics: null # Master toggle for analytics: set to 'true' to enable all analytics, 'false' to disable all analytics, or leave as 'null' to prompt admin on first launch
enablePosthog: null # Enable PostHog analytics (open-source product analytics): set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled
enableScarf: null # Enable Scarf tracking pixel: set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
maxDPI: 500 # Maximum allowed DPI for PDF to image conversion
+141
View File
@@ -0,0 +1,141 @@
# Unified Dockerfile - Frontend + Backend in single container
# Supports MODE parameter: BOTH (default), FRONTEND, BACKEND
# Stage 1: Build Frontend
FROM node:20-alpine AS frontend-build
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend .
RUN npm run build
# Stage 2: Build Backend
FROM gradle:8.14-jdk21 AS backend-build
COPY build.gradle .
COPY settings.gradle .
COPY gradlew .
COPY gradle gradle/
COPY app/core/build.gradle core/.
COPY app/common/build.gradle common/.
COPY app/proprietary/build.gradle proprietary/.
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
WORKDIR /app
COPY . .
RUN DISABLE_ADDITIONAL_FEATURES=false \
STIRLING_PDF_DESKTOP_UI=false \
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
# Stage 3: Final unified image
FROM alpine:3.22.1
ARG VERSION_TAG
# Labels
LABEL org.opencontainers.image.title="Stirling-PDF Unified"
LABEL org.opencontainers.image.description="Unified container for Stirling-PDF - Frontend + Backend with MODE parameter"
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.vendor="Stirling-Tools"
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
LABEL maintainer="Stirling-Tools"
LABEL org.opencontainers.image.authors="Stirling-Tools"
LABEL org.opencontainers.image.version="${VERSION_TAG}"
LABEL org.opencontainers.image.keywords="PDF, manipulation, unified, API, Spring Boot, React"
# Copy backend files
COPY scripts /scripts
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY --from=backend-build /app/app/core/build/libs/*.jar app.jar
# Copy frontend files
COPY --from=frontend-build /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY docker/unified/nginx.conf /etc/nginx/nginx.conf
COPY docker/unified/entrypoint.sh /entrypoint.sh
# Environment Variables
ENV DISABLE_ADDITIONAL_FEATURES=false \
VERSION_TAG=$VERSION_TAG \
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_CUSTOM_OPTS="" \
HOME=/home/stirlingpdfuser \
PUID=1000 \
PGID=1000 \
UMASK=022 \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=$PATH:/opt/venv/bin \
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
TMPDIR=/tmp/stirling-pdf \
TEMP=/tmp/stirling-pdf \
TMP=/tmp/stirling-pdf \
MODE=BOTH \
BACKEND_INTERNAL_PORT=8081 \
VITE_API_BASE_URL=http://localhost:8080
# Install all dependencies
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
apk upgrade --no-cache -a && \
apk add --no-cache \
ca-certificates \
tzdata \
tini \
bash \
curl \
shadow \
su-exec \
openssl \
openssl-dev \
openjdk21-jre \
nginx \
# Doc conversion
gcompat \
libc6-compat \
libreoffice \
# pdftohtml
poppler-utils \
# OCR MY PDF
unpaper \
tesseract-ocr-data-eng \
tesseract-ocr-data-chi_sim \
tesseract-ocr-data-deu \
tesseract-ocr-data-fra \
tesseract-ocr-data-por \
ocrmypdf \
# CV
py3-opencv \
python3 \
py3-pip \
py3-pillow@testing \
py3-pdf2image@testing && \
python3 -m venv /opt/venv && \
/opt/venv/bin/pip install --upgrade pip setuptools && \
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \
mkdir -p /var/lib/nginx/tmp /var/log/nginx && \
fc-cache -f -v && \
chmod +x /scripts/* && \
chmod +x /entrypoint.sh && \
# User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf /var/lib/nginx /var/log/nginx /usr/share/nginx && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp
ENTRYPOINT ["tini", "--", "/entrypoint.sh"]
@@ -0,0 +1,58 @@
# Example Docker Compose for Unified Stirling-PDF Container
# MODE=BACKEND: Backend API only (no frontend)
services:
stirling-pdf-backend-only:
container_name: Stirling-PDF-Backend-Only
build:
context: ../..
dockerfile: docker/Dockerfile.unified
ports:
- "8080:8080"
volumes:
- ./stirling/data:/usr/share/tessdata:rw
- ./stirling/config:/configs:rw
- ./stirling/logs:/logs:rw
- ./stirling/customFiles:/customFiles:rw
- ./stirling/pipeline:/pipeline:rw
environment:
# MODE parameter: BACKEND only
MODE: BACKEND
# Standard Stirling-PDF configuration
DISABLE_ADDITIONAL_FEATURES: "false"
DOCKER_ENABLE_SECURITY: "false"
PUID: 1000
PGID: 1000
UMASK: "022"
# Application settings
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
# Optional: Add OCR languages (comma-separated)
# TESSERACT_LANGS: "deu,fra,spa"
# Optional: Java memory settings
# JAVA_CUSTOM_OPTS: "-Xmx4g"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 2G
# Access the API at: http://localhost:8080/api
# Swagger UI at: http://localhost:8080/swagger-ui/index.html
@@ -0,0 +1,59 @@
# Example Docker Compose for Unified Stirling-PDF Container
# MODE=BOTH (default): Frontend + Backend in single container on port 8080
services:
stirling-pdf-unified:
container_name: Stirling-PDF-Unified-Both
build:
context: ../..
dockerfile: docker/Dockerfile.unified
ports:
- "8080:8080"
volumes:
- ./stirling/data:/usr/share/tessdata:rw
- ./stirling/config:/configs:rw
- ./stirling/logs:/logs:rw
- ./stirling/customFiles:/customFiles:rw
- ./stirling/pipeline:/pipeline:rw
environment:
# MODE parameter: BOTH (default), FRONTEND, or BACKEND
MODE: BOTH
# Backend runs internally on this port when MODE=BOTH
BACKEND_INTERNAL_PORT: 8081
# Standard Stirling-PDF configuration
DISABLE_ADDITIONAL_FEATURES: "false"
DOCKER_ENABLE_SECURITY: "false"
PUID: 1000
PGID: 1000
UMASK: "022"
# Application settings
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Your locally hosted one-stop-shop for all your PDF needs
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
# Optional: Add OCR languages (comma-separated)
# TESSERACT_LANGS: "deu,fra,spa"
# Optional: Java memory settings
# JAVA_CUSTOM_OPTS: "-Xmx4g"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 2G
@@ -0,0 +1,63 @@
# Example Docker Compose for Unified Stirling-PDF Container
# MODE=FRONTEND: Frontend only, connects to separate backend
services:
stirling-pdf-backend:
container_name: Stirling-PDF-Backend
build:
context: ../..
dockerfile: docker/Dockerfile.unified
ports:
- "8081:8080"
volumes:
- ./stirling/data:/usr/share/tessdata:rw
- ./stirling/config:/configs:rw
- ./stirling/logs:/logs:rw
- ./stirling/customFiles:/customFiles:rw
- ./stirling/pipeline:/pipeline:rw
environment:
MODE: BACKEND
DISABLE_ADDITIONAL_FEATURES: "false"
DOCKER_ENABLE_SECURITY: "false"
PUID: 1000
PGID: 1000
UMASK: "022"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 4G
stirling-pdf-frontend:
container_name: Stirling-PDF-Frontend
build:
context: ../..
dockerfile: docker/Dockerfile.unified
ports:
- "8080:8080"
environment:
MODE: FRONTEND
# Point to the backend service
VITE_API_BASE_URL: http://stirling-pdf-backend:8080
# Minimal config needed for frontend
PUID: 1000
PGID: 1000
depends_on:
- stirling-pdf-backend
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 512M
+458
View File
@@ -0,0 +1,458 @@
# Stirling-PDF Unified Container
Single Docker container that can run as **frontend + backend**, **frontend only**, or **backend only** using the `MODE` environment variable.
## Quick Start
### MODE=BOTH (Default)
Single container with both frontend and backend on port 8080:
```bash
docker run -p 8080:8080 \
-e MODE=BOTH \
stirlingtools/stirling-pdf:unified
```
Access at: `http://localhost:8080`
### MODE=FRONTEND
Frontend only, connecting to separate backend:
```bash
docker run -p 8080:8080 \
-e MODE=FRONTEND \
-e VITE_API_BASE_URL=http://backend:8080 \
stirlingtools/stirling-pdf:unified
```
### MODE=BACKEND
Backend API only:
```bash
docker run -p 8080:8080 \
-e MODE=BACKEND \
stirlingtools/stirling-pdf:unified
```
Access API at: `http://localhost:8080/api`
Swagger UI at: `http://localhost:8080/swagger-ui/index.html`
---
## Architecture
### MODE=BOTH (Default)
```
┌─────────────────────────────────────┐
│ Port 8080 (External) │
│ ┌───────────────────────────────┐ │
│ │ Nginx │ │
│ │ • Serves frontend (/) │ │
│ │ • Proxies /api/* → backend │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌───────────▼───────────────────┐ │
│ │ Backend (Internal 8081) │ │
│ │ • Spring Boot │ │
│ │ • PDF Processing │ │
│ │ • UnoServer │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
```
### MODE=FRONTEND
```
┌─────────────────────────────┐ ┌──────────────────┐
│ Frontend Container │ │ Backend │
│ Port 8080 │ │ (External) │
│ ┌───────────────────────┐ │ │ │
│ │ Nginx │ │──────▶ :8080/api │
│ │ • Serves frontend │ │ │ │
│ │ • Proxies to backend │ │ │ │
│ └───────────────────────┘ │ └──────────────────┘
└─────────────────────────────┘
```
### MODE=BACKEND
```
┌─────────────────────────────┐
│ Backend Container │
│ Port 8080 │
│ ┌───────────────────────┐ │
│ │ Spring Boot │ │
│ │ • API Endpoints │ │
│ │ • PDF Processing │ │
│ │ • UnoServer │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
```
---
## Environment Variables
### MODE Configuration
| Variable | Values | Default | Description |
|----------|--------|---------|-------------|
| `MODE` | `BOTH`, `FRONTEND`, `BACKEND` | `BOTH` | Container operation mode |
### MODE=BOTH Specific
| Variable | Default | Description |
|----------|---------|-------------|
| `BACKEND_INTERNAL_PORT` | `8081` | Internal port for backend when MODE=BOTH |
### MODE=FRONTEND Specific
| Variable | Default | Description |
|----------|---------|-------------|
| `VITE_API_BASE_URL` | `http://backend:8080` | Backend URL for API proxying |
### Standard Configuration
All modes support standard Stirling-PDF environment variables:
- `DISABLE_ADDITIONAL_FEATURES` - Enable/disable OCR and LibreOffice features
- `DOCKER_ENABLE_SECURITY` - Enable authentication
- `PUID` / `PGID` - User/Group IDs
- `SYSTEM_MAXFILESIZE` - Max upload size (MB)
- `TESSERACT_LANGS` - Comma-separated OCR language codes
- `JAVA_CUSTOM_OPTS` - Additional JVM options
See full configuration docs at: https://docs.stirlingpdf.com
---
## Docker Compose Examples
### Example 1: All-in-One (MODE=BOTH)
**File:** `docker/compose/docker-compose-unified-both.yml`
```yaml
services:
stirling-pdf:
image: stirlingtools/stirling-pdf:unified
ports:
- "8080:8080"
volumes:
- ./data:/usr/share/tessdata:rw
- ./config:/configs:rw
environment:
MODE: BOTH
restart: unless-stopped
```
### Example 2: Separate Frontend & Backend
**File:** `docker/compose/docker-compose-unified-frontend.yml`
```yaml
services:
backend:
image: stirlingtools/stirling-pdf:unified
ports:
- "8081:8080"
environment:
MODE: BACKEND
volumes:
- ./data:/usr/share/tessdata:rw
- ./config:/configs:rw
frontend:
image: stirlingtools/stirling-pdf:unified
ports:
- "8080:8080"
environment:
MODE: FRONTEND
VITE_API_BASE_URL: http://backend:8080
depends_on:
- backend
```
### Example 3: Backend API Only
**File:** `docker/compose/docker-compose-unified-backend.yml`
```yaml
services:
stirling-pdf-api:
image: stirlingtools/stirling-pdf:unified
ports:
- "8080:8080"
environment:
MODE: BACKEND
volumes:
- ./data:/usr/share/tessdata:rw
- ./config:/configs:rw
restart: unless-stopped
```
---
## Building the Image
```bash
# From repository root
docker build -t stirlingtools/stirling-pdf:unified -f docker/Dockerfile.unified .
```
### Build Arguments
| Argument | Description |
|----------|-------------|
| `VERSION_TAG` | Version tag for the image |
Example:
```bash
docker build \
--build-arg VERSION_TAG=v1.0.0 \
-t stirlingtools/stirling-pdf:unified \
-f docker/Dockerfile.unified .
```
---
## Use Cases
### 1. Simple Deployment (MODE=BOTH)
- **Best for:** Personal use, small teams, simple deployments
- **Pros:** Single container, easy setup, minimal configuration
- **Cons:** Frontend and backend scale together
### 2. Scaled Frontend (MODE=FRONTEND + BACKEND)
- **Best for:** High traffic, need to scale frontend independently
- **Pros:** Scale frontend containers separately, CDN-friendly
- **Example:**
```yaml
services:
backend:
image: stirlingtools/stirling-pdf:unified
environment:
MODE: BACKEND
deploy:
replicas: 1
frontend:
image: stirlingtools/stirling-pdf:unified
environment:
MODE: FRONTEND
VITE_API_BASE_URL: http://backend:8080
deploy:
replicas: 5 # Scale frontend independently
```
### 3. API-Only (MODE=BACKEND)
- **Best for:** Headless deployments, custom frontends, API integrations
- **Pros:** Minimal resources, no nginx overhead
- **Example:** Use with external frontend or API consumers
### 4. Multi-Backend Setup
- **Best for:** Load balancing, high availability
- **Example:**
```yaml
services:
backend-1:
image: stirlingtools/stirling-pdf:unified
environment:
MODE: BACKEND
backend-2:
image: stirlingtools/stirling-pdf:unified
environment:
MODE: BACKEND
frontend:
image: stirlingtools/stirling-pdf:unified
environment:
MODE: FRONTEND
VITE_API_BASE_URL: http://load-balancer:8080
```
---
## Port Configuration
All modes use **port 8080** by default:
- **MODE=BOTH**: Nginx listens on 8080, proxies to backend on internal 8081
- **MODE=FRONTEND**: Nginx listens on 8080
- **MODE=BACKEND**: Spring Boot listens on 8080
**Expose port 8080** in all configurations:
```yaml
ports:
- "8080:8080"
```
---
## Health Checks
### MODE=BOTH and MODE=BACKEND
```yaml
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"]
interval: 30s
timeout: 10s
retries: 3
```
### MODE=FRONTEND
```yaml
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
```
---
## Troubleshooting
### Check logs
```bash
docker logs stirling-pdf-container
```
Look for the startup banner:
```
===================================
Stirling-PDF Unified Container
MODE: BOTH
===================================
```
### Invalid MODE error
```
ERROR: Invalid MODE 'XYZ'. Must be BOTH, FRONTEND, or BACKEND
```
**Fix:** Set `MODE` to one of the three valid values.
### Frontend can't connect to backend (MODE=FRONTEND)
**Check:**
1. `VITE_API_BASE_URL` points to correct backend URL
2. Backend container is running and accessible
3. Network connectivity between containers
### Backend not starting (MODE=BOTH or BACKEND)
**Check:**
1. Sufficient memory allocated (4GB recommended)
2. Java heap size (`JAVA_CUSTOM_OPTS`)
3. Volume permissions for `/tmp/stirling-pdf`
---
## Migration Guide
### From Separate Containers → MODE=BOTH
**Before:**
```yaml
services:
frontend:
image: stirlingtools/stirling-pdf:frontend
ports: ["80:80"]
backend:
image: stirlingtools/stirling-pdf:backend
ports: ["8080:8080"]
```
**After:**
```yaml
services:
stirling-pdf:
image: stirlingtools/stirling-pdf:unified
ports: ["8080:8080"]
environment:
MODE: BOTH
```
### From Legacy → MODE=BACKEND
```yaml
services:
stirling-pdf:
image: stirlingtools/stirling-pdf:latest
ports: ["8080:8080"]
```
**Becomes:**
```yaml
services:
stirling-pdf:
image: stirlingtools/stirling-pdf:unified
ports: ["8080:8080"]
environment:
MODE: BACKEND
```
---
## Performance Tuning
### MODE=BOTH
```yaml
environment:
JAVA_CUSTOM_OPTS: "-Xmx4g -XX:MaxRAMPercentage=75"
BACKEND_INTERNAL_PORT: 8081
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 2G
```
### MODE=FRONTEND (Lightweight)
```yaml
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
```
### MODE=BACKEND (Heavy Processing)
```yaml
environment:
JAVA_CUSTOM_OPTS: "-Xmx8g"
deploy:
resources:
limits:
memory: 10G
reservations:
memory: 4G
```
---
## Security Considerations
1. **MODE=BOTH**: Backend not exposed externally (runs on internal port)
2. **MODE=BACKEND**: API exposed directly - consider API authentication
3. **MODE=FRONTEND**: Only serves static files - minimal attack surface
Enable security features:
```yaml
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
```
---
## Support
- Documentation: https://docs.stirlingpdf.com
- GitHub Issues: https://github.com/Stirling-Tools/Stirling-PDF/issues
- Docker Hub: https://hub.docker.com/r/stirlingtools/stirling-pdf
---
## License
MIT License - See repository for full details
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Build script for Stirling-PDF Unified Container
# Usage: ./build.sh [version-tag]
set -e
VERSION_TAG=${1:-latest}
IMAGE_NAME="stirlingtools/stirling-pdf:unified-${VERSION_TAG}"
echo "==================================="
echo "Building Stirling-PDF Unified Container"
echo "Version: $VERSION_TAG"
echo "Image: $IMAGE_NAME"
echo "==================================="
# Navigate to repository root (assuming script is in docker/unified/)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
REPO_ROOT="$SCRIPT_DIR/../.."
cd "$REPO_ROOT"
# Build the image
docker build \
--build-arg VERSION_TAG="$VERSION_TAG" \
-t "$IMAGE_NAME" \
-f docker/Dockerfile.unified \
.
echo "==================================="
echo "✓ Build complete!"
echo "Image: $IMAGE_NAME"
echo ""
echo "Test the image:"
echo " MODE=BOTH: docker run -p 8080:8080 -e MODE=BOTH $IMAGE_NAME"
echo " MODE=FRONTEND: docker run -p 8080:8080 -e MODE=FRONTEND $IMAGE_NAME"
echo " MODE=BACKEND: docker run -p 8080:8080 -e MODE=BACKEND $IMAGE_NAME"
echo "==================================="
+176
View File
@@ -0,0 +1,176 @@
#!/bin/bash
set -e
# Default MODE to BOTH if not set
MODE=${MODE:-BOTH}
echo "==================================="
echo "Stirling-PDF Unified Container"
echo "MODE: $MODE"
echo "==================================="
# Function to setup OCR (from init.sh)
setup_ocr() {
echo "Setting up OCR languages..."
# Copy tessdata
mkdir -p /usr/share/tessdata
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata 2>/dev/null || true
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata 2>/dev/null || true
fi
if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then
cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata 2>/dev/null || true
fi
# Install additional languages if specified
if [[ -n "$TESSERACT_LANGS" ]]; then
SPACE_SEPARATED_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
for LANG in $SPACE_SEPARATED_LANGS; do
if [[ $LANG =~ $pattern ]]; then
apk add --no-cache "tesseract-ocr-data-$LANG" 2>/dev/null || true
fi
done
fi
}
# Function to setup user permissions (from init-without-ocr.sh)
setup_permissions() {
echo "Setting up user permissions..."
export JAVA_TOOL_OPTIONS="${JAVA_BASE_OPTS} ${JAVA_CUSTOM_OPTS}"
# Update user and group IDs
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
usermod -o -u "$PUID" stirlingpdfuser || true
fi
if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -f3)" ]; then
groupmod -o -g "$PGID" stirlingpdfgroup || true
fi
umask "$UMASK" || true
# Install fonts if needed
if [[ -n "$LANGS" ]]; then
/scripts/installFonts.sh $LANGS
fi
# Ensure directories exist with correct permissions
mkdir -p /tmp/stirling-pdf || true
# Set ownership and permissions
chown -R stirlingpdfuser:stirlingpdfgroup \
$HOME /logs /scripts /usr/share/fonts/opentype/noto \
/configs /customFiles /pipeline /tmp/stirling-pdf \
/var/lib/nginx /var/log/nginx /usr/share/nginx \
/app.jar 2>/dev/null || echo "[WARN] Some chown operations failed, may run as host user"
chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto \
/configs /customFiles /pipeline /tmp/stirling-pdf 2>/dev/null || true
}
# Function to configure nginx
configure_nginx() {
local backend_url=$1
echo "Configuring nginx with backend URL: $backend_url"
sed -i "s|\${BACKEND_URL}|${backend_url}|g" /etc/nginx/nginx.conf
}
# Function to run as user or root depending on permissions
run_as_user() {
if [ "$(id -u)" = "0" ]; then
# Running as root, use su-exec
su-exec stirlingpdfuser "$@"
else
# Already running as non-root
exec "$@"
fi
}
# Setup OCR and permissions
setup_ocr
setup_permissions
# Handle different modes
case "$MODE" in
BOTH)
echo "Starting in BOTH mode: Frontend + Backend on port 8080"
# Configure nginx to proxy to internal backend
configure_nginx "http://localhost:${BACKEND_INTERNAL_PORT:-8081}"
# Start backend on internal port
echo "Starting backend on port ${BACKEND_INTERNAL_PORT:-8081}..."
run_as_user sh -c "java -Dfile.encoding=UTF-8 \
-Djava.io.tmpdir=/tmp/stirling-pdf \
-Dserver.port=${BACKEND_INTERNAL_PORT:-8081} \
-jar /app.jar" &
BACKEND_PID=$!
# Start unoserver for document conversion
run_as_user /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1 &
UNO_PID=$!
# Wait for backend to start
sleep 3
# Start nginx on port 8080
echo "Starting nginx on port 8080..."
run_as_user nginx -g "daemon off;" &
NGINX_PID=$!
echo "==================================="
echo "✓ Frontend available at: http://localhost:8080"
echo "✓ Backend API at: http://localhost:8080/api"
echo "✓ Backend running internally on port ${BACKEND_INTERNAL_PORT:-8081}"
echo "==================================="
;;
FRONTEND)
echo "Starting in FRONTEND mode: Frontend only on port 8080"
# Configure nginx with external backend URL
BACKEND_URL=${VITE_API_BASE_URL:-http://backend:8080}
configure_nginx "$BACKEND_URL"
# Start nginx on port 8080
echo "Starting nginx on port 8080..."
run_as_user nginx -g "daemon off;" &
NGINX_PID=$!
echo "==================================="
echo "✓ Frontend available at: http://localhost:8080"
echo "✓ Proxying API calls to: $BACKEND_URL"
echo "==================================="
;;
BACKEND)
echo "Starting in BACKEND mode: Backend only on port 8080"
# Start backend on port 8080
echo "Starting backend on port 8080..."
run_as_user sh -c "java -Dfile.encoding=UTF-8 \
-Djava.io.tmpdir=/tmp/stirling-pdf \
-Dserver.port=8080 \
-jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1" &
BACKEND_PID=$!
echo "==================================="
echo "✓ Backend API available at: http://localhost:8080/api"
echo "✓ Swagger UI at: http://localhost:8080/swagger-ui/index.html"
echo "==================================="
;;
*)
echo "ERROR: Invalid MODE '$MODE'. Must be BOTH, FRONTEND, or BACKEND"
exit 1
;;
esac
# Wait for all background processes
wait
+118
View File
@@ -0,0 +1,118 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Add .mjs MIME type mapping
types {
text/javascript mjs;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html index.htm;
# Global settings for file uploads
client_max_body_size 100m;
# Handle client-side routing - support subpaths
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API calls to backend
location /api/ {
proxy_pass ${BACKEND_URL}/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Additional headers for proper API proxying
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
# Timeout settings for large file uploads
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Request size limits for file uploads
client_max_body_size 100m;
proxy_request_buffering off;
}
# Proxy Swagger UI to backend (including versioned paths)
location ~ ^/swagger-ui(.*)$ {
proxy_pass ${BACKEND_URL}/swagger-ui$1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
}
# Proxy API docs to backend (with query parameters and sub-paths)
location ~ ^/v3/api-docs(.*)$ {
proxy_pass ${BACKEND_URL}/v3/api-docs$1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
# Proxy v1 API docs to backend (with query parameters and sub-paths)
location ~ ^/v1/api-docs(.*)$ {
proxy_pass ${BACKEND_URL}/v1/api-docs$1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
# Serve .mjs files with correct MIME type (must come before general static assets)
location ~* \.mjs$ {
try_files $uri =404;
add_header Content-Type "text/javascript; charset=utf-8" always;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
}
+1 -1
View File
@@ -18,6 +18,6 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+54 -37
View File
@@ -94,6 +94,7 @@
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
}
},
@@ -436,7 +437,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -483,7 +483,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -507,7 +506,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.14.tgz",
"integrity": "sha512-lE/vfhA53CxamaCfGWEibrEPr+JeZT42QCF+cOELUwv4+Zt6b+IE6+4wsznx/8wjjJYwllXJ3GJ/un1UzTqARw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/engines": "1.3.14",
"@embedpdf/models": "1.3.14"
@@ -588,7 +586,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.14.tgz",
"integrity": "sha512-77hnNLp0W0FHw8lT7SeqzCgp8bOClfeOAPZdcInu/jPDhVASUGYbtE/0fkLhiaqPH7kyMirNCLif4sF6n4b5vg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.3.14"
},
@@ -605,7 +602,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.14.tgz",
"integrity": "sha512-nR0ZxNoTQtGqOHhweFh6QJ+nUJ4S4Ag1wWur6vAUAi8U95HUOfZhOEa0polZo0zR9WmmblGqRWjFM+mVSOoi1w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.3.14"
},
@@ -622,7 +618,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.14.tgz",
"integrity": "sha512-KoJX1MacEWE2DrO1OeZeG/Ehz76//u+ida/xb4r9BfwqAp5TfYlksq09cOvcF8LMW5FY4pbAL+AHKI1Hjz+HNA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.3.14"
},
@@ -657,7 +652,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.14.tgz",
"integrity": "sha512-IPj7GCQXJBsY++JaU+z7y+FwX5NaDBj4YYV6hsHNtSGf42Y1AdlwJzDYetivG2bA84xmk7KgD1X2Y3eIFBhjwA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.3.14"
},
@@ -690,7 +684,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.14.tgz",
"integrity": "sha512-fQbt7OlRMLQJMuZj/Bzh0qpRxMw1ld5Qe/OTw8N54b/plljnFA52joE7cITl3H03huWWyHS3NKOScbw7f34dog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.3.14"
},
@@ -725,7 +718,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.14.tgz",
"integrity": "sha512-EXENuaAsse3rT6cjA1nYzyrNvoy62ojJl28wblCng6zcs3HSlGPemIQZAvaYKPUxoY608M+6nKlcMQ5neRnk/A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.3.14"
},
@@ -797,7 +789,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.14.tgz",
"integrity": "sha512-mfJ7EbbU68eKk6oFvQ4ozGJNpxUxWbjQ5Gm3uuB+Gj5/tWgBocBOX36k/9LgivEEeX7g2S0tOgyErljApmH8Vg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.3.14"
},
@@ -951,7 +942,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -995,7 +985,6 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -2029,7 +2018,6 @@
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.1.tgz",
"integrity": "sha512-OYfxn9cTv+K6RZ8+Ozn/HDQXkB8Fmn+KJJt5lxyFDP9F09EHnC59Ldadv1LyUZVBGtNqz4sn6b3vBShbxwAmYw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@floating-ui/react": "^0.27.16",
"clsx": "^2.1.1",
@@ -2080,7 +2068,6 @@
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.1.tgz",
"integrity": "sha512-lQutBS+Q0iz/cNFvdrsYassPWo3RtWcmDGJeOtKfHigLzFOhxUuLOkQgepDbMf3WcVMB/tist6Px1PQOv57JTw==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^18.x || ^19.x"
}
@@ -2148,7 +2135,6 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.2.tgz",
"integrity": "sha512-qXvbnawQhqUVfH1LMgMaiytP+ZpGoYhnGl7yYq2x57GYzcFL/iPzSZ3L30tlbwEjSVKNYcbiKO8tANR1tadjUg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.3",
"@mui/core-downloads-tracker": "^7.3.2",
@@ -3591,7 +3577,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -3915,7 +3900,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3926,7 +3910,6 @@
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -3987,7 +3970,6 @@
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.44.1",
"@typescript-eslint/types": "8.44.1",
@@ -4701,6 +4683,7 @@
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz",
"integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.21"
}
@@ -4710,6 +4693,7 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz",
"integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.21",
"@vue/shared": "3.5.21"
@@ -4720,6 +4704,7 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz",
"integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.21",
"@vue/runtime-core": "3.5.21",
@@ -4732,6 +4717,7 @@
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz",
"integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.21",
"@vue/shared": "3.5.21"
@@ -4759,7 +4745,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5434,7 +5419,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -6438,8 +6422,7 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
"integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -6834,7 +6817,6 @@
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7006,7 +6988,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7997,6 +7978,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true,
"license": "MIT"
},
"node_modules/gonzales-pe": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz",
@@ -8280,7 +8268,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -9077,7 +9064,6 @@
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@asamuzakjp/dom-selector": "^6.5.4",
"cssstyle": "^5.3.0",
@@ -10866,7 +10852,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11158,7 +11143,6 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -11531,7 +11515,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11541,7 +11524,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -13179,7 +13161,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -13325,6 +13306,27 @@
"node": ">=18"
}
},
"node_modules/tsconfck": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
"integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==",
"dev": true,
"license": "MIT",
"bin": {
"tsconfck": "bin/tsconfck.js"
},
"engines": {
"node": "^18 || >=20"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/tsconfig-paths": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
@@ -13462,7 +13464,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -13545,7 +13546,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -13765,7 +13765,6 @@
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -13858,6 +13857,26 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite-tsconfig-paths": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",
"integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"globrex": "^0.1.2",
"tsconfck": "^3.0.3"
},
"peerDependencies": {
"vite": "*"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -13897,7 +13916,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -13911,7 +13929,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
+7 -3
View File
@@ -57,12 +57,15 @@
},
"scripts": {
"predev": "npm run generate-icons",
"dev": "npm run typecheck && vite",
"dev": "vite",
"prebuild": "npm run generate-icons",
"lint": "eslint --max-warnings=0",
"build": "npm run typecheck && vite build",
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"typecheck:core": "tsc --noEmit --project tsconfig.core.json",
"typecheck:proprietary": "tsc --noEmit --project tsconfig.proprietary.json",
"typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary",
"check": "npm run typecheck && npm run lint && npm run test:run",
"generate-licenses": "node scripts/generate-licenses.js",
"generate-icons": "node scripts/generate-icons.js",
@@ -120,9 +123,9 @@
"@vitejs/plugin-react-swc": "^4.1.0",
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"jsdom": "^27.0.0",
"license-checker": "^25.0.1",
"madge": "^8.0.0",
@@ -134,6 +137,7 @@
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"depcheck": {
@@ -250,6 +250,7 @@
"title": "Do you want make Stirling PDF better?",
"paragraph1": "Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.",
"paragraph2": "Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.",
"learnMore": "Learn more",
"enable": "Enable analytics",
"disable": "Disable analytics",
"settings": "You can change the settings for analytics in the config/settings.yml file"
@@ -3371,6 +3372,10 @@
"title": "Analytics",
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
},
"services": {
"posthog": "PostHog Analytics",
"scarf": "Scarf Pixel"
}
},
"removeMetadata": {
@@ -4193,6 +4198,10 @@
"preview": "Preview"
},
"config": {
"overview": {
"title": "Application Configuration",
"description": "Current application settings and configuration details."
},
"account": {
"overview": {
"title": "Account Settings",
-38
View File
@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
-103
View File
@@ -1,103 +0,0 @@
import { Suspense } from "react";
import { Routes, Route } from "react-router-dom";
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
import { FileContextProvider } from "./contexts/FileContext";
import { NavigationProvider } from "./contexts/NavigationContext";
import { ToolRegistryProvider } from "./contexts/ToolRegistryProvider";
import { FilesModalProvider } from "./contexts/FilesModalContext";
import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext";
import { HotkeyProvider } from "./contexts/HotkeyContext";
import { SidebarProvider } from "./contexts/SidebarContext";
import { PreferencesProvider } from "./contexts/PreferencesContext";
import { OnboardingProvider } from "./contexts/OnboardingContext";
import { TourOrchestrationProvider } from "./contexts/TourOrchestrationContext";
import ErrorBoundary from "./components/shared/ErrorBoundary";
import OnboardingTour from "./components/onboarding/OnboardingTour";
// Import auth components
import { AuthProvider } from "./auth/UseSession";
import Landing from "./routes/Landing";
import Login from "./routes/Login";
import Signup from "./routes/Signup";
import AuthCallback from "./routes/AuthCallback";
// Import global styles
import "./styles/tailwind.css";
import "./styles/cookieconsent.css";
import "./index.css";
import { RightRailProvider } from "./contexts/RightRailContext";
import { ViewerProvider } from "./contexts/ViewerContext";
import { SignatureProvider } from "./contexts/SignatureContext";
// Import file ID debugging helpers (development only)
import "./utils/fileIdSafety";
// Loading component for i18next suspense
const LoadingFallback = () => (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
fontSize: "18px",
color: "#666",
}}
>
Loading...
</div>
);
export default function App() {
return (
<Suspense fallback={<LoadingFallback />}>
<PreferencesProvider>
<RainbowThemeProvider>
<ErrorBoundary>
<AuthProvider>
<Routes>
{/* Auth routes - no FileContext or other providers needed */}
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/auth/callback" element={<AuthCallback />} />
{/* Main app routes - wrapped with all providers */}
<Route
path="/*"
element={
<OnboardingProvider>
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<ToolRegistryProvider>
<NavigationProvider>
<FilesModalProvider>
<ToolWorkflowProvider>
<HotkeyProvider>
<SidebarProvider>
<ViewerProvider>
<SignatureProvider>
<RightRailProvider>
<TourOrchestrationProvider>
<Landing />
<OnboardingTour />
</TourOrchestrationProvider>
</RightRailProvider>
</SignatureProvider>
</ViewerProvider>
</SidebarProvider>
</HotkeyProvider>
</ToolWorkflowProvider>
</FilesModalProvider>
</NavigationProvider>
</ToolRegistryProvider>
</FileContextProvider>
</OnboardingProvider>
}
/>
</Routes>
</AuthProvider>
</ErrorBoundary>
</RainbowThemeProvider>
</PreferencesProvider>
</Suspense>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { Suspense } from "react";
import { AppProviders } from "@app/components/AppProviders";
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
import HomePage from "@app/pages/HomePage";
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
// Import global styles
import "@app/styles/tailwind.css";
import "@app/styles/cookieconsent.css";
import "@app/styles/index.css";
// Import file ID debugging helpers (development only)
import "@app/utils/fileIdSafety";
export default function App() {
return (
<Suspense fallback={<LoadingFallback />}>
<AppProviders>
<HomePage />
<OnboardingTour />
</AppProviders>
</Suspense>
);
}
@@ -0,0 +1,67 @@
import { ReactNode } from "react";
import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider";
import { FileContextProvider } from "@app/contexts/FileContext";
import { NavigationProvider } from "@app/contexts/NavigationContext";
import { ToolRegistryProvider } from "@app/contexts/ToolRegistryProvider";
import { FilesModalProvider } from "@app/contexts/FilesModalContext";
import { ToolWorkflowProvider } from "@app/contexts/ToolWorkflowContext";
import { HotkeyProvider } from "@app/contexts/HotkeyContext";
import { SidebarProvider } from "@app/contexts/SidebarContext";
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
import { AppConfigProvider } from "@app/contexts/AppConfigContext";
import { RightRailProvider } from "@app/contexts/RightRailContext";
import { ViewerProvider } from "@app/contexts/ViewerContext";
import { SignatureProvider } from "@app/contexts/SignatureContext";
import { OnboardingProvider } from "@app/contexts/OnboardingContext";
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
import { useScarfTracking } from "@app/hooks/useScarfTracking";
// Component to initialize scarf tracking (must be inside AppConfigProvider)
function ScarfTrackingInitializer() {
useScarfTracking();
return null;
}
/**
* Core application providers
* Contains all providers needed for the core
*/
export function AppProviders({ children }: { children: ReactNode }) {
return (
<PreferencesProvider>
<RainbowThemeProvider>
<ErrorBoundary>
<OnboardingProvider>
<AppConfigProvider>
<ScarfTrackingInitializer />
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<ToolRegistryProvider>
<NavigationProvider>
<FilesModalProvider>
<ToolWorkflowProvider>
<HotkeyProvider>
<SidebarProvider>
<ViewerProvider>
<SignatureProvider>
<RightRailProvider>
<TourOrchestrationProvider>
{children}
</TourOrchestrationProvider>
</RightRailProvider>
</SignatureProvider>
</ViewerProvider>
</SidebarProvider>
</HotkeyProvider>
</ToolWorkflowProvider>
</FilesModalProvider>
</NavigationProvider>
</ToolRegistryProvider>
</FileContextProvider>
</AppConfigProvider>
</OnboardingProvider>
</ErrorBoundary>
</RainbowThemeProvider>
</PreferencesProvider>
);
}
@@ -1,17 +1,17 @@
import React, { useState, useCallback, useEffect } from 'react';
import { Modal } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { StirlingFileStub } from '../types/fileContext';
import { useFileManager } from '../hooks/useFileManager';
import { useFilesModalContext } from '../contexts/FilesModalContext';
import { Tool } from '../types/tool';
import MobileLayout from './fileManager/MobileLayout';
import DesktopLayout from './fileManager/DesktopLayout';
import DragOverlay from './fileManager/DragOverlay';
import { FileManagerProvider } from '../contexts/FileManagerContext';
import { Z_INDEX_FILE_MANAGER_MODAL } from '../styles/zIndex';
import { isGoogleDriveConfigured } from '../services/googleDrivePickerService';
import { loadScript } from '../utils/scriptLoader';
import { StirlingFileStub } from '@app/types/fileContext';
import { useFileManager } from '@app/hooks/useFileManager';
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
import { Tool } from '@app/types/tool';
import MobileLayout from '@app/components/fileManager/MobileLayout';
import DesktopLayout from '@app/components/fileManager/DesktopLayout';
import DragOverlay from '@app/components/fileManager/DragOverlay';
import { FileManagerProvider } from '@app/contexts/FileManagerContext';
import { Z_INDEX_FILE_MANAGER_MODAL } from '@app/styles/zIndex';
import { isGoogleDriveConfigured } from '@app/services/googleDrivePickerService';
import { loadScript } from '@app/utils/scriptLoader';
interface FileManagerProps {
selectedTool?: Tool | null;
@@ -3,9 +3,9 @@ import { Card, Group, Text, Button, Progress } from "@mantine/core";
import { useTranslation } from "react-i18next";
import StorageIcon from "@mui/icons-material/Storage";
import DeleteIcon from "@mui/icons-material/Delete";
import { StorageStats } from "../services/fileStorage";
import { formatFileSize } from "../utils/fileUtils";
import { getStorageUsagePercent } from "../utils/storageUtils";
import { StorageStats } from "@app/services/fileStorage";
import { formatFileSize } from "@app/utils/fileUtils";
import { getStorageUsagePercent } from "@app/utils/storageUtils";
interface StorageStatsCardProps {
storageStats: StorageStats | null;
@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import { Stack, Alert, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { DrawingControls } from './DrawingControls';
import { ColorPicker } from './ColorPicker';
import { usePDFAnnotation } from '../providers/PDFAnnotationProvider';
import { DrawingControls } from '@app/components/annotation/shared/DrawingControls';
import { ColorPicker } from '@app/components/annotation/shared/ColorPicker';
import { usePDFAnnotation } from '@app/components/annotation/providers/PDFAnnotationProvider';
export interface AnnotationToolConfig {
enableDrawing?: boolean;
@@ -1,7 +1,7 @@
import React, { useRef, useState } from 'react';
import { Paper, Button, Modal, Stack, Text, Popover, ColorPicker as MantineColorPicker } from '@mantine/core';
import { ColorSwatchButton } from './ColorPicker';
import PenSizeSelector from '../../tools/sign/PenSizeSelector';
import { ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker';
import PenSizeSelector from '@app/components/tools/sign/PenSizeSelector';
import SignaturePad from 'signature_pad';
interface DrawingCanvasProps {
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Stack, TextInput, Select, Combobox, useCombobox, Group, Box } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ColorPicker } from './ColorPicker';
import { ColorPicker } from '@app/components/annotation/shared/ColorPicker';
interface TextInputWithFontProps {
text: string;
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Stack } from '@mantine/core';
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
import { DrawingCanvas } from '../shared/DrawingCanvas';
import { BaseAnnotationTool } from '@app/components/annotation/shared/BaseAnnotationTool';
import { DrawingCanvas } from '@app/components/annotation/shared/DrawingCanvas';
interface DrawingToolProps {
onDrawingChange?: (data: string | null) => void;
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Stack } from '@mantine/core';
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
import { ImageUploader } from '../shared/ImageUploader';
import { BaseAnnotationTool } from '@app/components/annotation/shared/BaseAnnotationTool';
import { ImageUploader } from '@app/components/annotation/shared/ImageUploader';
interface ImageToolProps {
onImageChange?: (data: string | null) => void;
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Stack } from '@mantine/core';
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
import { TextInputWithFont } from '../shared/TextInputWithFont';
import { BaseAnnotationTool } from '@app/components/annotation/shared/BaseAnnotationTool';
import { TextInputWithFont } from '@app/components/annotation/shared/TextInputWithFont';
interface TextToolProps {
onTextChange?: (text: string) => void;
@@ -2,10 +2,10 @@ import React, { useRef, useState } from 'react';
import { Button, Group, useMantineColorScheme } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import AddIcon from '@mui/icons-material/Add';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import LocalIcon from '../shared/LocalIcon';
import { BASE_PATH } from '../../constants/app';
import styles from './FileEditor.module.css';
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
import LocalIcon from '@app/components/shared/LocalIcon';
import { BASE_PATH } from '@app/constants/app';
import styles from '@app/components/fileEditor/FileEditor.module.css';
interface AddFileCardProps {
onFileSelect: (files: File[]) => void;
@@ -1,19 +1,19 @@
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
import { useState, useCallback, useRef, useMemo, useEffect } from 'react';
import {
Text, Center, Box, LoadingOverlay, Stack
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useFileSelection, useFileState, useFileManagement, useFileActions, useFileContext } from '../../contexts/FileContext';
import { useNavigationActions } from '../../contexts/NavigationContext';
import { zipFileService } from '../../services/zipFileService';
import { detectFileExtension } from '../../utils/fileUtils';
import FileEditorThumbnail from './FileEditorThumbnail';
import AddFileCard from './AddFileCard';
import FilePickerModal from '../shared/FilePickerModal';
import { FileId, StirlingFile } from '../../types/fileContext';
import { alert } from '../toast';
import { downloadBlob } from '../../utils/downloadUtils';
import { useFileEditorRightRailButtons } from './fileEditorRightRailButtons';
import { useFileSelection, useFileState, useFileManagement, useFileActions, useFileContext } from '@app/contexts/FileContext';
import { useNavigationActions } from '@app/contexts/NavigationContext';
import { zipFileService } from '@app/services/zipFileService';
import { detectFileExtension } from '@app/utils/fileUtils';
import FileEditorThumbnail from '@app/components/fileEditor/FileEditorThumbnail';
import AddFileCard from '@app/components/fileEditor/AddFileCard';
import FilePickerModal from '@app/components/shared/FilePickerModal';
import { FileId, StirlingFile } from '@app/types/fileContext';
import { alert } from '@app/components/toast';
import { downloadBlob } from '@app/utils/downloadUtils';
import { useFileEditorRightRailButtons } from '@app/components/fileEditor/fileEditorRightRailButtons';
interface FileEditorProps {
@@ -1,7 +1,7 @@
import React, { useState, useCallback, useRef, useMemo } from 'react';
import { Text, ActionIcon, CheckboxIndicator, Tooltip, Modal, Button, Group, Stack } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { alert } from '../toast';
import { alert } from '@app/components/toast';
import { useTranslation } from 'react-i18next';
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
import CloseIcon from '@mui/icons-material/Close';
@@ -11,16 +11,16 @@ import PushPinIcon from '@mui/icons-material/PushPin';
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { StirlingFileStub } from '../../types/fileContext';
import { zipFileService } from '../../services/zipFileService';
import { StirlingFileStub } from '@app/types/fileContext';
import { zipFileService } from '@app/services/zipFileService';
import styles from './FileEditor.module.css';
import { useFileContext } from '../../contexts/FileContext';
import { useFileState } from '../../contexts/file/fileHooks';
import { FileId } from '../../types/file';
import { formatFileSize } from '../../utils/fileUtils';
import ToolChain from '../shared/ToolChain';
import HoverActionMenu, { HoverAction } from '../shared/HoverActionMenu';
import styles from '@app/components/fileEditor/FileEditor.module.css';
import { useFileContext } from '@app/contexts/FileContext';
import { useFileState } from '@app/contexts/file/fileHooks';
import { FileId } from '@app/types/file';
import { formatFileSize } from '@app/utils/fileUtils';
import ToolChain from '@app/components/shared/ToolChain';
import HoverActionMenu, { HoverAction } from '@app/components/shared/HoverActionMenu';
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useRightRailButtons, RightRailButtonWithAction } from '../../hooks/useRightRailButtons';
import LocalIcon from '../shared/LocalIcon';
import { useRightRailButtons, RightRailButtonWithAction } from '@app/hooks/useRightRailButtons';
import LocalIcon from '@app/components/shared/LocalIcon';
interface FileEditorRightRailButtonsParams {
totalItems: number;
@@ -4,8 +4,8 @@ import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { useTranslation } from 'react-i18next';
import { getFileSize } from '../../utils/fileUtils';
import { StirlingFileStub } from '../../types/fileContext';
import { getFileSize } from '@app/utils/fileUtils';
import { StirlingFileStub } from '@app/types/fileContext';
interface CompactFileDetailsProps {
currentFile: StirlingFileStub | null;
@@ -1,12 +1,12 @@
import React from 'react';
import { Grid } from '@mantine/core';
import FileSourceButtons from './FileSourceButtons';
import FileDetails from './FileDetails';
import SearchInput from './SearchInput';
import FileListArea from './FileListArea';
import FileActions from './FileActions';
import HiddenFileInput from './HiddenFileInput';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import FileSourceButtons from '@app/components/fileManager/FileSourceButtons';
import FileDetails from '@app/components/fileManager/FileDetails';
import SearchInput from '@app/components/fileManager/SearchInput';
import FileListArea from '@app/components/fileManager/FileListArea';
import FileActions from '@app/components/fileManager/FileActions';
import HiddenFileInput from '@app/components/fileManager/HiddenFileInput';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
const DesktopLayout: React.FC = () => {
const {
@@ -2,9 +2,9 @@ import React, { useState } from 'react';
import { Button, Group, Text, Stack, useMantineColorScheme } from '@mantine/core';
import HistoryIcon from '@mui/icons-material/History';
import { useTranslation } from 'react-i18next';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import LocalIcon from '../shared/LocalIcon';
import { BASE_PATH } from '../../constants/app';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import LocalIcon from '@app/components/shared/LocalIcon';
import { BASE_PATH } from '@app/constants/app';
const EmptyFilesState: React.FC = () => {
const { t } = useTranslation();
@@ -4,7 +4,7 @@ import SelectAllIcon from "@mui/icons-material/SelectAll";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import { useTranslation } from "react-i18next";
import { useFileManagerContext } from "../../contexts/FileManagerContext";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
const FileActions: React.FC = () => {
const { t } = useTranslation();
@@ -1,11 +1,11 @@
import React, { useEffect, useState } from 'react';
import { Stack, Button, Box } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import FilePreview from '../shared/FilePreview';
import FileInfoCard from './FileInfoCard';
import CompactFileDetails from './CompactFileDetails';
import { useIndexedDBThumbnail } from '@app/hooks/useIndexedDBThumbnail';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import FilePreview from '@app/components/shared/FilePreview';
import FileInfoCard from '@app/components/fileManager/FileInfoCard';
import CompactFileDetails from '@app/components/fileManager/CompactFileDetails';
interface FileDetailsProps {
compact?: boolean;
@@ -1,8 +1,8 @@
import React from 'react';
import { Box, Text, Collapse, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { StirlingFileStub } from '../../types/fileContext';
import FileListItem from './FileListItem';
import { StirlingFileStub } from '@app/types/fileContext';
import FileListItem from '@app/components/fileManager/FileListItem';
interface FileHistoryGroupProps {
leafFile: StirlingFileStub;
@@ -1,9 +1,9 @@
import React from 'react';
import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { detectFileExtension, getFileSize } from '../../utils/fileUtils';
import { StirlingFileStub } from '../../types/fileContext';
import ToolChain from '../shared/ToolChain';
import { detectFileExtension, getFileSize } from '@app/utils/fileUtils';
import { StirlingFileStub } from '@app/types/fileContext';
import ToolChain from '@app/components/shared/ToolChain';
interface FileInfoCardProps {
currentFile: StirlingFileStub | null;
@@ -2,10 +2,10 @@ import React from 'react';
import { Center, ScrollArea, Text, Stack } from '@mantine/core';
import CloudIcon from '@mui/icons-material/Cloud';
import { useTranslation } from 'react-i18next';
import FileListItem from './FileListItem';
import FileHistoryGroup from './FileHistoryGroup';
import EmptyFilesState from './EmptyFilesState';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import FileListItem from '@app/components/fileManager/FileListItem';
import FileHistoryGroup from '@app/components/fileManager/FileHistoryGroup';
import EmptyFilesState from '@app/components/fileManager/EmptyFilesState';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
interface FileListAreaProps {
scrollAreaHeight: string;
@@ -7,12 +7,12 @@ import HistoryIcon from '@mui/icons-material/History';
import RestoreIcon from '@mui/icons-material/Restore';
import UnarchiveIcon from '@mui/icons-material/Unarchive';
import { useTranslation } from 'react-i18next';
import { getFileSize, getFileDate } from '../../utils/fileUtils';
import { FileId, StirlingFileStub } from '../../types/fileContext';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import { zipFileService } from '../../services/zipFileService';
import ToolChain from '../shared/ToolChain';
import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '../../styles/zIndex';
import { getFileSize, getFileDate } from '@app/utils/fileUtils';
import { FileId, StirlingFileStub } from '@app/types/fileContext';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import { zipFileService } from '@app/services/zipFileService';
import ToolChain from '@app/components/shared/ToolChain';
import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex';
interface FileListItemProps {
file: StirlingFileStub;
@@ -4,8 +4,8 @@ import HistoryIcon from '@mui/icons-material/History';
import UploadIcon from '@mui/icons-material/Upload';
import CloudIcon from '@mui/icons-material/Cloud';
import { useTranslation } from 'react-i18next';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import { useGoogleDrivePicker } from '../../hooks/useGoogleDrivePicker';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import { useGoogleDrivePicker } from '@app/hooks/useGoogleDrivePicker';
interface FileSourceButtonsProps {
horizontal?: boolean;
@@ -1,5 +1,5 @@
import React from 'react';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
const HiddenFileInput: React.FC = () => {
const { fileInputRef, onFileInputChange } = useFileManagerContext();
@@ -1,12 +1,12 @@
import React from 'react';
import { Box } from '@mantine/core';
import FileSourceButtons from './FileSourceButtons';
import FileDetails from './FileDetails';
import SearchInput from './SearchInput';
import FileListArea from './FileListArea';
import FileActions from './FileActions';
import HiddenFileInput from './HiddenFileInput';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import FileSourceButtons from '@app/components/fileManager/FileSourceButtons';
import FileDetails from '@app/components/fileManager/FileDetails';
import SearchInput from '@app/components/fileManager/SearchInput';
import FileListArea from '@app/components/fileManager/FileListArea';
import FileActions from '@app/components/fileManager/FileActions';
import HiddenFileInput from '@app/components/fileManager/HiddenFileInput';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
const MobileLayout: React.FC = () => {
const {
@@ -2,7 +2,7 @@ import React from 'react';
import { TextInput } from '@mantine/core';
import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
interface SearchInputProps {
style?: React.CSSProperties;
@@ -1,6 +1,6 @@
import React from 'react';
import { HotkeyBinding } from '../../utils/hotkeys';
import { useHotkeys } from '../../contexts/HotkeyContext';
import { HotkeyBinding } from '@app/utils/hotkeys';
import { useHotkeys } from '@app/contexts/HotkeyContext';
interface HotkeyDisplayProps {
binding: HotkeyBinding | null | undefined;
@@ -1,23 +1,22 @@
import React from 'react';
import { Box } from '@mantine/core';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useFileHandler } from '../../hooks/useFileHandler';
import { useFileState } from '../../contexts/FileContext';
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
import { isBaseWorkbench } from '../../types/workbench';
import { useViewer } from '../../contexts/ViewerContext';
import { useAppConfig } from '../../hooks/useAppConfig';
import './Workbench.css';
import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider';
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
import { useFileHandler } from '@app/hooks/useFileHandler';
import { useFileState } from '@app/contexts/FileContext';
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
import { isBaseWorkbench } from '@app/types/workbench';
import { useViewer } from '@app/contexts/ViewerContext';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import '@app/components/layout/Workbench.css';
import TopControls from '../shared/TopControls';
import FileEditor from '../fileEditor/FileEditor';
import PageEditor from '../pageEditor/PageEditor';
import PageEditorControls from '../pageEditor/PageEditorControls';
import Viewer from '../viewer/Viewer';
import LandingPage from '../shared/LandingPage';
import Footer from '../shared/Footer';
import DismissAllErrorsButton from '../shared/DismissAllErrorsButton';
import TopControls from '@app/components/shared/TopControls';
import FileEditor from '@app/components/fileEditor/FileEditor';
import PageEditor from '@app/components/pageEditor/PageEditor';
import PageEditorControls from '@app/components/pageEditor/PageEditorControls';
import Viewer from '@app/components/viewer/Viewer';
import LandingPage from '@app/components/shared/LandingPage';
import Footer from '@app/components/shared/Footer';
import DismissAllErrorsButton from '@app/components/shared/DismissAllErrorsButton';
// No props needed - component uses contexts directly
export default function Workbench() {
@@ -192,7 +191,7 @@ export default function Workbench() {
</Box>
<Footer
analyticsEnabled={config?.enableAnalytics}
analyticsEnabled={config?.enableAnalytics === true}
termsAndConditions={config?.termsAndConditions}
privacyPolicy={config?.privacyPolicy}
cookiePolicy={config?.cookiePolicy}
@@ -1,14 +1,14 @@
import React from "react";
import { TourProvider, useTour, type StepType } from '@reactour/tour';
import { useOnboarding } from '../../contexts/OnboardingContext';
import { useOnboarding } from '@app/contexts/OnboardingContext';
import { useTranslation } from 'react-i18next';
import { CloseButton, ActionIcon } from '@mantine/core';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import { useTourOrchestration } from '../../contexts/TourOrchestrationContext';
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import CheckIcon from '@mui/icons-material/Check';
import TourWelcomeModal from './TourWelcomeModal';
import './OnboardingTour.css';
import TourWelcomeModal from '@app/components/onboarding/TourWelcomeModal';
import '@app/components/onboarding/OnboardingTour.css';
// Enum case order defines order steps will appear
enum TourStep {
@@ -1,6 +1,6 @@
import { Modal, Title, Text, Button, Stack, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex';
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
interface TourWelcomeModalProps {
opened: boolean;
@@ -1,9 +1,9 @@
import { useState, useEffect } from 'react';
import classes from './bulkSelectionPanel/BulkSelectionPanel.module.css';
import { parseSelectionWithDiagnostics } from '../../utils/bulkselection/parseSelection';
import PageSelectionInput from './bulkSelectionPanel/PageSelectionInput';
import SelectedPagesDisplay from './bulkSelectionPanel/SelectedPagesDisplay';
import AdvancedSelectionPanel from './bulkSelectionPanel/AdvancedSelectionPanel';
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
import { parseSelectionWithDiagnostics } from '@app/utils/bulkselection/parseSelection';
import PageSelectionInput from '@app/components/pageEditor/bulkSelectionPanel/PageSelectionInput';
import SelectedPagesDisplay from '@app/components/pageEditor/bulkSelectionPanel/SelectedPagesDisplay';
import AdvancedSelectionPanel from '@app/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel';
interface BulkSelectionPanelProps {
csvInput: string;
@@ -1,7 +1,7 @@
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Box } from '@mantine/core';
import { useVirtualizer } from '@tanstack/react-virtual';
import { GRID_CONSTANTS } from './constants';
import { GRID_CONSTANTS } from '@app/components/pageEditor/constants';
interface DragDropItem {
id: string;
@@ -9,9 +9,9 @@ import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import styles from './PageEditor.module.css';
import { useFileContext } from '../../contexts/FileContext';
import { FileId } from '../../types/file';
import styles from '@app/components/pageEditor/PageEditor.module.css';
import { useFileContext } from '@app/contexts/FileContext';
import { FileId } from '@app/types/file';
interface FileItem {
id: FileId;
@@ -1,19 +1,19 @@
import { useState, useCallback, useRef, useEffect } from "react";
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
import { useFileState, useFileActions } from "../../contexts/FileContext";
import { useNavigationGuard } from "../../contexts/NavigationContext";
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
import { pdfExportService } from "../../services/pdfExportService";
import { documentManipulationService } from "../../services/documentManipulationService";
import { exportProcessedDocumentsToFiles } from "../../services/pdfExportHelpers";
import { createStirlingFilesAndStubs } from "../../services/fileStubHelpers";
import { useFileState, useFileActions } from "@app/contexts/FileContext";
import { useNavigationGuard } from "@app/contexts/NavigationContext";
import { PDFDocument, PageEditorFunctions } from "@app/types/pageEditor";
import { pdfExportService } from "@app/services/pdfExportService";
import { documentManipulationService } from "@app/services/documentManipulationService";
import { exportProcessedDocumentsToFiles } from "@app/services/pdfExportHelpers";
import { createStirlingFilesAndStubs } from "@app/services/fileStubHelpers";
// Thumbnail generation is now handled by individual PageThumbnail components
import './PageEditor.module.css';
import PageThumbnail from './PageThumbnail';
import DragDropGrid from './DragDropGrid';
import SkeletonLoader from '../shared/SkeletonLoader';
import NavigationWarningModal from '../shared/NavigationWarningModal';
import { FileId } from "../../types/file";
import '@app/components/pageEditor/PageEditor.module.css';
import PageThumbnail from '@app/components/pageEditor/PageThumbnail';
import DragDropGrid from '@app/components/pageEditor/DragDropGrid';
import SkeletonLoader from '@app/components/shared/SkeletonLoader';
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
import { FileId } from "@app/types/file";
import {
DeletePagesCommand,
@@ -22,12 +22,12 @@ import {
BulkRotateCommand,
PageBreakCommand,
UndoManager
} from './commands/pageCommands';
import { GRID_CONSTANTS } from './constants';
import { usePageDocument } from './hooks/usePageDocument';
import { usePageEditorState } from './hooks/usePageEditorState';
import { parseSelection } from "../../utils/bulkselection/parseSelection";
import { usePageEditorRightRailButtons } from "./pageEditorRightRailButtons";
} from '@app/components/pageEditor/commands/pageCommands';
import { GRID_CONSTANTS } from '@app/components/pageEditor/constants';
import { usePageDocument } from '@app/components/pageEditor/hooks/usePageDocument';
import { usePageEditorState } from '@app/components/pageEditor/hooks/usePageEditorState';
import { parseSelection } from "@app/utils/bulkselection/parseSelection";
import { usePageEditorRightRailButtons } from "@app/components/pageEditor/pageEditorRightRailButtons";
export interface PageEditorProps {
onFunctionsReady?: (functions: PageEditorFunctions) => void;
@@ -1,7 +1,7 @@
import { ActionIcon, Popover } from '@mantine/core';
import LocalIcon from '../shared/LocalIcon';
import { Tooltip } from '../shared/Tooltip';
import BulkSelectionPanel from './BulkSelectionPanel';
import LocalIcon from '@app/components/shared/LocalIcon';
import { Tooltip } from '@app/components/shared/Tooltip';
import BulkSelectionPanel from '@app/components/pageEditor/BulkSelectionPanel';
interface PageSelectByNumberButtonProps {
disabled: boolean;
@@ -9,11 +9,11 @@ import DeleteIcon from '@mui/icons-material/Delete';
import ContentCutIcon from '@mui/icons-material/ContentCut';
import AddIcon from '@mui/icons-material/Add';
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { PDFPage, PDFDocument } from '../../types/pageEditor';
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import styles from './PageEditor.module.css';
import HoverActionMenu, { HoverAction } from '../shared/HoverActionMenu';
import { PDFPage, PDFDocument } from '@app/types/pageEditor';
import { useThumbnailGeneration } from '@app/hooks/useThumbnailGeneration';
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
import styles from '@app/components/pageEditor/PageEditor.module.css';
import HoverActionMenu, { HoverAction } from '@app/components/shared/HoverActionMenu';
interface PageThumbnailProps {
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Flex } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import classes from './BulkSelectionPanel.module.css';
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
import {
appendExpression,
insertOperatorSmart,
@@ -10,9 +10,9 @@ import {
everyNthExpression,
rangeExpression,
LogicalOperator,
} from './BulkSelection';
import SelectPages from './SelectPages';
import OperatorsSection from './OperatorsSection';
} from '@app/components/pageEditor/bulkSelectionPanel/BulkSelection';
import SelectPages from '@app/components/pageEditor/bulkSelectionPanel/SelectPages';
import OperatorsSection from '@app/components/pageEditor/bulkSelectionPanel/OperatorsSection';
interface AdvancedSelectionPanelProps {
csvInput: string;
@@ -1,7 +1,7 @@
import { Button, Text, Group, Divider } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import classes from './BulkSelectionPanel.module.css';
import { LogicalOperator } from './BulkSelection';
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
import { LogicalOperator } from '@app/components/pageEditor/bulkSelectionPanel/BulkSelection';
interface OperatorsSectionProps {
csvInput: string;
@@ -1,9 +1,9 @@
import { TextInput, Button, Text, Flex, Switch } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LocalIcon from '../../shared/LocalIcon';
import { Tooltip } from '../../shared/Tooltip';
import { usePageSelectionTips } from '../../tooltips/usePageSelectionTips';
import classes from './BulkSelectionPanel.module.css';
import LocalIcon from '@app/components/shared/LocalIcon';
import { Tooltip } from '@app/components/shared/Tooltip';
import { usePageSelectionTips } from '@app/components/tooltips/usePageSelectionTips';
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
interface PageSelectionInputProps {
csvInput: string;
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Button, Text, NumberInput, Group } from '@mantine/core';
import classes from './BulkSelectionPanel.module.css';
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
interface SelectPagesProps {
title: string;
@@ -1,5 +1,5 @@
import { Text } from '@mantine/core';
import classes from './BulkSelectionPanel.module.css';
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
interface SelectedPagesDisplayProps {
selectedPageIds: string[];
@@ -1,5 +1,5 @@
import { FileId } from '../../../types/file';
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
import { FileId } from '@app/types/file';
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
// V1-style DOM-first command system (replaces the old React state commands)
export abstract class DOMCommand {
@@ -694,7 +694,7 @@ export class InsertFilesCommand extends DOMCommand {
private async generateThumbnailsForInsertedPages(updatedDocument: PDFDocument): Promise<void> {
try {
const { thumbnailGenerationService } = await import('../../../services/thumbnailGenerationService');
const { thumbnailGenerationService } = await import('@app/services/thumbnailGenerationService');
// Group pages by file ID to generate thumbnails efficiently
const pagesByFileId = new Map<FileId, PDFPage[]>();
@@ -776,7 +776,7 @@ export class InsertFilesCommand extends DOMCommand {
const clonedArrayBuffer = arrayBuffer.slice(0);
// Use PDF.js via the worker manager to extract pages
const { pdfWorkerManager } = await import('../../../services/pdfWorkerManager');
const { pdfWorkerManager } = await import('@app/services/pdfWorkerManager');
const pdf = await pdfWorkerManager.createDocument(clonedArrayBuffer);
const pageCount = pdf.numPages;
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useFileState } from '../../../contexts/FileContext';
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
import { FileId } from '../../../types/file';
import { useFileState } from '@app/contexts/FileContext';
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
import { FileId } from '@app/types/file';
export interface PageDocumentHook {
document: PDFDocument | null;
@@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useRightRailButtons, RightRailButtonWithAction } from '../../hooks/useRightRailButtons';
import LocalIcon from '../shared/LocalIcon';
import PageSelectByNumberButton from './PageSelectByNumberButton';
import { useRightRailButtons, RightRailButtonWithAction } from '@app/hooks/useRightRailButtons';
import LocalIcon from '@app/components/shared/LocalIcon';
import PageSelectByNumberButton from '@app/components/pageEditor/PageSelectByNumberButton';
interface PageEditorRightRailButtonsParams {
totalPages: number;
@@ -0,0 +1,112 @@
import { Modal, Stack, Button, Text, Title, Anchor } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { Z_ANALYTICS_MODAL } from '@app/styles/zIndex';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import apiClient from '@app/services/apiClient';
interface AdminAnalyticsChoiceModalProps {
opened: boolean;
onClose: () => void;
}
export default function AdminAnalyticsChoiceModal({ opened, onClose }: AdminAnalyticsChoiceModalProps) {
const { t } = useTranslation();
const { refetch } = useAppConfig();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleChoice = async (enableAnalytics: boolean) => {
setLoading(true);
setError(null);
try {
const formData = new FormData();
formData.append('enabled', enableAnalytics.toString());
await apiClient.post('/api/v1/settings/update-enable-analytics', formData);
// Refetch config to apply new settings without page reload
await refetch();
// Close the modal after successful save
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error occurred');
setLoading(false);
}
};
const handleEnable = () => {
handleChoice(true);
};
const handleDisable = () => {
handleChoice(false);
};
return (
<Modal
opened={opened}
onClose={() => {}} // Prevent closing
closeOnClickOutside={false}
closeOnEscape={false}
withCloseButton={false}
size="lg"
centered
zIndex={Z_ANALYTICS_MODAL}
>
<Stack gap="md">
<Title order={2}>{t('analytics.title', 'Do you want make Stirling PDF better?')}</Title>
<Text size="sm" c="dimmed">
{t('analytics.paragraph1', 'Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.')}
</Text>
<Text size="sm" c="dimmed">
{t('analytics.paragraph2', 'Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.')}{' '}
<Anchor
href="https://docs.stirlingpdf.com/analytics-telemetry"
target="_blank"
rel="noopener noreferrer"
size="sm"
>
{t('analytics.learnMore', 'Learn more')}
</Anchor>
</Text>
{error && (
<Text c="red" size="sm">
{error}
</Text>
)}
<Stack gap="sm">
<Button
onClick={handleEnable}
loading={loading}
fullWidth
size="md"
>
{t('analytics.enable', 'Enable analytics')}
</Button>
<Button
onClick={handleDisable}
loading={loading}
fullWidth
size="md"
variant="subtle"
c="gray"
>
{t('analytics.disable', 'Disable analytics')}
</Button>
</Stack>
<Text size="xs" c="dimmed" ta="center">
{t('analytics.settings', 'You can change the settings for analytics in the config/settings.yml file')}
</Text>
</Stack>
</Modal>
);
}
@@ -1,11 +1,11 @@
import React from 'react';
import { ActionIcon } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { Tooltip } from './Tooltip';
import { Tooltip } from '@app/components/shared/Tooltip';
import AppsIcon from '@mui/icons-material/AppsRounded';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useSidebarNavigation } from '../../hooks/useSidebarNavigation';
import { handleUnlessSpecialClick } from '../../utils/clickHandlers';
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
interface AllToolsNavButtonProps {
activeButton: string;
@@ -1,13 +1,13 @@
import React, { useMemo, useState, useEffect } from 'react';
import { Modal, Text, ActionIcon } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import LocalIcon from './LocalIcon';
import Overview from './config/configSections/Overview';
import { createConfigNavSections } from './config/configNavSections';
import { NavKey } from './config/types';
import { useAppConfig } from '../../hooks/useAppConfig';
import './AppConfigModal.css';
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex';
import LocalIcon from '@app/components/shared/LocalIcon';
import Overview from '@app/components/shared/config/configSections/Overview';
import { createConfigNavSections } from '@app/components/shared/config/configNavSections';
import { NavKey } from '@app/components/shared/config/types';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import '@app/components/shared/AppConfigModal.css';
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
interface AppConfigModalProps {
opened: boolean;
@@ -1,7 +1,7 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import ButtonSelector from './ButtonSelector';
import ButtonSelector from '@app/components/shared/ButtonSelector';
// Wrapper component to provide Mantine context
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
@@ -1,5 +1,5 @@
import { Button, Group, Stack, Text } from "@mantine/core";
import FitText from "./FitText";
import FitText from "@app/components/shared/FitText";
export interface ButtonOption<T> {
value: T;
@@ -1,5 +1,5 @@
import { Stack, Card, Text, Flex } from '@mantine/core';
import { Tooltip } from './Tooltip';
import { Tooltip } from '@app/components/shared/Tooltip';
import { useTranslation } from 'react-i18next';
export interface CardOption<T = string> {
@@ -1,8 +1,8 @@
import React from 'react';
import { Button, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useFileState } from '../../contexts/FileContext';
import { useFileActions } from '../../contexts/file/fileHooks';
import { useFileState } from '@app/contexts/FileContext';
import { useFileActions } from '@app/contexts/file/fileHooks';
import CloseIcon from '@mui/icons-material/Close';
import { Z_INDEX_TOAST } from '../../styles/zIndex';
@@ -6,9 +6,9 @@ import StorageIcon from "@mui/icons-material/Storage";
import VisibilityIcon from "@mui/icons-material/Visibility";
import EditIcon from "@mui/icons-material/Edit";
import { StirlingFileStub } from "../../types/fileContext";
import { getFileSize, getFileDate } from "../../utils/fileUtils";
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
import { StirlingFileStub } from "@app/types/fileContext";
import { getFileSize, getFileDate } from "@app/utils/fileUtils";
import { useIndexedDBThumbnail } from "@app/hooks/useIndexedDBThumbnail";
interface FileCardProps {
file: File;
@@ -2,7 +2,7 @@ import React from 'react';
import { Menu, Loader, Group, Text } from '@mantine/core';
import VisibilityIcon from '@mui/icons-material/Visibility';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import FitText from './FitText';
import FitText from '@app/components/shared/FitText';
interface FileDropdownMenuProps {
displayName: string;
@@ -3,9 +3,9 @@ import { Box, Flex, Group, Text, Button, TextInput, Select } from "@mantine/core
import { useTranslation } from "react-i18next";
import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort";
import FileCard from "./FileCard";
import { StirlingFileStub } from "../../types/fileContext";
import { FileId } from "../../types/file";
import FileCard from "@app/components/shared/FileCard";
import { StirlingFileStub } from "@app/types/fileContext";
import { FileId } from "@app/types/file";
interface FileGridProps {
files: Array<{ file: File; record?: StirlingFileStub }>;
@@ -15,7 +15,7 @@ import {
} from '@mantine/core';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { useTranslation } from 'react-i18next';
import { FileId } from '../../types/file';
import { FileId } from '@app/types/file';
interface FilePickerModalProps {
opened: boolean;
@@ -1,11 +1,11 @@
import React from 'react';
import { Box, Center } from '@mantine/core';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import { StirlingFileStub } from '../../types/fileContext';
import DocumentThumbnail from './filePreview/DocumentThumbnail';
import DocumentStack from './filePreview/DocumentStack';
import HoverOverlay from './filePreview/HoverOverlay';
import NavigationArrows from './filePreview/NavigationArrows';
import { StirlingFileStub } from '@app/types/fileContext';
import DocumentThumbnail from '@app/components/shared/filePreview/DocumentThumbnail';
import DocumentStack from '@app/components/shared/filePreview/DocumentStack';
import HoverOverlay from '@app/components/shared/filePreview/HoverOverlay';
import NavigationArrows from '@app/components/shared/filePreview/NavigationArrows';
export interface FilePreviewProps {
// Core file data
@@ -1,5 +1,5 @@
import React, { CSSProperties, useMemo, useRef } from 'react';
import { useAdjustFontSizeToFit } from './fitText/textFit';
import { useAdjustFontSizeToFit } from '@app/components/shared/fitText/textFit';
type FitTextProps = {
text: string;
@@ -1,6 +1,6 @@
import { Flex } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useCookieConsent } from '../../hooks/useCookieConsent';
import { useCookieConsent } from '@app/hooks/useCookieConsent';
interface FooterProps {
privacyPolicy?: string;

Some files were not shown because too many files have changed in this diff Show More