@signiphi/page-assembly 0.2.0-beta.1 → 0.2.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,24 @@
1
1
  # @signiphi/page-assembly
2
2
 
3
- React component for PDF and image page manipulation - upload, reorder, delete, and combine pages into a single document.
3
+ > A powerful React component for PDF and image page manipulation - upload, reorder, delete, and combine pages into a single document.
4
4
 
5
- ## Features
5
+ [![npm version](https://img.shields.io/npm/v/@signiphi/page-assembly.svg)](https://www.npmjs.com/package/@signiphi/page-assembly)
6
+ [![License](https://img.shields.io/npm/l/@signiphi/page-assembly.svg)](https://github.com/signiphi/signiphi)
6
7
 
7
- - 📄 **File Upload** - Upload multiple PDF files and images
8
- - 🔄 **Drag & Drop** - Reorder pages and files with intuitive drag-and-drop
9
- - ✂️ **Multi-Select** - Select multiple pages with Ctrl+Click, Shift+Click, or checkboxes
10
- - 🗑️ **Bulk Operations** - Move or delete multiple pages at once
8
+ ## Features
9
+
10
+ - 📄 **Multi-File Upload** - Upload multiple PDF files and images simultaneously
11
+ - 🔄 **Drag & Drop Interface** - Intuitive drag-and-drop for reordering pages and files
12
+ - ✂️ **Advanced Selection** - Multi-select pages with Ctrl+Click, Shift+Click, or checkboxes
13
+ - 🗑️ **Bulk Operations** - Move or delete multiple pages at once with ease
11
14
  - 🖼️ **Image Support** - Convert and combine images (PNG, JPEG, WebP) into PDFs
12
- - 📦 **PDF Assembly** - Generate a final assembled PDF with all selected pages
13
- - ♻️ **State Management** - Save and restore page assembly state
14
- - 🎨 **Customizable** - Scoped styles that don't leak to parent application
15
+ - 📦 **PDF Assembly** - Generate a final assembled PDF with all selected pages in order
16
+ - ♻️ **State Management** - Save and restore page assembly state for persistence
17
+ - 🎨 **Scoped Styling** - CSS is scoped and won't conflict with your application styles
18
+ - ⚡ **Performance** - Optimized for handling large documents with many pages
19
+ - 🔒 **Client-Side** - All processing happens in the browser, no server required
15
20
 
16
- ## Installation
21
+ ## 📦 Installation
17
22
 
18
23
  ```bash
19
24
  npm install @signiphi/page-assembly
@@ -28,12 +33,22 @@ yarn add @signiphi/page-assembly
28
33
  This package requires the following peer dependencies:
29
34
 
30
35
  ```bash
31
- npm install react react-dom lucide-react @radix-ui/react-alert-dialog @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-label @radix-ui/react-select @radix-ui/react-tooltip
36
+ npm install react react-dom lucide-react \
37
+ @radix-ui/react-alert-dialog \
38
+ @radix-ui/react-dialog \
39
+ @radix-ui/react-dropdown-menu \
40
+ @radix-ui/react-label \
41
+ @radix-ui/react-select \
42
+ @radix-ui/react-tooltip
32
43
  ```
33
44
 
34
- ## Usage
45
+ Or install them all at once:
46
+
47
+ ```bash
48
+ npm install @signiphi/page-assembly react react-dom lucide-react @radix-ui/react-alert-dialog @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-label @radix-ui/react-select @radix-ui/react-tooltip
49
+ ```
35
50
 
36
- ### Basic Example
51
+ ## 🚀 Quick Start
37
52
 
38
53
  ```tsx
39
54
  import { useRef } from 'react';
@@ -78,8 +93,12 @@ function App() {
78
93
  }
79
94
  ```
80
95
 
96
+ ## 📖 Usage Examples
97
+
81
98
  ### With Initial PDF
82
99
 
100
+ Load an existing PDF as the starting point:
101
+
83
102
  ```tsx
84
103
  import { PageAssembler } from '@signiphi/page-assembly';
85
104
  import '@signiphi/page-assembly/styles';
@@ -110,6 +129,8 @@ function App() {
110
129
 
111
130
  ### With State Persistence
112
131
 
132
+ Save and restore the page assembly state:
133
+
113
134
  ```tsx
114
135
  import { PageAssembler, PageAssemblerState } from '@signiphi/page-assembly';
115
136
  import '@signiphi/page-assembly/styles';
@@ -148,57 +169,166 @@ function App() {
148
169
  }
149
170
  ```
150
171
 
151
- ## API Reference
172
+ ### Template Mode (Disable File Upload)
173
+
174
+ Use when you want users to only manipulate pages from a pre-loaded document:
175
+
176
+ ```tsx
177
+ import { PageAssembler } from '@signiphi/page-assembly';
178
+ import '@signiphi/page-assembly/styles';
179
+
180
+ function App() {
181
+ const [templatePdf, setTemplatePdf] = useState<Uint8Array | null>(null);
182
+
183
+ useEffect(() => {
184
+ // Load template PDF
185
+ fetch('/template.pdf')
186
+ .then(res => res.arrayBuffer())
187
+ .then(buffer => setTemplatePdf(new Uint8Array(buffer)));
188
+ }, []);
189
+
190
+ return (
191
+ <div className="signiphi-page-assembly">
192
+ <PageAssembler
193
+ initialPdf={templatePdf || undefined}
194
+ disableFileUpload={true}
195
+ />
196
+ </div>
197
+ );
198
+ }
199
+ ```
200
+
201
+ ### Upload to Server
202
+
203
+ Assemble and upload the PDF to your backend:
204
+
205
+ ```tsx
206
+ const handleAssembleAndUpload = async () => {
207
+ if (!assemblerRef.current) return;
208
+
209
+ try {
210
+ // Assemble the PDF
211
+ const pdfBlob = await assemblerRef.current.assemble();
212
+
213
+ // Create form data
214
+ const formData = new FormData();
215
+ formData.append('pdf', pdfBlob, 'assembled.pdf');
216
+
217
+ // Upload to server
218
+ const response = await fetch('/api/documents/upload', {
219
+ method: 'POST',
220
+ body: formData
221
+ });
222
+
223
+ if (response.ok) {
224
+ const result = await response.json();
225
+ console.log('Upload successful:', result);
226
+ }
227
+ } catch (error) {
228
+ console.error('Failed to assemble/upload PDF:', error);
229
+ }
230
+ };
231
+ ```
232
+
233
+ ### Custom Image Sizing
234
+
235
+ Control how images are converted to PDF pages:
236
+
237
+ ```tsx
238
+ const handleAssemble = async () => {
239
+ if (!assemblerRef.current) return;
240
+
241
+ // Use original image dimensions
242
+ const pdfBlob = await assemblerRef.current.assemble({
243
+ imageSizing: 'original-size'
244
+ });
245
+
246
+ // Or use auto letter sizing (default)
247
+ const pdfBlob2 = await assemblerRef.current.assemble({
248
+ imageSizing: 'auto-letter'
249
+ });
250
+ };
251
+ ```
252
+
253
+ ### Monitor State Changes
254
+
255
+ Track when users add, remove, or reorder pages:
256
+
257
+ ```tsx
258
+ function App() {
259
+ const handleStateChange = (state: PageAssemblerState) => {
260
+ const totalPages = state.files.reduce((acc, file) => acc + file.pages.length, 0);
261
+ console.log(`Total files: ${state.files.length}`);
262
+ console.log(`Total pages: ${totalPages}`);
263
+ console.log(`Selected pages: ${state.selection.length}`);
264
+ };
265
+
266
+ return (
267
+ <div className="signiphi-page-assembly">
268
+ <PageAssembler
269
+ onChange={handleStateChange}
270
+ onValidityChange={(isValid) => {
271
+ if (!isValid) {
272
+ console.warn('No pages to assemble');
273
+ }
274
+ }}
275
+ />
276
+ </div>
277
+ );
278
+ }
279
+ ```
280
+
281
+ ## 📚 API Reference
152
282
 
153
283
  ### PageAssembler Props
154
284
 
155
- | Prop | Type | Description |
156
- |------|------|-------------|
157
- | `initialPdf` | `File \| Blob \| ArrayBuffer \| Uint8Array` | Optional initial PDF to pre-load |
158
- | `initialState` | `PageAssemblerState` | Optional initial state to restore previous session |
159
- | `onChange` | `(state: PageAssemblerState) => void` | Called when state changes (files/pages added/removed/reordered) |
160
- | `onValidityChange` | `(isValid: boolean) => void` | Called when validity changes (e.g., 0 pages) |
161
- | `limits` | `{ maxFileSizeMB?: number; maxTotalPages?: number }` | Optional file size and page count limits |
162
- | `disableFileUpload` | `boolean` | Disable file upload UI (for template-only flows) |
163
- | `className` | `string` | Optional className passthrough |
285
+ | Prop | Type | Default | Description |
286
+ |------|------|---------|-------------|
287
+ | `initialPdf` | `File \| Blob \| ArrayBuffer \| Uint8Array` | `undefined` | Optional initial PDF to pre-load into the assembler |
288
+ | `initialState` | `PageAssemblerState` | `undefined` | Optional initial state to restore previous session |
289
+ | `onChange` | `(state: PageAssemblerState) => void` | `undefined` | Called when state changes (files/pages added/removed/reordered) |
290
+ | `onValidityChange` | `(isValid: boolean) => void` | `undefined` | Called when validity changes (e.g., when there are 0 pages) |
291
+ | `limits` | `{ maxFileSizeMB?: number; maxTotalPages?: number }` | `undefined` | Optional file size and page count limits |
292
+ | `disableFileUpload` | `boolean` | `false` | Disable file upload UI (useful for template-only workflows) |
293
+ | `className` | `string` | `undefined` | Optional className passthrough for custom styling |
164
294
 
165
295
  ### PageAssemblerHandle Methods
166
296
 
167
- The `ref` provides access to imperative methods:
297
+ Access these methods via the `ref`:
168
298
 
169
299
  | Method | Type | Description |
170
300
  |--------|------|-------------|
171
- | `assemble` | `(opts?: AssembleOptions) => Promise<Blob>` | Assembles current pages into a single PDF |
172
- | `getSummary` | `() => { totalFiles: number; totalPages: number }` | Returns summary of current state |
173
- | `isBusy` | `() => boolean` | Returns true if loading/assembling |
174
- | `getState` | `() => PageAssemblerState` | Returns current state |
301
+ | `assemble` | `(opts?: AssembleOptions) => Promise<Blob>` | Assembles current pages into a single PDF blob |
302
+ | `getSummary` | `() => { totalFiles: number; totalPages: number }` | Returns summary information about current state |
303
+ | `isBusy` | `() => boolean` | Returns true if currently loading or assembling |
304
+ | `getState` | `() => PageAssemblerState` | Returns the current state (files, pages, selection) |
175
305
 
176
306
  ### Types
177
307
 
178
308
  ```typescript
179
309
  interface PageInfo {
180
- id: string;
181
- srcIndex: number;
182
- thumbUrl: string;
183
- width?: number;
184
- height?: number;
185
- mime: string;
186
- originalFileId?: string;
310
+ id: string; // Unique page identifier
311
+ srcIndex: number; // Original page index in source document
312
+ thumbUrl: string; // Data URL for thumbnail preview
313
+ width?: number; // Page width in points
314
+ height?: number; // Page height in points
315
+ mime: string; // MIME type (application/pdf or image/*)
316
+ originalFileId?: string; // ID of the source file
187
317
  }
188
318
 
189
319
  interface FileInfo {
190
- id: string;
191
- name: string;
192
- type: "pdf" | "image";
193
- originalFile: File;
194
- originalBytes: Uint8Array;
195
- pages: PageInfo[];
320
+ id: string; // Unique file identifier
321
+ name: string; // Original filename
322
+ type: "pdf" | "image"; // File type
323
+ originalFile: File; // Original File object
324
+ originalBytes: Uint8Array; // Original file bytes
325
+ pages: PageInfo[]; // Array of pages from this file
196
326
  }
197
327
 
198
328
  interface PageAssemblerState {
199
- files: FileInfo[];
200
- selection: string[];
201
- lastSelectedPageId?: string;
329
+ files: FileInfo[]; // All uploaded files
330
+ selection: string[]; // IDs of currently selected pages
331
+ lastSelectedPageId?: string; // ID of last selected page (for shift-click)
202
332
  }
203
333
 
204
334
  interface AssembleOptions {
@@ -206,91 +336,152 @@ interface AssembleOptions {
206
336
  }
207
337
  ```
208
338
 
209
- ## Styling
339
+ ## 🎨 Styling
340
+
341
+ The package includes scoped Tailwind CSS styles that won't conflict with your application.
210
342
 
211
- The package includes scoped Tailwind CSS styles. Import the styles in your application:
343
+ ### Import Styles
212
344
 
213
345
  ```tsx
214
346
  import '@signiphi/page-assembly/styles';
215
347
  ```
216
348
 
217
- All styles are prefixed with `.signiphi-page-assembly` to prevent conflicts with your application's styles.
349
+ All styles are prefixed with `.signiphi-page-assembly` to prevent conflicts.
218
350
 
219
351
  ### Custom Styling
220
352
 
221
- You can override the default styles by wrapping the component in a container with custom CSS:
353
+ Override default styles by wrapping the component:
222
354
 
223
355
  ```tsx
224
- <div className="my-custom-page-assembly signiphi-page-assembly">
356
+ <div className="my-custom-styles signiphi-page-assembly">
225
357
  <PageAssembler />
226
358
  </div>
227
359
  ```
228
360
 
229
- ## Advanced Usage
361
+ Then add your custom CSS:
230
362
 
231
- ### Working with Assembled PDF
363
+ ```css
364
+ .my-custom-styles {
365
+ /* Your custom styles here */
366
+ }
367
+ ```
232
368
 
233
- ```tsx
234
- const handleAssemble = async () => {
235
- if (!assemblerRef.current) return;
236
-
237
- const pdfBlob = await assemblerRef.current.assemble();
238
-
239
- // Upload to server
240
- const formData = new FormData();
241
- formData.append('pdf', pdfBlob, 'assembled.pdf');
242
-
243
- await fetch('/api/upload', {
244
- method: 'POST',
245
- body: formData
246
- });
247
- };
369
+ ## 🏗️ Architecture
370
+
371
+ The PageAssembler component is built with:
372
+
373
+ - **PDF Processing**: Uses `pdf-lib` for PDF manipulation and assembly
374
+ - **PDF Rendering**: Uses `pdfjs-dist` for rendering PDF thumbnails
375
+ - **Drag & Drop**: Uses `react-dnd` for intuitive page reordering
376
+ - **UI Components**: Uses Radix UI for accessible, unstyled component primitives
377
+ - **State Management**: React hooks for efficient state management
378
+ - **Performance**: Optimized with React.memo, useMemo, and useCallback
379
+
380
+ ### Component Structure
381
+
382
+ ```
383
+ PageAssembler
384
+ ├── FileUploadArea (drag & drop, file selection)
385
+ ├── FileList (list of uploaded files)
386
+ │ └── FileCard (individual file with pages)
387
+ │ └── PageThumbnail[] (draggable page previews)
388
+ └── BulkActions (multi-select operations)
248
389
  ```
249
390
 
250
- ### Getting Summary Information
391
+ ## 🔧 Troubleshooting
392
+
393
+ ### PDF.js Worker Issues
394
+
395
+ If you encounter errors related to PDF.js worker, make sure you're properly configured:
251
396
 
252
397
  ```tsx
253
- const assemblerRef = useRef<PageAssemblerHandle>(null);
398
+ // In your app's entry point (e.g., main.tsx or App.tsx)
399
+ import { GlobalWorkerOptions } from 'pdfjs-dist';
254
400
 
255
- const summary = assemblerRef.current?.getSummary();
256
- console.log(`Total files: ${summary.totalFiles}`);
257
- console.log(`Total pages: ${summary.totalPages}`);
401
+ // Set the worker source
402
+ GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@5.3.93/build/pdf.worker.min.mjs`;
258
403
  ```
259
404
 
260
- ### Checking if Busy
405
+ ### Large File Performance
406
+
407
+ For better performance with large files:
261
408
 
409
+ 1. Set appropriate limits:
410
+ ```tsx
411
+ <PageAssembler
412
+ limits={{
413
+ maxFileSizeMB: 10, // Limit individual file size
414
+ maxTotalPages: 50 // Limit total page count
415
+ }}
416
+ />
417
+ ```
418
+
419
+ 2. Implement loading states:
262
420
  ```tsx
263
421
  const isBusy = assemblerRef.current?.isBusy();
264
422
  if (isBusy) {
265
- console.log('Page assembler is currently processing...');
423
+ return <LoadingSpinner />;
266
424
  }
267
425
  ```
268
426
 
269
- ## Browser Support
427
+ ### Memory Management
428
+
429
+ When working with many large files, consider:
430
+
431
+ - Limiting the number of concurrent file uploads
432
+ - Clearing state when unmounting
433
+ - Using the `onChange` callback to implement auto-save and state cleanup
434
+
435
+ ## 🌐 Browser Support
436
+
437
+ | Browser | Minimum Version |
438
+ |---------|-----------------|
439
+ | Chrome | Latest 2 versions |
440
+ | Edge | Latest 2 versions |
441
+ | Firefox | Latest 2 versions |
442
+ | Safari | Latest 2 versions |
443
+
444
+ **Note**: This component requires modern browser APIs including:
445
+ - Web Workers (for PDF.js)
446
+ - FileReader API
447
+ - Blob API
448
+ - Drag and Drop API
449
+
450
+ ## 📦 Dependencies
451
+
452
+ ### Core Dependencies
453
+ - `pdf-lib` (^1.17.1) - PDF manipulation and assembly
454
+ - `pdfjs-dist` (5.3.93) - PDF rendering and parsing
455
+ - `react-dnd` (^16.0.1) - Drag and drop functionality
456
+ - `react-dnd-html5-backend` (^16.0.1) - HTML5 backend for react-dnd
457
+
458
+ ### UI Dependencies (Peer Dependencies)
459
+ - `react` (^18.2.0 || ^19.0.0)
460
+ - `react-dom` (^18.2.0 || ^19.0.0)
461
+ - `lucide-react` (^0.454.0) - Icon components
462
+ - `@radix-ui/react-*` - Accessible UI primitives
463
+
464
+ ## 🤝 Contributing
465
+
466
+ Contributions are welcome! Please read the [contributing guidelines](https://github.com/signiphi/signiphi/blob/main/CONTRIBUTING.md) before submitting PRs.
270
467
 
271
- - Chrome/Edge: Latest 2 versions
272
- - Firefox: Latest 2 versions
273
- - Safari: Latest 2 versions
468
+ ## 📄 License
274
469
 
275
- ## Dependencies
470
+ See the [LICENSE](https://github.com/signiphi/signiphi/blob/main/LICENSE) file in the root of the repository.
276
471
 
277
- - `react` - ^18.2.0 || ^19.0.0
278
- - `react-dom` - ^18.2.0 || ^19.0.0
279
- - `pdf-lib` - ^1.17.1 (PDF manipulation)
280
- - `pdfjs-dist` - 5.3.93 (PDF rendering)
281
- - `react-dnd` - ^16.0.1 (Drag and drop)
282
- - `react-dnd-html5-backend` - ^16.0.1
283
- - Radix UI components (see peer dependencies)
284
- - `lucide-react` - ^0.454.0 (Icons)
472
+ ## 💬 Support
285
473
 
286
- ## License
474
+ - 📚 [Documentation](https://github.com/signiphi/signiphi)
475
+ - 🐛 [Report Issues](https://github.com/signiphi/signiphi/issues)
476
+ - 💡 [Feature Requests](https://github.com/signiphi/signiphi/issues)
287
477
 
288
- See the LICENSE file in the root of the repository.
478
+ ## 🔗 Related Packages
289
479
 
290
- ## Contributing
480
+ Part of the Signiphi ecosystem:
291
481
 
292
- Contributions are welcome! Please read the contributing guidelines in the repository.
482
+ - [`@signiphi/pdf-signer`](https://www.npmjs.com/package/@signiphi/pdf-signer) - PDF signing and form filling
483
+ - [`@signiphi/document-prepare`](https://www.npmjs.com/package/@signiphi/document-prepare) - Document preparation workflow
293
484
 
294
- ## Support
485
+ ---
295
486
 
296
- For issues and questions, please visit the [GitHub repository](https://github.com/signiphi/signiphi).
487
+ Made with ❤️ by the Signiphi team
package/dist/index.js CHANGED
@@ -1624,7 +1624,7 @@ var PageAssembler = React7.forwardRef(
1624
1624
  const fileMap = /* @__PURE__ */ new Map();
1625
1625
  for (const file of state.files) {
1626
1626
  fileMap.set(file.id, file);
1627
- if (file.type === "pdf" && file.originalBytes) {
1627
+ if (file.type === "pdf" && file.originalBytes && file.originalBytes.length > 0) {
1628
1628
  try {
1629
1629
  const sourcePdf = await PDFDocument2.load(file.originalBytes);
1630
1630
  loadedPdfs.set(file.id, sourcePdf);
@@ -1672,7 +1672,8 @@ var PageAssembler = React7.forwardRef(
1672
1672
  }
1673
1673
  }
1674
1674
  const pdfBytes = await combinedDoc.save();
1675
- const blob = new Blob([pdfBytes.buffer], { type: "application/pdf" });
1675
+ const arrayBuffer = pdfBytes.buffer.slice(pdfBytes.byteOffset, pdfBytes.byteOffset + pdfBytes.byteLength);
1676
+ const blob = new Blob([arrayBuffer], { type: "application/pdf" });
1676
1677
  return blob;
1677
1678
  } catch (error) {
1678
1679
  console.error("Error assembling PDF:", error);
@@ -1686,7 +1687,7 @@ var PageAssembler = React7.forwardRef(
1686
1687
  isBusy: () => isLoading,
1687
1688
  getState: () => state
1688
1689
  }), [state, summary, isLoading]);
1689
- return /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(reactDnd.DndProvider, { backend: reactDndHtml5Backend.HTML5Backend, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("space-y-6 pb-6", className), children: [
1690
+ return /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(reactDnd.DndProvider, { backend: reactDndHtml5Backend.HTML5Backend, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("space-y-3", className), children: [
1690
1691
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1691
1692
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1692
1693
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-medium", children: "Page Assembly" }),
@@ -1777,9 +1778,9 @@ var PageAssembler = React7.forwardRef(
1777
1778
  ]
1778
1779
  }
1779
1780
  ) : !disableFileUpload && /* @__PURE__ */ jsxRuntime.jsx(
1780
- Card,
1781
+ "div",
1781
1782
  {
1782
- className: "p-12 border-dashed border-2 border-gray-300 hover:border-gray-400 transition-colors cursor-pointer",
1783
+ className: "p-12 rounded-lg bg-card text-card-foreground cursor-pointer transition-all border-2 border-dashed border-border hover:border-primary/60 hover:bg-primary/10",
1783
1784
  onClick: handleFileUpload,
1784
1785
  onDrop: handleDrop,
1785
1786
  onDragOver: handleDragOver,