feat: integrate PDF Tools with toolbox portal

This commit is contained in:
2026-07-20 18:13:43 +02:00
parent bff9e059d8
commit d40a288a4e
23 changed files with 879 additions and 354 deletions

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import Layout from './components/Layout';
import { AppShell } from '@add-ideas/toolbox-shell-react';
import FileLoader from './components/FileLoader';
import ReorderPanel from './components/ReorderPanel';
import ActionsPanel from './components/ActionsPanel';
@@ -43,6 +43,7 @@ import {
createSplitResultsZip,
createSplitZipFilename,
} from './pdf/pdfZipService';
import { toolboxApp } from './toolboxApp';
import {
createSelectionPdfName,
createSelectionWorkspaceName,
@@ -1139,7 +1140,21 @@ const App: React.FC = () => {
previewVisualIndex >= 0 && previewVisualIndex < pages.length - 1;
return (
<Layout onOpenHelp={() => setHelpOpen(true)}>
<AppShell
app={toolboxApp}
className="pdf-toolbox-shell"
appActions={
<button
type="button"
className="app-help-button"
onClick={() => setHelpOpen(true)}
aria-haspopup="dialog"
title="Open help and keyboard shortcuts (F1 or ?)"
>
Help <span aria-hidden="true">?</span>
</button>
}
>
<FileLoader pdf={pdf} onFilesLoaded={handleFilesLoaded} />
{mergeQueueOpen && mergeQueueItems.length > 0 && (
@@ -1252,7 +1267,7 @@ const App: React.FC = () => {
</ActionDialog>
<HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
</Layout>
</AppShell>
);
};

View File

@@ -1,47 +0,0 @@
import React from 'react';
import { APP_VERSION } from '../version';
interface LayoutProps {
children: React.ReactNode;
onOpenHelp?: () => void;
}
const Layout: React.FC<LayoutProps> = ({ children, onOpenHelp }) => {
return (
<div className="app-root">
<header className="app-header">
<div className="app-header-content">
<div className="app-header-title">
<span className="app-logo">📄</span>
<div>
<h1>PDF Workbench</h1>
<small>All in your browser</small>
</div>
</div>
<div className="app-header-actions">
{onOpenHelp && (
<button
type="button"
className="app-help-button"
onClick={onOpenHelp}
aria-haspopup="dialog"
title="Open help and keyboard shortcuts (F1 or ?)"
>
Help <span aria-hidden="true">?</span>
</button>
)}
<div className="app-version" title={`Version ${APP_VERSION}`}>
v{APP_VERSION}
</div>
</div>
</div>
</header>
<main className="app-main">{children}</main>
</div>
);
};
export default Layout;

View File

@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@add-ideas/toolbox-shell-react/styles.css';
import './styles.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(

View File

@@ -124,45 +124,20 @@ button.secondary {
margin-top: 0.5rem;
}
.app-root {
min-height: 100vh;
background-color: #f3f4f6;
.pdf-toolbox-shell {
color-scheme: light;
--toolbox-background: #f3f4f6;
--toolbox-surface: #ffffff;
--toolbox-text: #111827;
--toolbox-muted: #4b5563;
--toolbox-border: #d1d5db;
--toolbox-accent: #2563eb;
--toolbox-accent-contrast: #ffffff;
}
.app-header {
position: sticky;
top: 0;
z-index: 10;
background: #111827;
color: #e5e7eb;
padding: 0.6rem 1rem;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.4);
}
.app-header-title {
display: flex;
align-items: center;
gap: 0.6rem;
}
.app-header h1 {
font-size: 1rem;
margin: 0;
}
.app-header small {
color: #9ca3af;
font-size: 0.8rem;
}
.app-logo {
font-size: 1.4rem;
}
.app-main {
.pdf-toolbox-shell .toolbox-shell__main {
padding: 0.75rem;
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 0.75rem;
@@ -184,36 +159,11 @@ button.secondary {
border-top: 1px solid #e5e7eb;
}
.app-header-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.app-version {
flex: 0 0 auto;
border-radius: 999px;
background: #374151;
color: #d1d5db;
padding: 0.4rem 0.7rem;
font-size: 0.85rem;
font-weight: 500;
line-height: 1;
white-space: nowrap;
}
.app-header-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.app-help-button {
border: 1px solid #4b5563;
border: 1px solid var(--toolbox-border);
border-radius: 999px;
background: transparent;
color: #e5e7eb;
color: var(--toolbox-accent);
padding: 0.35rem 0.65rem;
font-size: 0.85rem;
font-weight: 500;
@@ -223,8 +173,9 @@ button.secondary {
.app-help-button:hover,
.app-help-button:focus-visible {
background: #374151;
outline: none;
background: color-mix(in srgb, var(--toolbox-accent) 10%, transparent);
outline: 2px solid var(--toolbox-accent);
outline-offset: 2px;
}
.help-dialog-backdrop {
@@ -393,10 +344,6 @@ button.secondary {
.help-concepts dl {
grid-template-columns: 1fr;
}
.app-header-content {
align-items: flex-start;
}
}
.merge-queue-list {

View File

@@ -0,0 +1,36 @@
{
"id": "de.add-ideas.pdf-tools",
"name": "PDF Workbench",
"description": "Page-level PDF operations performed locally in the browser.",
"entry": "./",
"icon": "./favicon.svg",
"categories": ["documents", "pdf"],
"tags": ["merge", "split", "rotate", "reorder"],
"actions": [
{
"id": "source",
"label": "Source",
"url": "https://git.add-ideas.de/zemion/pdf-tools/src/tag/v0.3.4"
}
],
"integration": {
"contextVersion": 1,
"launchModes": ["navigate", "new-tab"],
"embedding": "unsupported"
},
"requirements": {
"secureContext": true,
"workers": true,
"indexedDb": true,
"crossOriginIsolated": false
},
"privacy": {
"processing": "local",
"fileUploads": false,
"telemetry": false
},
"source": {
"repository": "https://git.add-ideas.de/zemion/pdf-tools",
"license": "AGPL-3.0-only"
}
}

10
src/toolboxApp.test.ts Normal file
View File

@@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest';
import { parseToolboxApp } from '@add-ideas/toolbox-contract';
import generatedManifest from '../public/toolbox-app.json';
import { toolboxApp } from './toolboxApp';
describe('toolbox app definition', () => {
it('matches the generated public manifest', () => {
expect(toolboxApp).toEqual(parseToolboxApp(generatedManifest));
});
});

9
src/toolboxApp.ts Normal file
View File

@@ -0,0 +1,9 @@
import { parseToolboxApp } from '@add-ideas/toolbox-contract';
import definition from './toolboxApp.definition.json';
import { APP_VERSION } from './version';
export const toolboxApp = parseToolboxApp({
schemaVersion: 1,
...definition,
version: APP_VERSION,
});