hazo_pdf 1.0.0 → 1.1.0

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,13 +1,17 @@
1
1
  # Hazo PDF
2
2
 
3
- A React component library for viewing and annotating PDF documents.
3
+ A React component library for viewing and annotating PDF documents with support for Square annotations, FreeText annotations, custom stamps, timestamps, and comprehensive styling customization.
4
4
 
5
5
  ## Features
6
6
 
7
- - PDF viewing
8
- - Annotation support
9
- - Built with React, TypeScript, and TailwindCSS
10
- - Lightweight and customizable
7
+ - 📄 **PDF Viewing** - Render PDF documents with customizable zoom levels
8
+ - ✏️ **Annotations** - Square and FreeText annotation tools
9
+ - 🎨 **Customizable Styling** - Extensive configuration options via INI file
10
+ - **Timestamp Support** - Automatic timestamp appending to annotations
11
+ - 🏷️ **Custom Stamps** - Add quick-insert stamps via right-click menu
12
+ - 💾 **Annotation Persistence** - Save annotations directly into PDF files
13
+ - 🎯 **Pan Tool** - Default pan/scroll mode for document navigation
14
+ - ↪️ **Undo/Redo** - Full annotation history management
11
15
 
12
16
  ## Installation
13
17
 
@@ -15,7 +19,7 @@ A React component library for viewing and annotating PDF documents.
15
19
  npm install hazo_pdf
16
20
  ```
17
21
 
18
- ## Usage
22
+ ## Quick Start
19
23
 
20
24
  ```tsx
21
25
  import { PdfViewer } from 'hazo_pdf';
@@ -30,6 +34,911 @@ function App() {
30
34
  }
31
35
  ```
32
36
 
37
+ That's it! The PDF viewer will load and display your document with default styling and pan mode enabled.
38
+
39
+ ---
40
+
41
+ ## Examples
42
+
43
+ ### Simple Example - Basic PDF Viewer
44
+
45
+ The simplest usage - just display a PDF:
46
+
47
+ ```tsx
48
+ import { PdfViewer } from 'hazo_pdf';
49
+ import 'hazo_pdf/styles.css';
50
+
51
+ function SimpleViewer() {
52
+ return (
53
+ <div style={{ width: '100%', height: '800px' }}>
54
+ <PdfViewer
55
+ url="/api/documents/sample.pdf"
56
+ className="h-full w-full"
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
63
+ **Features demonstrated:**
64
+ - Basic PDF rendering
65
+ - Default pan tool (drag to scroll)
66
+ - Default styling
67
+
68
+ ---
69
+
70
+ ### Medium Complexity Example - With Annotations and Callbacks
71
+
72
+ A more feature-rich implementation with annotation handling:
73
+
74
+ ```tsx
75
+ import { useState } from 'react';
76
+ import { PdfViewer } from 'hazo_pdf';
77
+ import type { PdfAnnotation, PDFDocumentProxy } from 'hazo_pdf';
78
+ import 'hazo_pdf/styles.css';
79
+
80
+ function AnnotatedViewer() {
81
+ const [annotations, setAnnotations] = useState<PdfAnnotation[]>([]);
82
+ const [pdfDoc, setPdfDoc] = useState<PDFDocumentProxy | null>(null);
83
+
84
+ const handleLoad = (pdf: PDFDocumentProxy) => {
85
+ console.log('PDF loaded:', pdf.numPages, 'pages');
86
+ setPdfDoc(pdf);
87
+ };
88
+
89
+ const handleAnnotationCreate = (annotation: PdfAnnotation) => {
90
+ console.log('Annotation created:', annotation);
91
+ setAnnotations(prev => [...prev, annotation]);
92
+ };
93
+
94
+ const handleAnnotationUpdate = (annotation: PdfAnnotation) => {
95
+ console.log('Annotation updated:', annotation);
96
+ setAnnotations(prev =>
97
+ prev.map(a => a.id === annotation.id ? annotation : a)
98
+ );
99
+ };
100
+
101
+ const handleAnnotationDelete = (annotationId: string) => {
102
+ console.log('Annotation deleted:', annotationId);
103
+ setAnnotations(prev => prev.filter(a => a.id !== annotationId));
104
+ };
105
+
106
+ const handleSave = (pdfBytes: Uint8Array, filename: string) => {
107
+ // Create a blob and download
108
+ const blob = new Blob([pdfBytes], { type: 'application/pdf' });
109
+ const url = URL.createObjectURL(blob);
110
+ const a = document.createElement('a');
111
+ a.href = url;
112
+ a.download = filename || 'annotated-document.pdf';
113
+ a.click();
114
+ URL.revokeObjectURL(url);
115
+ };
116
+
117
+ const handleError = (error: Error) => {
118
+ console.error('PDF error:', error);
119
+ alert(`Failed to load PDF: ${error.message}`);
120
+ };
121
+
122
+ return (
123
+ <div style={{ width: '100%', height: '100vh' }}>
124
+ <PdfViewer
125
+ url="/api/documents/report.pdf"
126
+ className="h-full w-full"
127
+ scale={1.2}
128
+ annotations={annotations}
129
+ on_load={handleLoad}
130
+ on_error={handleError}
131
+ on_annotation_create={handleAnnotationCreate}
132
+ on_annotation_update={handleAnnotationUpdate}
133
+ on_annotation_delete={handleAnnotationDelete}
134
+ on_save={handleSave}
135
+ background_color="#f5f5f5"
136
+ />
137
+ </div>
138
+ );
139
+ }
140
+ ```
141
+
142
+ **Features demonstrated:**
143
+ - Annotation state management
144
+ - Callback handlers for all events
145
+ - Custom zoom level (1.2x)
146
+ - Custom background color
147
+ - PDF download with annotations
148
+
149
+ ---
150
+
151
+ ### Complex Example - Full Configuration with Custom Stamps and Timestamps
152
+
153
+ A production-ready implementation with configuration file, custom stamps, timestamps, and advanced features:
154
+
155
+ ```tsx
156
+ import { useState, useEffect } from 'react';
157
+ import { PdfViewer } from 'hazo_pdf';
158
+ import type { PdfAnnotation, PDFDocumentProxy } from 'hazo_pdf';
159
+ import 'hazo_pdf/styles.css';
160
+
161
+ function ProductionViewer() {
162
+ const [annotations, setAnnotations] = useState<PdfAnnotation[]>([]);
163
+ const [initialScale, setInitialScale] = useState(1.0);
164
+
165
+ // Load saved annotations from localStorage
166
+ useEffect(() => {
167
+ const saved = localStorage.getItem('pdf-annotations');
168
+ if (saved) {
169
+ try {
170
+ setAnnotations(JSON.parse(saved));
171
+ } catch (e) {
172
+ console.error('Failed to load saved annotations:', e);
173
+ }
174
+ }
175
+
176
+ // Save window size to adjust scale
177
+ const updateScale = () => {
178
+ const width = window.innerWidth;
179
+ if (width < 768) {
180
+ setInitialScale(0.75); // Mobile
181
+ } else if (width < 1024) {
182
+ setInitialScale(1.0); // Tablet
183
+ } else {
184
+ setInitialScale(1.25); // Desktop
185
+ }
186
+ };
187
+
188
+ updateScale();
189
+ window.addEventListener('resize', updateScale);
190
+ return () => window.removeEventListener('resize', updateScale);
191
+ }, []);
192
+
193
+ // Persist annotations to localStorage
194
+ useEffect(() => {
195
+ if (annotations.length > 0) {
196
+ localStorage.setItem('pdf-annotations', JSON.stringify(annotations));
197
+ }
198
+ }, [annotations]);
199
+
200
+ const handleAnnotationCreate = (annotation: PdfAnnotation) => {
201
+ setAnnotations(prev => {
202
+ const updated = [...prev, annotation];
203
+ // Save to server
204
+ fetch('/api/annotations', {
205
+ method: 'POST',
206
+ headers: { 'Content-Type': 'application/json' },
207
+ body: JSON.stringify(annotation),
208
+ }).catch(err => console.error('Failed to save annotation:', err));
209
+ return updated;
210
+ });
211
+ };
212
+
213
+ const handleAnnotationUpdate = (annotation: PdfAnnotation) => {
214
+ setAnnotations(prev => {
215
+ const updated = prev.map(a => a.id === annotation.id ? annotation : a);
216
+ // Update on server
217
+ fetch(`/api/annotations/${annotation.id}`, {
218
+ method: 'PUT',
219
+ headers: { 'Content-Type': 'application/json' },
220
+ body: JSON.stringify(annotation),
221
+ }).catch(err => console.error('Failed to update annotation:', err));
222
+ return updated;
223
+ });
224
+ };
225
+
226
+ const handleAnnotationDelete = (annotationId: string) => {
227
+ setAnnotations(prev => {
228
+ const updated = prev.filter(a => a.id !== annotationId);
229
+ // Delete on server
230
+ fetch(`/api/annotations/${annotationId}`, {
231
+ method: 'DELETE',
232
+ }).catch(err => console.error('Failed to delete annotation:', err));
233
+ return updated;
234
+ });
235
+ };
236
+
237
+ const handleSave = async (pdfBytes: Uint8Array, filename: string) => {
238
+ try {
239
+ // Upload to server
240
+ const formData = new FormData();
241
+ formData.append('file', new Blob([pdfBytes], { type: 'application/pdf' }), filename);
242
+
243
+ const response = await fetch('/api/documents/upload', {
244
+ method: 'POST',
245
+ body: formData,
246
+ });
247
+
248
+ if (response.ok) {
249
+ const result = await response.json();
250
+ alert(`PDF saved successfully: ${result.url}`);
251
+ } else {
252
+ throw new Error('Failed to upload PDF');
253
+ }
254
+ } catch (error) {
255
+ console.error('Save error:', error);
256
+ alert('Failed to save PDF');
257
+ }
258
+ };
259
+
260
+ // Custom stamps configuration
261
+ const customStamps = JSON.stringify([
262
+ {
263
+ name: "Verified",
264
+ text: "✅",
265
+ order: 1,
266
+ time_stamp_suffix_enabled: true,
267
+ fixed_text_suffix_enabled: true,
268
+ background_color: "rgb(255, 255, 255)",
269
+ border_size: 0,
270
+ font_color: "#000000",
271
+ font_weight: "bold",
272
+ font_size: 16
273
+ },
274
+ {
275
+ name: "Rejected",
276
+ text: "❌",
277
+ order: 2,
278
+ time_stamp_suffix_enabled: true,
279
+ fixed_text_suffix_enabled: false,
280
+ background_color: "rgb(255, 200, 200)",
281
+ border_size: 1,
282
+ font_color: "#000000",
283
+ font_size: 14
284
+ },
285
+ {
286
+ name: "Needs Review",
287
+ text: "⚠️",
288
+ order: 3,
289
+ time_stamp_suffix_enabled: false,
290
+ fixed_text_suffix_enabled: false,
291
+ background_color: "rgb(255, 255, 200)",
292
+ border_size: 2,
293
+ font_color: "#000000",
294
+ font_size: 12
295
+ }
296
+ ]);
297
+
298
+ return (
299
+ <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
300
+ <div style={{ padding: '1rem', background: '#f0f0f0', borderBottom: '1px solid #ccc' }}>
301
+ <h1>Document Viewer</h1>
302
+ <p>Annotations: {annotations.length} |
303
+ <button onClick={() => setAnnotations([])}>Clear All</button>
304
+ </p>
305
+ </div>
306
+
307
+ <div style={{ flex: 1, overflow: 'hidden' }}>
308
+ <PdfViewer
309
+ url="/api/documents/contract.pdf"
310
+ config_file="hazo_pdf_config.ini"
311
+ className="h-full w-full"
312
+ scale={initialScale}
313
+ annotations={annotations}
314
+ on_annotation_create={handleAnnotationCreate}
315
+ on_annotation_update={handleAnnotationUpdate}
316
+ on_annotation_delete={handleAnnotationDelete}
317
+ on_save={handleSave}
318
+ append_timestamp_to_text_edits={true}
319
+ annotation_text_suffix_fixed_text="Reviewer"
320
+ right_click_custom_stamps={customStamps}
321
+ background_color="#ffffff"
322
+ />
323
+ </div>
324
+ </div>
325
+ );
326
+ }
327
+ ```
328
+
329
+ **Features demonstrated:**
330
+ - Configuration file integration (`hazo_pdf_config.ini`)
331
+ - Custom stamps with styling
332
+ - Timestamp and fixed text suffixes
333
+ - Responsive scaling based on screen size
334
+ - LocalStorage persistence
335
+ - Server synchronization
336
+ - Complex state management
337
+ - Custom UI wrapper
338
+
339
+ ---
340
+
341
+ ## Configuration File
342
+
343
+ The PDF viewer can be configured via an INI file (default: `hazo_pdf_config.ini`). This allows you to customize styling, colors, fonts, and behavior without modifying code.
344
+
345
+ **Basic setup:**
346
+
347
+ ```ini
348
+ [viewer]
349
+ viewer_background_color = #f5f5f5
350
+ append_timestamp_to_text_edits = true
351
+ annotation_text_suffix_fixed_text = user_x
352
+
353
+ [freetext_annotation]
354
+ freetext_text_color = #0066cc
355
+ freetext_background_color = rgb(230, 243, 255)
356
+ freetext_background_opacity = 0.1
357
+ freetext_border_color = #003366
358
+ freetext_border_width = 1
359
+
360
+ [context_menu]
361
+ right_click_custom_stamps = [{"name":"Verified","text":"✅","order":1,"time_stamp_suffix_enabled":true,"fixed_text_suffix_enabled":true}]
362
+ ```
363
+
364
+ See `hazo_pdf_config.ini` in the project root for all available configuration options.
365
+
366
+ ---
367
+
368
+ ## API Reference
369
+
370
+ ### PdfViewer Props
371
+
372
+ #### Required Props
373
+
374
+ | Prop | Type | Description |
375
+ |------|------|-------------|
376
+ | `url` | `string` | URL or path to the PDF file. Can be a relative path, absolute URL, or API endpoint. |
377
+
378
+ #### Optional Props
379
+
380
+ ##### Basic Configuration
381
+
382
+ | Prop | Type | Default | Description |
383
+ |------|------|---------|-------------|
384
+ | `className` | `string` | `""` | Additional CSS classes to apply to the viewer container. |
385
+ | `scale` | `number` | `1.0` | Initial zoom level. Values > 1.0 zoom in, < 1.0 zoom out. |
386
+ | `background_color` | `string` | `"#2d2d2d"` | Background color for areas outside PDF pages (hex format: `#RRGGBB`). Overrides config file value. |
387
+ | `config_file` | `string` | `undefined` | Path to configuration INI file (e.g., `"hazo_pdf_config.ini"`). If not provided, uses default configuration. |
388
+
389
+ ##### Event Callbacks
390
+
391
+ | Prop | Type | Description |
392
+ |------|------|-------------|
393
+ | `on_load` | `(pdf: PDFDocumentProxy) => void` | Called when PDF is successfully loaded. Receives the PDF document proxy with metadata (page count, etc.). |
394
+ | `on_error` | `(error: Error) => void` | Called when an error occurs (PDF load failure, rendering error, etc.). Receives the error object. |
395
+ | `on_save` | `(pdf_bytes: Uint8Array, filename: string) => void` | Called when user clicks the Save button. Receives the PDF bytes with annotations embedded and a suggested filename. You can create a Blob and trigger download, or upload to a server. |
396
+
397
+ ##### Annotation Management
398
+
399
+ | Prop | Type | Default | Description |
400
+ |------|------|---------|-------------|
401
+ | `annotations` | `PdfAnnotation[]` | `[]` | Array of existing annotations to display. Used to restore saved annotations or sync from a server. |
402
+ | `on_annotation_create` | `(annotation: PdfAnnotation) => void` | `undefined` | Called when a new annotation is created (Square or FreeText). Use this to persist annotations to your backend. |
403
+ | `on_annotation_update` | `(annotation: PdfAnnotation) => void` | `undefined` | Called when an existing annotation is edited (text content changed). Use this to sync updates to your backend. |
404
+ | `on_annotation_delete` | `(annotation_id: string) => void` | `undefined` | Called when an annotation is deleted. Receives the annotation ID. Use this to remove annotations from your backend. |
405
+
406
+ ##### Timestamp and Suffix Configuration
407
+
408
+ | Prop | Type | Default | Description |
409
+ |------|------|---------|-------------|
410
+ | `append_timestamp_to_text_edits` | `boolean` | `false` | If `true`, automatically appends a timestamp to all FreeText annotations when created or edited. Format: `[YYYY-MM-DD h:mmam/pm]`. Overrides config file value. |
411
+ | `annotation_text_suffix_fixed_text` | `string` | `""` | Fixed text string to append before the timestamp (if timestamps are enabled). This text will be enclosed in brackets. Overrides config file value. |
412
+
413
+ ##### Sidepanel Metadata
414
+
415
+ | Prop | Type | Default | Description |
416
+ |------|------|---------|-------------|
417
+ | `sidepanel_metadata_enabled` | `boolean` | `false` | If `true`, enables the metadata sidepanel on the right side of the viewer. The panel can be toggled from the toolbar or right edge button. |
418
+ | `metadata_input` | `MetadataInput` | `undefined` | Metadata structure with header, data (accordions), and footer sections. Each section supports different format types (h1-h5, body) and editable fields. See [Sidepanel Metadata](#sidepanel-metadata) section for details. |
419
+ | `on_metadata_change` | `(updatedRow: MetadataDataItem, allData: MetadataInput) => { updatedRow: MetadataDataItem; allData: MetadataInput }` | `undefined` | Callback when a metadata field is edited. Receives the updated row and complete metadata structure. Must return both parameters. Use this to persist metadata changes to your backend. |
420
+
421
+ ##### Custom Stamps
422
+
423
+ | Prop | Type | Default | Description |
424
+ |------|------|---------|-------------|
425
+ | `right_click_custom_stamps` | `string` | `undefined` | JSON array string defining custom stamp menu items for the right-click context menu. Each stamp appears as a menu item that, when clicked, adds predefined text to the PDF. Overrides config file value. |
426
+
427
+ **Custom Stamp JSON Format:**
428
+
429
+ ```typescript
430
+ [
431
+ {
432
+ name: string; // Menu item label (required)
433
+ text: string; // Text to add to PDF (required)
434
+ order: number; // Menu position, lower = higher (required)
435
+ time_stamp_suffix_enabled?: boolean; // Append timestamp? (default: false)
436
+ fixed_text_suffix_enabled?: boolean; // Append fixed text? (default: false)
437
+ background_color?: string; // Hex or rgb() format (default: config)
438
+ border_size?: number; // Pixels, 0 = no border (default: config)
439
+ font_color?: string; // Hex or rgb() format (default: config)
440
+ font_weight?: string; // CSS font-weight (default: config)
441
+ font_style?: string; // CSS font-style (default: config)
442
+ font_size?: number; // Pixels (default: config)
443
+ font_name?: string; // CSS font-family (default: config)
444
+ }
445
+ ]
446
+ ```
447
+
448
+ **Example:**
449
+
450
+ ```tsx
451
+ const stamps = JSON.stringify([
452
+ {
453
+ name: "Approved",
454
+ text: "✓",
455
+ order: 1,
456
+ time_stamp_suffix_enabled: true,
457
+ background_color: "rgb(200, 255, 200)",
458
+ border_size: 1,
459
+ font_color: "#000000",
460
+ font_weight: "bold",
461
+ font_size: 16
462
+ }
463
+ ]);
464
+
465
+ <PdfViewer
466
+ url="/document.pdf"
467
+ right_click_custom_stamps={stamps}
468
+ />
469
+ ```
470
+
471
+ ### PdfAnnotation Interface
472
+
473
+ Represents a PDF annotation in the standard PDF coordinate space.
474
+
475
+ ```typescript
476
+ interface PdfAnnotation {
477
+ id: string; // Unique identifier (auto-generated)
478
+ type: 'Square' | 'FreeText' | 'Highlight' | 'CustomBookmark';
479
+ page_index: number; // Zero-based page number
480
+ rect: [number, number, number, number]; // PDF coordinates [x1, y1, x2, y2]
481
+ author: string; // Author name (default: "User")
482
+ date: string; // ISO date string
483
+ contents: string; // Annotation text/content
484
+ color?: string; // Color in hex format (e.g., "#FF0000")
485
+ subject?: string; // Optional subject/title
486
+ flags?: string; // Optional PDF flags
487
+ }
488
+ ```
489
+
490
+ ### PDFDocumentProxy
491
+
492
+ Type from `pdfjs-dist`. Contains PDF metadata and page proxies.
493
+
494
+ **Common properties:**
495
+ - `numPages: number` - Total number of pages
496
+ - `getPage(pageNumber: number): Promise<PDFPageProxy>` - Get a specific page
497
+
498
+ ---
499
+
500
+ ## Toolbar Controls
501
+
502
+ The PDF viewer includes a toolbar at the top with the following controls:
503
+
504
+ ### Zoom Controls
505
+
506
+ - **Zoom In (+ button)**: Increases zoom level by 0.25x increments (max 3.0x)
507
+ - **Zoom Out (- button)**: Decreases zoom level by 0.25x increments (min 0.5x)
508
+ - **Reset Zoom button**: Resets zoom to 1.0x (100%)
509
+ - **Zoom level display**: Shows current zoom percentage (e.g., "125%")
510
+
511
+ **Usage:**
512
+ - Click the `+` or `-` buttons to adjust zoom
513
+ - Click "Reset" to return to default zoom
514
+ - Zoom affects the PDF page size but maintains aspect ratio
515
+
516
+ ### Annotation Tools
517
+
518
+ - **Square button**: Activates square/rectangle annotation tool
519
+ - Click and drag on the PDF to create a rectangle
520
+ - Right-click the rectangle to add a comment
521
+ - Button highlights when active
522
+
523
+ ### History Controls
524
+
525
+ - **Undo button**: Reverses the last annotation action
526
+ - Keyboard shortcut: `Ctrl+Z` (Windows/Linux) or `Cmd+Z` (Mac)
527
+ - Disabled when there are no actions to undo
528
+ - Shows history icon
529
+
530
+ - **Redo button**: Reapplies the last undone action
531
+ - Keyboard shortcut: `Ctrl+Y` (Windows/Linux) or `Cmd+Y` (Mac)
532
+ - Disabled when there are no actions to redo
533
+ - Shows redo icon
534
+
535
+ ### Save Control
536
+
537
+ - **Save button**: Saves all annotations directly into the PDF file
538
+ - Disabled when there are no annotations
539
+ - Shows save icon and "Saving..." text during save operation
540
+ - Triggers `on_save` callback with PDF bytes and filename
541
+
542
+ ### Metadata Panel Toggle
543
+
544
+ - **Metadata button**: Toggles the metadata sidepanel (only visible when `sidepanel_metadata_enabled={true}`)
545
+ - Shows panel icon
546
+ - Button highlights when panel is open
547
+ - Opens/closes the right-side metadata panel
548
+
549
+ ---
550
+
551
+ ## Sidepanel Metadata
552
+
553
+ The PDF viewer can display a retractable sidepanel on the right side showing JSON metadata with header, data (accordions), and footer sections.
554
+
555
+ ### Enabling the Sidepanel
556
+
557
+ To enable the metadata sidepanel, set `sidepanel_metadata_enabled={true}` and provide `metadata_input`:
558
+
559
+ ```tsx
560
+ import { PdfViewer } from 'hazo_pdf';
561
+ import type { MetadataInput } from 'hazo_pdf';
562
+
563
+ const metadata: MetadataInput = {
564
+ header: [
565
+ { style: 'h1', label: 'Document Information' },
566
+ { style: 'body', label: 'Last updated: 2025-01-15' }
567
+ ],
568
+ data: [
569
+ {
570
+ label: 'Document Title',
571
+ style: 'h3',
572
+ value: 'Annual Report 2024',
573
+ editable: true
574
+ },
575
+ {
576
+ label: 'Author',
577
+ style: 'h4',
578
+ value: 'John Doe',
579
+ editable: true
580
+ },
581
+ {
582
+ label: 'Status',
583
+ style: 'body',
584
+ value: 'Draft',
585
+ editable: false
586
+ }
587
+ ],
588
+ footer: [
589
+ { style: 'body', label: 'Version 1.0' }
590
+ ]
591
+ };
592
+
593
+ <PdfViewer
594
+ url="/document.pdf"
595
+ sidepanel_metadata_enabled={true}
596
+ metadata_input={metadata}
597
+ on_metadata_change={(updatedRow, allData) => {
598
+ console.log('Updated row:', updatedRow);
599
+ console.log('All data:', allData);
600
+ // Save to server, update state, etc.
601
+ return { updatedRow, allData };
602
+ }}
603
+ />
604
+ ```
605
+
606
+ ### Metadata Structure
607
+
608
+ The `MetadataInput` interface has three sections:
609
+
610
+ #### Header Section
611
+ - Array of `MetadataHeaderItem` objects
612
+ - Each item has `style` (format type) and `label` (text)
613
+ - Rendered at the top of the panel
614
+
615
+ #### Data Section
616
+ - Array of `MetadataDataItem` objects
617
+ - Each item is rendered as a collapsible accordion (starts collapsed)
618
+ - Properties:
619
+ - `label`: Title shown as accordion header (required)
620
+ - `style`: Format type for the label (h1-h5, body) (required)
621
+ - `value`: Value to display (required)
622
+ - `editable`: Whether field can be edited (boolean, required)
623
+
624
+ #### Footer Section
625
+ - Array of `MetadataFooterItem` objects (same structure as header)
626
+ - Rendered at the bottom of the panel
627
+
628
+ ### Format Types
629
+
630
+ The `style` property accepts the following format types:
631
+ - `h1`: Large heading (3xl, bold)
632
+ - `h2`: Heading (2xl, bold)
633
+ - `h3`: Subheading (xl, semibold)
634
+ - `h4`: Smaller subheading (lg, semibold)
635
+ - `h5`: Small heading (base, semibold)
636
+ - `body`: Regular text (base)
637
+
638
+ ### Editable Fields
639
+
640
+ When `editable: true`:
641
+ - A pencil icon appears next to the value
642
+ - Clicking the pencil enters edit mode
643
+ - Edit mode shows:
644
+ - Text input field (single-line)
645
+ - Green circle-check button (save)
646
+ - Red circle-x button (cancel)
647
+ - Pressing `Enter` saves, `Escape` cancels
648
+ - On save, `on_metadata_change` callback is called with:
649
+ - `updatedRow`: The updated `MetadataDataItem`
650
+ - `allData`: The complete `MetadataInput` with all updates
651
+ - Callback must return `{ updatedRow, allData }`
652
+
653
+ ### Panel Controls
654
+
655
+ - **Toggle from toolbar**: Click the "Metadata" button in the toolbar
656
+ - **Toggle from right edge**: Click the chevron button on the right edge (when closed)
657
+ - **Resize**: Drag the left edge of the panel to resize (200px - 800px on desktop)
658
+ - **Close**: Click the chevron button in the panel header or toggle button in toolbar
659
+
660
+ ### Responsive Behavior
661
+
662
+ - **Desktop (> 1024px)**: Panel appears side-by-side with PDF viewer, max width 800px
663
+ - **Tablet (768px - 1024px)**: Panel max width is 50% of screen, can overlay
664
+ - **Mobile (< 768px)**: Panel uses overlay mode, max width 90vw, full height
665
+
666
+ ### Example: Complex Metadata (Test App Example)
667
+
668
+ The test app includes comprehensive test data demonstrating all metadata variations. This example shows all format types (h1-h5, body), editable and non-editable fields:
669
+
670
+ ```tsx
671
+ import type { MetadataInput, MetadataDataItem } from 'hazo_pdf';
672
+
673
+ const test_metadata: MetadataInput = {
674
+ header: [
675
+ { style: 'h1', label: 'Document Information' },
676
+ { style: 'h3', label: 'Test Document Metadata' },
677
+ { style: 'body', label: 'Last updated: 2025-01-15' }
678
+ ],
679
+ data: [
680
+ {
681
+ label: 'Document Title',
682
+ style: 'h2',
683
+ value: 'Annual Report 2024',
684
+ editable: true
685
+ },
686
+ {
687
+ label: 'Author Information',
688
+ style: 'h3',
689
+ value: 'John Doe\nSenior Analyst\nDepartment of Finance',
690
+ editable: true
691
+ },
692
+ {
693
+ label: 'Document Status',
694
+ style: 'h4',
695
+ value: 'Approved',
696
+ editable: false
697
+ },
698
+ {
699
+ label: 'Version',
700
+ style: 'h5',
701
+ value: '1.0.0',
702
+ editable: true
703
+ },
704
+ {
705
+ label: 'Category',
706
+ style: 'body',
707
+ value: 'Financial Report',
708
+ editable: false
709
+ },
710
+ {
711
+ label: 'Keywords',
712
+ style: 'body',
713
+ value: 'financial, annual, report, 2024',
714
+ editable: true
715
+ },
716
+ {
717
+ label: 'Document ID',
718
+ style: 'h5',
719
+ value: 'DOC-2024-001',
720
+ editable: false
721
+ },
722
+ {
723
+ label: 'Notes',
724
+ style: 'body',
725
+ value: 'This document contains comprehensive financial data for the fiscal year 2024. Please review all sections carefully before finalizing.',
726
+ editable: true
727
+ },
728
+ {
729
+ label: 'Confidentiality Level',
730
+ style: 'h4',
731
+ value: 'Internal Use Only',
732
+ editable: false
733
+ },
734
+ {
735
+ label: 'Approval Date',
736
+ style: 'body',
737
+ value: '2025-01-10',
738
+ editable: false
739
+ }
740
+ ],
741
+ footer: [
742
+ { style: 'body', label: 'This is a test metadata example' },
743
+ { style: 'h5', label: 'Version 1.0 - Test App' }
744
+ ]
745
+ };
746
+
747
+ <PdfViewer
748
+ url="/document.pdf"
749
+ sidepanel_metadata_enabled={true}
750
+ metadata_input={test_metadata}
751
+ on_metadata_change={(updatedRow, allData) => {
752
+ console.log('Updated:', updatedRow);
753
+ console.log('All data:', allData);
754
+ // Update state, save to backend, etc.
755
+ return { updatedRow, allData };
756
+ }}
757
+ />
758
+ ```
759
+
760
+ **This example demonstrates:**
761
+ - **Header section**: Multiple format types (h1, h3, body)
762
+ - **Data section**: All format types (h2, h3, h4, h5, body) with both editable and non-editable fields
763
+ - **Footer section**: Multiple format types (body, h5)
764
+ - **Multi-line values**: Author Information includes newlines
765
+ - **Long text values**: Notes field contains a longer description
766
+ - **Mixed editability**: Some fields editable, others read-only
767
+
768
+ **Note:** This test data is used in the test app (`app/viewer/[filename]/page.tsx`) to demonstrate all sidepanel metadata features.
769
+
770
+ ---
771
+
772
+ ## Usage Tips
773
+
774
+ ### Styling
775
+
776
+ - The viewer uses TailwindCSS classes. Ensure TailwindCSS is configured in your project or import the bundled styles.
777
+ - Use the `className` prop to add custom wrapper styles.
778
+ - Use the configuration file for comprehensive styling customization.
779
+
780
+ ### Annotation Coordinate System
781
+
782
+ - Annotations use PDF coordinate space (points), not screen pixels.
783
+ - PDF coordinates start at the bottom-left (Y increases upward).
784
+ - The component handles coordinate conversion automatically.
785
+
786
+ ### Pan Tool
787
+
788
+ - Pan is the default tool (`current_tool = null`).
789
+ - Users can drag to scroll/pan the document.
790
+ - The cursor changes to a hand icon when panning.
791
+
792
+ ### Square Annotations
793
+
794
+ 1. Click the "Square" button in the toolbar.
795
+ 2. Click and drag on the PDF to create a rectangle.
796
+ 3. Right-click the rectangle to add a comment.
797
+
798
+ ### FreeText Annotations
799
+
800
+ 1. Right-click anywhere on the PDF.
801
+ 2. Select "Annotate" from the context menu.
802
+ 3. Enter text in the dialog.
803
+ 4. Click the checkmark to save.
804
+ 5. Left-click an existing FreeText annotation to edit or delete it.
805
+
806
+ ### Custom Stamps
807
+
808
+ 1. Configure stamps via `right_click_custom_stamps` prop or config file.
809
+ 2. Right-click on the PDF.
810
+ 3. Select a stamp from the bottom of the menu.
811
+ 4. The stamp text is added at the click position with optional timestamp/fixed text.
812
+
813
+ ### Saving Annotations
814
+
815
+ - Click the "Save" button in the toolbar.
816
+ - The `on_save` callback receives PDF bytes with annotations embedded using `pdf-lib`.
817
+ - You can download directly or upload to a server.
818
+
819
+ **Example download:**
820
+
821
+ ```tsx
822
+ const handleSave = (pdfBytes: Uint8Array, filename: string) => {
823
+ const blob = new Blob([pdfBytes], { type: 'application/pdf' });
824
+ const url = URL.createObjectURL(blob);
825
+ const a = document.createElement('a');
826
+ a.href = url;
827
+ a.download = filename || 'document.pdf';
828
+ a.click();
829
+ URL.revokeObjectURL(url);
830
+ };
831
+ ```
832
+
833
+ ### Timestamp Formatting
834
+
835
+ When `append_timestamp_to_text_edits` is enabled, timestamps are formatted as:
836
+ - Format: `YYYY-MM-DD h:mmam/pm`
837
+ - Example: `2025-11-17 2:24pm`
838
+
839
+ The position and bracket style can be configured via the config file:
840
+ - `suffix_text_position`: `"adjacent"`, `"below_single_line"`, or `"below_multi_line"` (default)
841
+ - `suffix_enclosing_brackets`: Two-character string like `"[]"`, `"()"`, `"{}"` (default: `"[]"`)
842
+ - `add_enclosing_brackets_to_suffixes`: `true` or `false` (default: `true`)
843
+
844
+ ### Responsive Design
845
+
846
+ The PDF viewer is fully responsive and adapts to different screen sizes:
847
+
848
+ **Desktop (> 1024px):**
849
+ - Full feature set available
850
+ - Sidepanel appears side-by-side with PDF (when enabled)
851
+ - All toolbar buttons visible
852
+ - Optimal viewing experience
853
+
854
+ **Tablet (768px - 1024px):**
855
+ - Sidepanel max width is 50% of screen (when enabled)
856
+ - Toolbar buttons may wrap to multiple rows
857
+ - Touch-friendly button sizes
858
+ - Horizontal scrolling available for zoomed PDFs
859
+
860
+ **Mobile (< 768px):**
861
+ - Sidepanel uses overlay mode (when enabled), max 90vw width
862
+ - Toolbar buttons wrap and use touch-friendly sizes (min 44px height)
863
+ - PDF viewer maintains full functionality
864
+ - Pan tool works with touch gestures
865
+ - All annotation tools remain accessible
866
+
867
+ **Breakpoints:**
868
+ - Mobile: `< 768px`
869
+ - Tablet: `768px - 1024px`
870
+ - Desktop: `> 1024px`
871
+
872
+ The viewer uses CSS media queries to adjust layout and component sizes automatically. All interactive elements are touch-friendly on mobile devices.
873
+
874
+ ---
875
+
876
+ ### Annotation Coordinate System
877
+
878
+ - Annotations use PDF coordinate space (points), not screen pixels.
879
+ - PDF coordinates start at the bottom-left (Y increases upward).
880
+ - The component handles coordinate conversion automatically.
881
+
882
+ ### Pan Tool
883
+
884
+ - Pan is the default tool (`current_tool = null`).
885
+ - Users can drag to scroll/pan the document.
886
+ - The cursor changes to a hand icon when panning.
887
+
888
+ ### Square Annotations
889
+
890
+ 1. Click the "Square" button in the toolbar.
891
+ 2. Click and drag on the PDF to create a rectangle.
892
+ 3. Right-click the rectangle to add a comment.
893
+
894
+ ### FreeText Annotations
895
+
896
+ 1. Right-click anywhere on the PDF.
897
+ 2. Select "Annotate" from the context menu.
898
+ 3. Enter text in the dialog.
899
+ 4. Click the checkmark to save.
900
+ 5. Left-click an existing FreeText annotation to edit or delete it.
901
+
902
+ ### Custom Stamps
903
+
904
+ 1. Configure stamps via `right_click_custom_stamps` prop or config file.
905
+ 2. Right-click on the PDF.
906
+ 3. Select a stamp from the bottom of the menu.
907
+ 4. The stamp text is added at the click position with optional timestamp/fixed text.
908
+
909
+ ### Saving Annotations
910
+
911
+ - Click the "Save" button in the toolbar.
912
+ - The `on_save` callback receives PDF bytes with annotations embedded using `pdf-lib`.
913
+ - You can download directly or upload to a server.
914
+
915
+ **Example download:**
916
+
917
+ ```tsx
918
+ const handleSave = (pdfBytes: Uint8Array, filename: string) => {
919
+ const blob = new Blob([pdfBytes], { type: 'application/pdf' });
920
+ const url = URL.createObjectURL(blob);
921
+ const a = document.createElement('a');
922
+ a.href = url;
923
+ a.download = filename || 'document.pdf';
924
+ a.click();
925
+ URL.revokeObjectURL(url);
926
+ };
927
+ ```
928
+
929
+ ### Timestamp Formatting
930
+
931
+ When `append_timestamp_to_text_edits` is enabled, timestamps are formatted as:
932
+ - Format: `YYYY-MM-DD h:mmam/pm`
933
+ - Example: `2025-11-17 2:24pm`
934
+
935
+ The position and bracket style can be configured via the config file:
936
+ - `suffix_text_position`: `"adjacent"`, `"below_single_line"`, or `"below_multi_line"` (default)
937
+ - `suffix_enclosing_brackets`: Two-character string like `"[]"`, `"()"`, `"{}"` (default: `"[]"`)
938
+ - `add_enclosing_brackets_to_suffixes`: `true` or `false` (default: `true`)
939
+
940
+ ---
941
+
33
942
  ## Development
34
943
 
35
944
  ### Setup
@@ -44,13 +953,26 @@ npm install
44
953
  npm run build
45
954
  ```
46
955
 
956
+ This builds the library to `dist/` using `tsup`.
957
+
47
958
  ### Watch Mode
48
959
 
49
960
  ```bash
50
961
  npm run dev
51
962
  ```
52
963
 
964
+ This runs `tsup` in watch mode, rebuilding on file changes.
965
+
966
+ ### Test App
967
+
968
+ ```bash
969
+ npm run test-app:dev
970
+ ```
971
+
972
+ Starts the Next.js test application (if `test_app_enabled = true` in config).
973
+
974
+ ---
975
+
53
976
  ## License
54
977
 
55
978
  MIT © Pubs Abayasiri
56
-