Compare commits

...
Author SHA1 Message Date
Anthony Stirling 62ac0370ac Refine mobile workspace slider and dropzone layout 2025-09-30 12:58:50 +01:00
Anthony Stirling c35c66abbc Fix mobile view toggles and restore desktop dropzone width 2025-09-30 12:48:51 +01:00
Anthony Stirling 9a4afae13f Refine mobile workspace toggle behavior 2025-09-30 12:33:25 +01:00
Anthony Stirling b1443fb0da Tighten landing page file picker layout 2025-09-30 12:13:24 +01:00
Anthony Stirling d854497266 Refine mobile header branding 2025-09-30 12:13:15 +01:00
Anthony Stirling 57ab30d1a6 Fix mobile files button click handler 2025-09-30 11:47:40 +01:00
Anthony Stirling 7dbf529a45 feat: add mobile slider layout for home page 2025-09-30 11:36:28 +01:00
02189a67bd refactor(frontend): remove unused React default imports (#4529)
## Description of Changes

- Removed unused `React` default imports across multiple frontend
components.
- Updated imports to only include required React hooks and types (e.g.,
`useState`, `useEffect`, `Suspense`, `createContext`).
- Ensured consistency with React 17+ JSX transform, where default
`React` import is no longer required.
- This cleanup reduces bundle size slightly and aligns code with modern
React best practices.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-09-29 13:01:09 +01:00
d4985f57d4 style(frontend): standardize semicolons across TS/JS configs and components (#4525)
# Description of Changes

- **What was changed**
- Added missing trailing semicolons across React components, utilities,
tests, and build/test configs to ensure consistent formatting.
- Normalized arrow-function assignments to end with semicolons (e.g.,
`const fn = () => { ... };`).
- Harmonized imports/exports and object literals in configuration files
to terminate statements with semicolons.
  - Updated test setup files and mocks to consistently use semicolons.

- **Why the change was made**
- Aligns the codebase with ESLint/Prettier conventions to prevent
auto-format churn and avoid ASI (automatic semicolon insertion) edge
cases.
- Improves readability and produces cleaner diffs in future
contributions.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-09-29 12:55:53 +01:00
4ab66fdf14 feat(frontend): refactor ToolStep props handling and children usage (#4524)
# Description of Changes

- Replaced `children` being passed as a prop to `React.createElement`
with proper usage as additional arguments (fixes
`react/no-children-prop` warning).
- Added stricter handling of `isVisible` and `_excludeFromCount` props
with improved variable naming (`stepProps`) for clarity.
- Refactored JSX structure for collapsed/expanded rendering logic to
improve readability.

This change was made to clean up prop handling, remove ESLint warnings,
and make the component more consistent with React best practices.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-09-29 12:51:42 +01:00
c19abe0da7 refactor(frontend): add display names for forwardRef components (#4523)
# Description of Changes

- Added `displayName` properties to `QuickAccessBar` and `TextInput`
components.
- This improves debugging and React DevTools readability by ensuring
components have clear, identifiable names instead of anonymous
`ForwardRef`.
- Minor formatting cleanup in `QuickAccessBar` for consistency.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-09-29 12:48:35 +01:00
dd6b7968db refactor(types): deduplicate AutomateParameters definition in automation types (#4522)
# Description of Changes

- Removed duplicate `AutomateParameters` interface from
`frontend/src/types/automation.ts`
- The interface was already defined earlier in the same file, leading to
redundancy
- Keeps type definitions consistent and avoids confusion

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-09-29 12:45:37 +01:00
LudyandGitHub 2228ae7197 ci(frontend): update licenses workflow dependencies and Node.js version (#4520)
# Description of Changes

- Added the workflow file itself
(`.github/workflows/frontend-licenses-update.yml`) to the trigger paths.
- Updated `step-security/harden-runner` from **v2.12.2** → **v2.13.1**.
- Bumped `actions/checkout` from **v4.2.2** → **v5.0.0**.  
- Upgraded `actions/setup-node` from **v4.1.0** (Node.js 18) →
**v5.0.0** (Node.js 22).
- Updated `actions/github-script` from **v7.0.1** → **v8.0.0**.  

These changes modernize the workflow, ensure compatibility with newer
Node.js versions, and keep GitHub Actions up to date.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-09-29 12:21:48 +01:00
Reece BrowneandGitHub 30987dcad2 Dockerfile package (#4517) 2025-09-26 20:56:26 +01:00
70 changed files with 585 additions and 245 deletions
@@ -12,6 +12,7 @@ on:
branches:
- V2
paths:
- ".github/workflows/frontend-licenses-update.yml"
- "frontend/package.json"
- "frontend/package-lock.json"
- "frontend/scripts/generate-licenses.js"
@@ -28,12 +29,12 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout PR head (default)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
persist-credentials: false
@@ -48,7 +49,7 @@ jobs:
- name: Checkout BASE branch (safe script)
if: github.event_name == 'pull_request'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
@@ -56,9 +57,9 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: '18'
node-version: '22'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
@@ -114,7 +115,7 @@ jobs:
# PR Event: Check licenses and comment on PR
- name: Delete previous license check comments
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -167,7 +168,7 @@ jobs:
- name: Comment on PR - License Check Results
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
+1 -1
View File
@@ -4,7 +4,7 @@ FROM node:20-alpine AS build
WORKDIR /app
# Copy package files
COPY frontend/package*.json ./
COPY frontend/package.json frontend/package-lock.json ./
# Install dependencies
RUN npm ci
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { Suspense } from "react";
import { Suspense } from "react";
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
import { FileContextProvider } from "./contexts/FileContext";
import { NavigationProvider } from "./contexts/NavigationContext";
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import {
Modal,
Text,
@@ -1,4 +1,4 @@
import React, { useRef } from "react";
import { useRef } from "react";
import { FileButton, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
@@ -1,5 +1,4 @@
import { Flex } from '@mantine/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCookieConsent } from '../../hooks/useCookieConsent';
@@ -0,0 +1,101 @@
.landing-dropzone {
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
width: min(92%, 32rem);
max-width: calc(100% - 1.5rem);
height: calc(100% - 1rem);
border-radius: 0.25rem 0.25rem 0 0;
display: flex;
align-items: center;
justify-content: center;
filter: var(--drop-shadow-filter);
background-color: var(--landing-paper-bg);
transition: background-color 0.4s ease;
}
.landing-dropzone__sheet {
position: relative;
width: min(100%, 26rem);
max-width: min(100%, 26rem);
margin: 0 auto;
padding: clamp(1.5rem, 4vw, 2.25rem);
border-radius: 0.5rem;
border: 1px solid var(--landing-inner-paper-border);
background-color: var(--landing-inner-paper-bg);
display: flex;
flex-direction: column;
align-items: center;
gap: clamp(1rem, 2.5vw, 1.5rem);
min-height: clamp(22rem, 45vh, 30rem);
box-sizing: border-box;
}
.landing-dropzone__badge {
position: absolute;
top: clamp(0.75rem, 2vw, 1.25rem);
right: clamp(0.75rem, 2vw, 1.25rem);
height: clamp(2rem, 4vw, 2.5rem);
width: auto;
pointer-events: none;
}
.landing-dropzone__body {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: clamp(1rem, 2.5vw, 1.5rem);
}
.landing-dropzone__brand {
justify-content: center;
}
.landing-dropzone__actions {
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
width: 100%;
max-width: min(100%, 22rem);
margin: clamp(0.5rem, 2vw, 0.9rem) auto;
}
.landing-dropzone__hint {
font-size: 0.8rem;
color: var(--accent-interactive);
text-align: center;
}
@media (max-width: 900px) {
.landing-dropzone {
width: min(94%, 28rem);
height: calc(100% - 0.75rem);
}
.landing-dropzone__sheet {
max-width: min(100%, 24rem);
padding: clamp(1.35rem, 5vw, 2rem);
min-height: clamp(20rem, 55vh, 28rem);
}
.landing-dropzone__actions {
max-width: min(100%, 18rem);
}
}
@media (min-width: 1100px) {
.landing-dropzone {
width: min(92%, 34rem);
}
.landing-dropzone__sheet {
max-width: min(100%, 30rem);
}
.landing-dropzone__actions {
max-width: min(100%, 24rem);
}
}
+10 -50
View File
@@ -7,6 +7,8 @@ import { useFileHandler } from '../../hooks/useFileHandler';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import { BASE_PATH } from '../../constants/app';
import './LandingPage.css';
const LandingPage = () => {
const { addFiles } = useFileHandler();
const fileInputRef = React.useRef<HTMLInputElement>(null);
@@ -43,17 +45,7 @@ const LandingPage = () => {
onDrop={handleFileDrop}
accept={["application/pdf", "application/zip", "application/x-zip-compressed"]}
multiple={true}
className="w-4/5 flex items-center justify-center h-[95%]"
style={{
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
bottom: 0,
borderRadius: '0.25rem 0.25rem 0 0',
filter: 'var(--drop-shadow-filter)',
backgroundColor: 'var(--landing-paper-bg)',
transition: 'background-color 0.4s ease',
}}
className="landing-dropzone"
activateOnClick={false}
styles={{
root: {
@@ -63,41 +55,18 @@ const LandingPage = () => {
},
}}
>
<div
style={{
position: 'absolute',
top: 0,
right: 0,
zIndex: 10,
}}
>
<div className="landing-dropzone__sheet dropzone-inner">
<img
className="landing-dropzone__badge"
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoNoTextDark.svg` : `${BASE_PATH}/branding/StirlingPDFLogoNoTextLight.svg`}
alt="Stirling PDF Logo"
style={{
height: 'auto',
pointerEvents: 'none',
}}
/>
</div>
<div
className={`min-h-[45vh] flex flex-col items-center justify-center px-8 py-8 w-full min-w-[30rem] max-w-[calc(100%-2rem)] border transition-all duration-200 dropzone-inner relative`}
style={{
borderRadius: '0.5rem',
backgroundColor: 'var(--landing-inner-paper-bg)',
borderColor: 'var(--landing-inner-paper-border)',
borderWidth: '1px',
borderStyle: 'solid',
}}
>
{/* Logo positioned absolutely in top right corner */}
{/* Centered content container */}
<div className="flex flex-col items-center gap-4 flex-none w-full">
<div className="landing-dropzone__body">
{/* Stirling PDF Branding */}
<Group gap="xs" align="center">
<Group gap="xs" align="center" className="landing-dropzone__brand">
<img
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
alt="Stirling PDF"
@@ -107,15 +76,7 @@ const LandingPage = () => {
{/* Add Files + Native Upload Buttons */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.6rem',
width: '80%',
marginTop: '0.8rem',
marginBottom: '0.8rem'
}}
className="landing-dropzone__actions"
onMouseLeave={() => setIsUploadHover(false)}
>
<Button
@@ -152,7 +113,7 @@ const LandingPage = () => {
border: '1px solid var(--landing-button-border)',
borderRadius: '1rem',
height: '38px',
width: isUploadHover ? 'calc(100% - 50px)' : '58px',
width: isUploadHover ? 'calc(100% - 58px - 0.6rem)' : '58px',
minWidth: '58px',
paddingLeft: isUploadHover ? '1rem' : 0,
paddingRight: isUploadHover ? '1rem' : 0,
@@ -187,8 +148,7 @@ const LandingPage = () => {
{/* Instruction Text */}
<span
className="text-[var(--accent-interactive)]"
style={{ fontSize: '.8rem' }}
className="landing-dropzone__hint"
>
{t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')}
</span>
@@ -1,4 +1,3 @@
import React from "react";
import { Box, Group, Text, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
@@ -37,7 +36,7 @@ const MultiSelectControls = ({
>
{t("fileManager.clearSelection", "Clear Selection")}
</Button>
{onAddToUpload && (
<Button
size="xs"
@@ -47,7 +46,7 @@ const MultiSelectControls = ({
{t("fileManager.addToUpload", "Add to Upload")}
</Button>
)}
{onOpenInFileEditor && (
<Button
size="xs"
@@ -58,7 +57,7 @@ const MultiSelectControls = ({
{t("fileManager.openInFileEditor", "Open in File Editor")}
</Button>
)}
{onOpenInPageEditor && (
<Button
size="xs"
@@ -69,7 +68,7 @@ const MultiSelectControls = ({
{t("fileManager.openInPageEditor", "Open in Page Editor")}
</Button>
)}
{onDeleteAll && (
<Button
size="xs"
@@ -85,4 +84,4 @@ const MultiSelectControls = ({
);
};
export default MultiSelectControls;
export default MultiSelectControls;
@@ -1,4 +1,3 @@
import React from 'react';
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
import { useNavigationGuard } from '../../contexts/NavigationContext';
import { useTranslation } from 'react-i18next';
@@ -41,10 +41,10 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
// Helper function to render navigation buttons with URL support
const renderNavButton = (config: ButtonConfig, index: number) => {
const isActive = isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView);
// Check if this button has URL navigation support
const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate')
? getToolNavigation(config.id)
const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate')
? getToolNavigation(config.id)
: null;
const handleClick = (e?: React.MouseEvent) => {
@@ -59,7 +59,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
return (
<div key={config.id} className="flex flex-col items-center gap-1" style={{ marginTop: index === 0 ? '0.5rem' : "0rem" }}>
<ActionIcon
{...(navProps ? {
{...(navProps ? {
component: "a" as const,
href: navProps.href,
onClick: (e: React.MouseEvent) => handleClick(e),
@@ -249,4 +249,6 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
);
});
QuickAccessBar.displayName = 'QuickAccessBar';
export default QuickAccessBar;
@@ -1,4 +1,4 @@
import React, { createContext, useContext, ReactNode } from 'react';
import { createContext, useContext, ReactNode } from 'react';
import { MantineProvider } from '@mantine/core';
import { useRainbowTheme } from '../../hooks/useRainbowTheme';
import { mantineTheme } from '../../theme/mantineTheme';
@@ -107,3 +107,5 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
</div>
);
});
TextInput.displayName = 'TextInput';
+1 -1
View File
@@ -32,7 +32,7 @@ const ToolChain: React.FC<ToolChainProps> = ({
const getToolName = (toolId: ToolId) => {
return t(`home.${toolId}.title`, toolId);
}
};
// Create full tool chain for tooltip
const fullChainDisplay = displayStyle === 'badges' ? (
@@ -15,7 +15,7 @@ const viewOptionStyle = {
gap: 6,
whiteSpace: 'nowrap',
paddingTop: '0.3rem',
}
};
// Build view options showing text always
@@ -50,7 +50,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
const animTimeoutRef = useRef<number | null>(null);
const replayRafRef = useRef<number | null>(null);
const isSwitchingToNewTool = () => { return prevKeyRef.current && prevKeyRef.current !== selectedToolKey };
const isSwitchingToNewTool = () => { return prevKeyRef.current && prevKeyRef.current !== selectedToolKey; };
const clearTimers = () => {
if (collapseTimeoutRef.current) {
@@ -81,7 +81,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
setReplayAnim(false);
animTimeoutRef.current = null;
}, 500);
}
};
const firstShow = () => {
clearTimers();
@@ -91,7 +91,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
animTimeoutRef.current = window.setTimeout(() => {
animTimeoutRef.current = null;
}, 500);
}
};
const triggerCollapse = () => {
clearTimers();
@@ -101,7 +101,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
prevKeyRef.current = null;
collapseTimeoutRef.current = null;
}, 500); // match CSS transition duration
}
};
useEffect(() => {
if (indicatorShouldShow) {
@@ -1,4 +1,3 @@
import React from 'react';
import { useToast } from './ToastContext';
import { ToastInstance, ToastLocation } from './types';
import { LocalIcon } from '../shared/LocalIcon';
@@ -66,7 +65,7 @@ export default function ToastRenderer() {
<LocalIcon icon={`material-symbols:${getDefaultIconName(t)}`} width={20} height={20} />
)}
</div>
{/* Title + count badge */}
<div className="toast-title-container">
<span>{t.title}</span>
@@ -74,7 +73,7 @@ export default function ToastRenderer() {
<span className="toast-count-badge">{t.count}</span>
)}
</div>
{/* Controls */}
<div className="toast-controls">
{t.expandable && (
@@ -101,20 +100,20 @@ export default function ToastRenderer() {
{/* Progress bar - always show when present */}
{typeof t.progress === 'number' && (
<div className="toast-progress-container">
<div
<div
className={getProgressBarClass(t)}
style={{ width: `${t.progress}%` }}
/>
</div>
)}
{/* Body content - only show when expanded */}
{(t.isExpanded || !t.expandable) && (
<div className="toast-body">
{t.body}
</div>
)}
{/* Button - always show when present, positioned below body */}
{t.buttonText && t.buttonCallback && (
<div className="toast-action-container">
@@ -10,5 +10,5 @@ export default function ToolLoadingFallback({ toolName }: { toolName?: string })
</Text>
</Stack>
</Center>
)
);
}
+6 -4
View File
@@ -8,6 +8,7 @@ import { useSidebarContext } from "../../contexts/SidebarContext";
import rainbowStyles from '../../styles/rainbow.module.css';
import { ScrollArea } from '@mantine/core';
import { ToolId } from '../../types/toolId';
import { useMediaQuery } from '@mantine/hooks';
// No props needed - component uses context
@@ -15,6 +16,7 @@ export default function ToolPanel() {
const { isRainbowMode } = useRainbowThemeContext();
const { sidebarRefs } = useSidebarContext();
const { toolPanelRef } = sidebarRefs;
const isMobile = useMediaQuery('(max-width: 1024px)');
// Use context-based hooks to eliminate prop drilling
@@ -34,17 +36,17 @@ export default function ToolPanel() {
<div
ref={toolPanelRef}
data-sidebar="tool-panel"
className={`h-screen flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
className={`flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
isRainbowMode ? rainbowStyles.rainbowPaper : ''
}`}
} ${isMobile ? 'h-full border-r-0' : 'h-screen'}`}
style={{
width: isPanelVisible ? '18.5rem' : '0',
width: isMobile ? '100%' : isPanelVisible ? '18.5rem' : '0',
padding: '0'
}}
>
<div
style={{
opacity: isPanelVisible ? 1 : 0,
opacity: isMobile || isPanelVisible ? 1 : 0,
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
height: '100%',
display: 'flex',
@@ -1,4 +1,4 @@
import React, { Suspense } from "react";
import { Suspense } from "react";
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
import { BaseToolProps } from "../../types/tool";
import ToolLoadingFallback from "./ToolLoadingFallback";
@@ -26,7 +26,7 @@ const ToolRenderer = ({
// Wrap lazy-loaded component with Suspense
return (
<Suspense fallback={<ToolLoadingFallback toolName={selectedTool.name} />}>
<Suspense fallback={<ToolLoadingFallback toolName={selectedTool.name} />}>
<ToolComponent
onPreviewFile={onPreviewFile}
onComplete={onComplete}
@@ -1,12 +1,11 @@
/**
* AddWatermarkSingleStepSettings - Used for automation only
*
*
* This component combines all watermark settings into a single step interface
* for use in the automation system. It includes type selection and all relevant
* settings in one unified component.
*/
import React from "react";
import { Stack } from "@mantine/core";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
import WatermarkTypeSettings from "./WatermarkTypeSettings";
@@ -67,4 +66,4 @@ const AddWatermarkSingleStepSettings = ({ parameters, onParameterChange, disable
);
};
export default AddWatermarkSingleStepSettings;
export default AddWatermarkSingleStepSettings;
@@ -1,4 +1,3 @@
import React from "react";
import { Stack, Checkbox, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
@@ -80,4 +79,4 @@ const WatermarkFormatting = ({ parameters, onParameterChange, disabled = false }
);
};
export default WatermarkFormatting;
export default WatermarkFormatting;
@@ -1,4 +1,3 @@
import React from "react";
import { Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
@@ -1,4 +1,3 @@
import React from "react";
import { Stack, Text, NumberInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
@@ -60,4 +59,4 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals
);
};
export default WatermarkStyleSettings;
export default WatermarkStyleSettings;
@@ -1,4 +1,3 @@
import React from "react";
import { Stack, Text, Select, ColorInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
@@ -27,7 +26,7 @@ const WatermarkTextStyle = ({ parameters, onParameterChange, disabled = false }:
format="hex"
/>
</Stack>
<Stack gap="xs">
<Text size="xs" fw={500}>
{t("watermark.settings.alphabet", "Alphabet")}
@@ -1,4 +1,3 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Title, Stack, Divider } from "@mantine/core";
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
@@ -19,11 +18,11 @@ interface AutomationSelectionProps {
toolRegistry: Record<string, ToolRegistryEntry>;
}
export default function AutomationSelection({
export default function AutomationSelection({
savedAutomations,
onCreateNew,
onRun,
onEdit,
onCreateNew,
onRun,
onEdit,
onDelete,
onCopyFromSuggested,
toolRegistry
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Stack, Text, Divider, Collapse, Button, NumberInput } from "@mantine/core";
import { BookletImpositionParameters } from "../../../hooks/tools/bookletImposition/useBookletImpositionParameters";
@@ -176,4 +176,4 @@ const BookletImpositionSettings = ({ parameters, onParameterChange, disabled = f
);
};
export default BookletImpositionSettings;
export default BookletImpositionSettings;
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters";
@@ -1,4 +1,3 @@
import React from 'react';
import { Stack, Text, NumberInput, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
@@ -1,4 +1,3 @@
import React from "react";
import { Stack, Text, Select, Switch } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { COLOR_TYPES, FIT_OPTIONS } from "../../../constants/convertConstants";
@@ -1,4 +1,3 @@
import React from 'react';
import { Stack, Text, NumberInput, Slider } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
@@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import { useMemo } from "react";
import { Stack, Text, Group, Divider, UnstyledButton, useMantineTheme, useMantineColorScheme } from "@mantine/core";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import { useTranslation } from "react-i18next";
@@ -1,4 +1,3 @@
import React from "react";
import { Stack, Text, Select, NumberInput, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { COLOR_TYPES, OUTPUT_OPTIONS } from "../../../constants/convertConstants";
@@ -1,4 +1,3 @@
import React from 'react';
import { Stack, Text, Select, Alert } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
@@ -1,4 +1,4 @@
import React, { useState, useMemo } from "react";
import { useState, useMemo } from "react";
import { Stack, Text, Group, Button, Box, Popover, UnstyledButton, useMantineTheme, useMantineColorScheme } from "@mantine/core";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
@@ -34,14 +34,14 @@ const GroupedFormatDropdown = ({
const groupedOptions = useMemo(() => {
const groups: Record<string, FormatOption[]> = {};
options.forEach(option => {
if (!groups[option.group]) {
groups[option.group] = [];
}
groups[option.group].push(option);
});
return groups;
}, [options]);
@@ -77,14 +77,14 @@ const GroupedFormatDropdown = ({
padding: '0.5rem 0.75rem',
border: `0.0625rem solid ${theme.colors.gray[4]}`,
borderRadius: theme.radius.sm,
backgroundColor: disabled
? theme.colors.gray[1]
: colorScheme === 'dark'
? theme.colors.dark[6]
backgroundColor: disabled
? theme.colors.gray[1]
: colorScheme === 'dark'
? theme.colors.dark[6]
: theme.white,
cursor: disabled ? 'not-allowed' : 'pointer',
width: '100%',
color: disabled
color: disabled
? colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.dark[7]
: colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.dark[9]
}}
@@ -93,19 +93,19 @@ const GroupedFormatDropdown = ({
<Text size="sm" c={value ? undefined : 'dimmed'}>
{selectedLabel}
</Text>
<KeyboardArrowDownIcon
style={{
<KeyboardArrowDownIcon
style={{
fontSize: '1rem',
transform: dropdownOpened ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
color: colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6]
}}
}}
/>
</Group>
</UnstyledButton>
</Popover.Target>
<Popover.Dropdown
style={{
<Popover.Dropdown
style={{
minWidth: Math.min(350, parseInt(minWidth.replace('rem', '')) * 16),
maxWidth: '90vw',
maxHeight: '40vh',
@@ -117,10 +117,10 @@ const GroupedFormatDropdown = ({
<Stack gap="md">
{Object.entries(groupedOptions).map(([groupName, groupOptions]) => (
<Box key={groupName}>
<Text
size="sm"
fw={600}
c={colorScheme === 'dark' ? 'dark.2' : 'gray.6'}
<Text
size="sm"
fw={600}
c={colorScheme === 'dark' ? 'dark.2' : 'gray.6'}
mb="xs"
>
{groupName}
@@ -153,4 +153,4 @@ const GroupedFormatDropdown = ({
);
};
export default GroupedFormatDropdown;
export default GroupedFormatDropdown;
@@ -1,4 +1,3 @@
import React from 'react';
import { Divider, Select, Stack, Switch } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { PageLayoutParameters } from '../../../hooks/tools/pageLayout/usePageLayoutParameters';
@@ -1,4 +1,3 @@
import React from 'react';
import { Divider, Select, Stack, TextInput } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ReorganizePagesParameters } from '../../../hooks/tools/reorganizePages/useReorganizePagesParameters';
@@ -31,7 +30,7 @@ export default function ReorganizePagesSettings({
disabled={disabled}
/>
{selectedMode && (
<div
<div
style={{
backgroundColor: 'var(--information-text-bg)',
color: 'var(--information-text-color)',
@@ -1,4 +1,3 @@
import React from "react";
import { Stack, Text, Select, ColorInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ReplaceColorParameters } from "../../../hooks/tools/replaceColor/useReplaceColorParameters";
@@ -105,4 +104,4 @@ const ReplaceColorSettings = ({ parameters, onParameterChange, disabled = false
);
};
export default ReplaceColorSettings;
export default ReplaceColorSettings;
@@ -30,6 +30,6 @@ const ErrorNotification = ({
{error}
</Notification>
);
}
};
export default ErrorNotification;
@@ -1,4 +1,3 @@
import React from 'react';
import { Stack, Text } from '@mantine/core';
import { formatFileSize, getFileDate } from '../../../utils/fileUtils';
@@ -24,4 +23,4 @@ const FileMetadata = ({ file }: FileMetadataProps) => {
);
};
export default FileMetadata;
export default FileMetadata;
@@ -1,4 +1,3 @@
import React from 'react';
import { Stack, Group, ActionIcon, Text } from '@mantine/core';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { Stack, Text, NumberInput } from "@mantine/core";
interface NumberInputWithUnitProps {
@@ -11,14 +11,14 @@ interface NumberInputWithUnitProps {
disabled?: boolean;
}
const NumberInputWithUnit = ({
label,
value,
onChange,
unit,
min,
max,
disabled = false
const NumberInputWithUnit = ({
label,
value,
onChange,
unit,
min,
max,
disabled = false
}: NumberInputWithUnitProps) => {
const [localValue, setLocalValue] = useState<number | string>(value);
@@ -54,4 +54,4 @@ const NumberInputWithUnit = ({
);
};
export default NumberInputWithUnit;
export default NumberInputWithUnit;
@@ -1,4 +1,3 @@
import React from 'react';
import { Button } from '@mantine/core';
import { useTranslation } from 'react-i18next';
@@ -52,6 +51,6 @@ const OperationButton = ({
}
</Button>
);
}
};
export default OperationButton;
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { Box, Text, Loader, Stack, Center, Flex } from '@mantine/core';
import FilePreview from '../../shared/FilePreview';
import FileMetadata from './FileMetadata';
@@ -149,7 +149,7 @@ const ToolStep = ({
<Divider style={{ color: '#E2E8F0', marginLeft: '1rem', marginRight: '-0.5rem' }} />
</div>
);
}
};
// ToolStepFactory for creating numbered steps
export function createToolSteps() {
@@ -164,13 +164,16 @@ export function createToolSteps() {
const isVisible = props.isVisible !== false;
const currentStepNumber = isVisible ? stepNumber++ : undefined;
const step = React.createElement(ToolStep, {
...props,
title,
_stepNumber: currentStepNumber,
children,
key: `step-${title.toLowerCase().replace(/\s+/g, '-')}`
});
const step = React.createElement(
ToolStep,
{
...props,
title,
_stepNumber: currentStepNumber,
key: `step-${title.toLowerCase().replace(/\s+/g, '-')}`
},
children
);
steps.push(step);
return step;
@@ -186,9 +189,9 @@ export function createToolSteps() {
const getVisibleCount = () => {
return steps.filter(step => {
const props = step.props as ToolStepProps;
const isVisible = props.isVisible !== false;
const excludeFromCount = props._excludeFromCount === true;
const stepProps = step.props as ToolStepProps;
const isVisible = stepProps.isVisible !== false;
const excludeFromCount = stepProps._excludeFromCount === true;
return isVisible && !excludeFromCount;
}).length;
};
@@ -203,9 +206,9 @@ export function ToolStepProvider({ children, forceStepNumbers }: { children: Rea
let count = 0;
React.Children.forEach(children, (child) => {
if (React.isValidElement(child) && child.type === ToolStep) {
const props = child.props as ToolStepProps;
const isVisible = props.isVisible !== false;
const excludeFromCount = props._excludeFromCount === true;
const stepProps = child.props as ToolStepProps;
const isVisible = stepProps.isVisible !== false;
const excludeFromCount = stepProps._excludeFromCount === true;
if (isVisible && !excludeFromCount) count++;
}
});
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useTranslation } from "react-i18next";
import { Stack, Button, Text, Alert, Tabs } from '@mantine/core';
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
@@ -256,4 +256,4 @@ const SignSettings = ({
);
};
export default SignSettings;
export default SignSettings;
@@ -158,6 +158,6 @@ const SplitSettings = ({
{parameters.method === SPLIT_METHODS.BY_PAGE_DIVIDER && renderByPageDividerForm()}
</Stack>
);
}
};
export default SplitSettings;
@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import { useState, useRef, useEffect, useMemo } from "react";
import { Stack, Button, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import LocalIcon from '../../shared/LocalIcon';
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { useSearch } from '@embedpdf/plugin-search/react';
import { useViewer } from '../../contexts/ViewerContext';
import { SEARCH_CONSTANTS } from './constants/search';
@@ -24,9 +24,9 @@ interface SearchResultState {
activeResultIndex?: number;
}
export function CustomSearchLayer({
pageIndex,
scale,
export function CustomSearchLayer({
pageIndex,
scale,
highlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.BACKGROUND,
activeHighlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.ACTIVE_BACKGROUND,
opacity = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.OPACITY,
@@ -42,17 +42,17 @@ export function CustomSearchLayer({
if (!searchProvides) {
return;
}
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
// Auto-scroll to active search result
if (state?.results && state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
const activeResult = state.results[state.activeResultIndex];
if (activeResult) {
if (activeResult) {
const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number
scrollActions.scrollToPage(pageNumber);
}
}
setSearchResultState(state);
});
@@ -69,7 +69,7 @@ export function CustomSearchLayer({
const filtered = searchResultState.results
.map((result, originalIndex) => ({ result, originalIndex }))
.filter(({ result }) => result.pageIndex === pageIndex);
return filtered;
}, [searchResultState, pageIndex]);
@@ -78,7 +78,7 @@ export function CustomSearchLayer({
}
return (
<div style={{
<div style={{
position: 'absolute',
top: 0,
left: 0,
@@ -117,4 +117,4 @@ export function CustomSearchLayer({
))}
</div>
);
}
}
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { createPluginRegistration } from '@embedpdf/core';
import { EmbedPDF } from '@embedpdf/core/react';
import { usePdfiumEngine } from '@embedpdf/engines/react';
@@ -312,4 +312,4 @@ export function LocalEmbedPDFWithAnnotations({
</EmbedPDF>
</div>
);
}
}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { Button, Paper, Group, NumberInput } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useViewer } from '../../contexts/ViewerContext';
@@ -14,11 +14,11 @@ interface PdfViewerToolbarProps {
currentPage?: number;
totalPages?: number;
onPageChange?: (page: number) => void;
// Dual page toggle (placeholder for now)
dualPage?: boolean;
onDualPageToggle?: () => void;
// Zoom controls (connected via ViewerContext)
currentZoom?: number;
}
@@ -33,7 +33,7 @@ export function PdfViewerToolbar({
}: PdfViewerToolbarProps) {
const { t } = useTranslation();
const { getScrollState, getZoomState, scrollActions, zoomActions, registerImmediateZoomUpdate, registerImmediateScrollUpdate } = useViewer();
const scrollState = getScrollState();
const zoomState = getZoomState();
const [pageInput, setPageInput] = useState(scrollState.currentPage || currentPage);
@@ -151,7 +151,7 @@ export function PdfViewerToolbar({
input: { width: 48, textAlign: "center", fontWeight: 500, fontSize: 16 },
}}
/>
<span style={{ fontWeight: 500, fontSize: 16 }}>
/ {scrollState.totalPages}
</span>
@@ -229,4 +229,4 @@ export function PdfViewerToolbar({
</Group>
</Paper>
);
}
}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { Box, ScrollArea } from '@mantine/core';
import { useViewer } from '../../contexts/ViewerContext';
+1 -2
View File
@@ -1,4 +1,3 @@
import React from 'react';
import EmbedPdfViewer from './EmbedPdfViewer';
export interface ViewerProps {
@@ -13,4 +12,4 @@ const Viewer = (props: ViewerProps) => {
return <EmbedPdfViewer {...props} />;
};
export default Viewer;
export default Viewer;
+1 -1
View File
@@ -22,7 +22,7 @@ export const ENDPOINTS = {
export type SplitMethod = typeof SPLIT_METHODS[keyof typeof SPLIT_METHODS];
export const isSplitMethod = (value: string | null): value is SplitMethod => {
return Object.values(SPLIT_METHODS).includes(value as SplitMethod);
}
};
import { CardOption } from '../components/shared/CardSelector';
+2 -2
View File
@@ -12,7 +12,7 @@
* Memory management handled by FileLifecycleManager (PDF.js cleanup, blob URL revocation).
*/
import React, { useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
import { useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
import {
FileContextProviderProps,
FileContextSelectors,
@@ -76,7 +76,7 @@ function FileContextInner({
const currentSelection = stateRef.current.ui.selectedFileIds;
const newFileIds = stirlingFiles.map(stirlingFile => stirlingFile.fileId);
dispatch({ type: 'SET_SELECTED_FILES', payload: { fileIds: [...currentSelection, ...newFileIds] } });
}
};
// File operations using unified addFiles helper with persistence
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
+3 -3
View File
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useRef, useMemo } from 'react';
import { createContext, useContext, useState, useRef, useMemo } from 'react';
import { SidebarState, SidebarRefs, SidebarContextValue, SidebarProviderProps } from '../types/sidebar';
const SidebarContext = createContext<SidebarContextValue | undefined>(undefined);
@@ -7,7 +7,7 @@ export function SidebarProvider({ children }: SidebarProviderProps) {
// All sidebar state management
const quickAccessRef = useRef<HTMLDivElement>(null);
const toolPanelRef = useRef<HTMLDivElement>(null);
const [sidebarsVisible, setSidebarsVisible] = useState(true);
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
const [readerMode, setReaderMode] = useState(false);
@@ -44,4 +44,4 @@ export function useSidebarContext(): SidebarContextValue {
throw new Error('useSidebarContext must be used within a SidebarProvider');
}
return context;
}
}
@@ -92,7 +92,7 @@ import SignSettings from "../components/tools/sign/SignSettings";
import CropSettings from "../components/tools/crop/CropSettings";
import RemoveAnnotations from "../tools/RemoveAnnotations";
import RemoveAnnotationsSettings from "../components/tools/removeAnnotations/RemoveAnnotationsSettings";
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings"
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings";
import ExtractImages from "../tools/ExtractImages";
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
import ReplaceColorSettings from "../components/tools/replaceColor/ReplaceColorSettings";
+118
View File
@@ -0,0 +1,118 @@
.mobile-layout {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background-color: var(--bg-background);
}
.mobile-toggle {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border-subtle);
background: var(--bg-toolbar);
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.mobile-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.mobile-brand {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.mobile-brand-mark {
height: 1.25rem;
width: auto;
}
.mobile-brand-name {
font-size: 0.95rem;
font-weight: 600;
color: var(--text-primary);
line-height: 1.2;
}
.mobile-toggle-buttons {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.25rem;
padding: 0.2rem;
border-radius: 9999px;
background: var(--bg-background);
border: 1px solid var(--border-subtle);
}
.mobile-toggle-button {
border: none;
border-radius: 9999px;
padding: 0.4rem 0.9rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-muted);
background: transparent;
transition: background 0.2s ease, color 0.2s ease;
}
.mobile-toggle-button:focus-visible {
outline: 2px solid var(--primary-color, #228be6);
outline-offset: 2px;
}
.mobile-toggle-button.active {
background: var(--primary-surface, rgba(34, 139, 230, 0.12));
color: var(--text-primary);
}
.mobile-toggle-hint {
font-size: 0.7rem;
color: var(--text-muted);
text-align: center;
}
.mobile-slider {
display: flex;
flex: 1 1 auto;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
touch-action: pan-x;
}
.mobile-slider::-webkit-scrollbar {
display: none;
}
.mobile-slide {
flex: 0 0 100%;
width: 100%;
height: 100%;
scroll-snap-align: start;
display: flex;
flex-direction: column;
min-height: 0;
}
.mobile-slide-content {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
}
.mobile-slide-content > * {
flex: 1 1 auto;
min-height: 0;
}
+194 -15
View File
@@ -1,15 +1,23 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
import { Group } from "@mantine/core";
import { ActionIcon, Group, useMantineColorScheme } from "@mantine/core";
import { useSidebarContext } from "../contexts/SidebarContext";
import { useDocumentMeta } from "../hooks/useDocumentMeta";
import { getBaseUrl } from "../constants/app";
import { BASE_PATH, getBaseUrl } from "../constants/app";
import { useMediaQuery } from "@mantine/hooks";
import ToolPanel from "../components/tools/ToolPanel";
import Workbench from "../components/layout/Workbench";
import QuickAccessBar from "../components/shared/QuickAccessBar";
import RightRail from "../components/shared/RightRail";
import FileManager from "../components/FileManager";
import LocalIcon from "../components/shared/LocalIcon";
import { useFilesModalContext } from "../contexts/FilesModalContext";
import "./HomePage.css";
type MobileView = "tools" | "workbench";
export default function HomePage() {
@@ -22,6 +30,106 @@ export default function HomePage() {
const { selectedTool, selectedToolKey } = useToolWorkflow();
const { openFilesModal } = useFilesModalContext();
const { colorScheme } = useMantineColorScheme();
const isMobile = useMediaQuery("(max-width: 900px)");
const sliderRef = useRef<HTMLDivElement | null>(null);
const slideRefs = useRef<Record<MobileView, HTMLDivElement | null>>({
tools: null,
workbench: null,
});
const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools");
const brandName = t("home.mobile.brandName", "Stirling");
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
const brandMarkSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${
colorScheme === "dark" ? "Dark" : "Light"
}.svg`;
const scrollToMobileView = useCallback(
(view: MobileView, behavior: ScrollBehavior = "smooth") => {
const container = sliderRef.current;
const target = slideRefs.current[view];
if (!container || !target) {
return;
}
if (behavior === "auto") {
container.scrollLeft = target.offsetLeft;
return;
}
if (typeof target.scrollIntoView === "function") {
target.scrollIntoView({
behavior,
block: "nearest",
inline: "start",
});
return;
}
if (typeof container.scrollTo === "function") {
container.scrollTo({ left: target.offsetLeft, behavior });
return;
}
container.scrollLeft = target.offsetLeft;
},
[]
);
const handleSelectMobileView = useCallback(
(view: MobileView) => {
scrollToMobileView(view);
setActiveMobileView(view);
},
[scrollToMobileView]
);
useEffect(() => {
if (isMobile) {
scrollToMobileView(activeMobileView, "auto");
return;
}
if (activeMobileView !== "tools") {
setActiveMobileView("tools");
}
scrollToMobileView("tools", "auto");
}, [activeMobileView, isMobile, scrollToMobileView]);
useEffect(() => {
if (!isMobile) return;
const container = sliderRef.current;
if (!container) return;
let animationFrame = 0;
const handleScroll = () => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
animationFrame = window.requestAnimationFrame(() => {
const { scrollLeft, offsetWidth } = container;
const threshold = offsetWidth / 2;
const nextView: MobileView = scrollLeft >= threshold ? "workbench" : "tools";
setActiveMobileView((current) => (current === nextView ? current : nextView));
});
};
container.addEventListener("scroll", handleScroll, { passive: true });
return () => {
container.removeEventListener("scroll", handleScroll);
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
};
}, [isMobile]);
const baseUrl = getBaseUrl();
// Update document meta when tool changes
@@ -38,19 +146,90 @@ export default function HomePage() {
return (
<div className="h-screen overflow-hidden">
<Group
align="flex-start"
gap={0}
h="100%"
className="flex-nowrap flex"
>
<QuickAccessBar
ref={quickAccessRef} />
<ToolPanel />
<Workbench />
<RightRail />
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
</Group>
{isMobile ? (
<div className="mobile-layout">
<div className="mobile-toggle">
<div className="mobile-header">
<div className="mobile-brand">
<img src={brandMarkSrc} alt={brandAltText} className="mobile-brand-mark" />
<span className="mobile-brand-name">{brandName}</span>
</div>
<ActionIcon
variant="subtle"
size="md"
aria-label={t('home.mobile.openFiles', 'Open files')}
onClick={() => openFilesModal()}
>
<LocalIcon icon="folder-rounded" width="1.25rem" height="1.25rem" />
</ActionIcon>
</div>
<div className="mobile-toggle-buttons" role="tablist" aria-label={t('home.mobile.viewSwitcher', 'Switch workspace view')}>
<button
type="button"
role="tab"
aria-selected={activeMobileView === "tools"}
className={`mobile-toggle-button ${activeMobileView === "tools" ? "active" : ""}`}
onClick={() => handleSelectMobileView("tools")}
>
{t('home.mobile.tools', 'Tools')}
</button>
<button
type="button"
role="tab"
aria-selected={activeMobileView === "workbench"}
className={`mobile-toggle-button ${activeMobileView === "workbench" ? "active" : ""}`}
onClick={() => handleSelectMobileView("workbench")}
>
{t('home.mobile.workspace', 'Workspace')}
</button>
</div>
<span className="mobile-toggle-hint">
{t('home.mobile.swipeHint', 'Swipe left or right to switch views')}
</span>
</div>
<div ref={sliderRef} className="mobile-slider">
<div
className="mobile-slide"
aria-label={t('home.mobile.toolsSlide', 'Tool selection panel')}
ref={(node) => {
slideRefs.current.tools = node;
}}
>
<div className="mobile-slide-content">
<ToolPanel />
</div>
</div>
<div
className="mobile-slide"
aria-label={t('home.mobile.workbenchSlide', 'Workspace panel')}
ref={(node) => {
slideRefs.current.workbench = node;
}}
>
<div className="mobile-slide-content">
<div className="flex-1 min-h-0 flex flex-col">
<Workbench />
</div>
</div>
</div>
</div>
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
</div>
) : (
<Group
align="flex-start"
gap={0}
h="100%"
className="flex-nowrap flex"
>
<QuickAccessBar
ref={quickAccessRef} />
<ToolPanel />
<Workbench />
<RightRail />
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
</Group>
)}
</div>
);
}
+1 -1
View File
@@ -11,7 +11,7 @@ interface CompressedObject {
const getData = (zipEntry: JSZipObject): CompressedObject | undefined => {
return (zipEntry as any)._data as CompressedObject;
}
};
export interface ZipExtractionResult {
success: boolean;
+9 -9
View File
@@ -1,5 +1,5 @@
import '@testing-library/jest-dom'
import { vi } from 'vitest'
import '@testing-library/jest-dom';
import { vi } from 'vitest';
// Mock i18next for tests
vi.mock('react-i18next', () => ({
@@ -27,8 +27,8 @@ vi.mock('i18next-http-backend', () => ({
}));
// Mock window.URL.createObjectURL and revokeObjectURL for tests
global.URL.createObjectURL = vi.fn(() => 'mocked-url')
global.URL.revokeObjectURL = vi.fn()
global.URL.createObjectURL = vi.fn(() => 'mocked-url');
global.URL.revokeObjectURL = vi.fn();
// Mock File and Blob API methods that aren't available in jsdom
if (!globalThis.File.prototype.arrayBuffer) {
@@ -88,21 +88,21 @@ global.Worker = vi.fn().mockImplementation(() => ({
removeEventListener: vi.fn(),
onmessage: null,
onerror: null,
}))
}));
// Mock ResizeObserver for Mantine components
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}))
}));
// Mock IntersectionObserver for components that might use it
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}))
}));
// Mock matchMedia for responsive components
Object.defineProperty(window, 'matchMedia', {
@@ -117,7 +117,7 @@ Object.defineProperty(window, 'matchMedia', {
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
});
// Set global test timeout to prevent hangs
vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 })
vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 });
@@ -452,7 +452,7 @@ describe('Convert Tool Integration Tests', () => {
const files = [
createPDFFile(),
createTestStirlingFile('test2.pdf', '%PDF-1.4...', 'application/pdf')
]
];
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useFileSelection } from "../contexts/FileContext";
-1
View File
@@ -1,4 +1,3 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "../types/tool";
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "../types/tool";
-5
View File
@@ -66,8 +66,3 @@ export interface SuggestedAutomation {
updatedAt: string;
icon: any; // MUI Icon component
}
// Export the AutomateParameters interface that was previously defined inline
export interface AutomateParameters extends AutomationExecutionCallbacks {
automationConfig?: AutomationConfig;
}
+1 -1
View File
@@ -17,7 +17,7 @@ export function firePixel(pathname: string): void {
lastFiredTime = now;
const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7'
+ '&path=' + encodeURIComponent(pathname)
+ '&path=' + encodeURIComponent(pathname);
const img = new Image();
img.referrerPolicy = "no-referrer-when-downgrade";
+3 -3
View File
@@ -63,11 +63,11 @@ module.exports = {
background: 'rgb(var(--background) / <alpha-value>)',
border: 'rgb(var(--border) / <alpha-value>)',
},
// Z-index scale
zIndex: {
'dropdown': '1000',
'sticky': '1020',
'sticky': '1020',
'fixed': '1030',
'modal-backdrop': '1040',
'modal': '1050',
@@ -81,4 +81,4 @@ module.exports = {
corePlugins: {
preflight: true,
},
}
};
+2 -2
View File
@@ -1,4 +1,4 @@
import { defineConfig } from 'vitest/config'
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react-swc';
export default defineConfig({
@@ -37,4 +37,4 @@ export default defineConfig({
'@': '/src'
}
}
})
});
+2 -2
View File
@@ -1,4 +1,4 @@
import { defineConfig } from 'vitest/config'
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
@@ -6,4 +6,4 @@ export default defineConfig({
testTimeout: 5000,
include: ['src/utils/convertUtils.test.ts']
},
})
});