@servicetitan/dte-pdf-editor 1.77.0 → 1.78.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.
Files changed (95) hide show
  1. package/dist/components/pdf-canvas/canvas-controls.d.ts +24 -0
  2. package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -0
  3. package/dist/components/pdf-canvas/canvas-controls.js +22 -0
  4. package/dist/components/pdf-canvas/canvas-controls.js.map +1 -0
  5. package/dist/components/pdf-canvas/pdf-canvas.d.ts +1 -0
  6. package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
  7. package/dist/components/pdf-canvas/pdf-canvas.js +6 -3
  8. package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
  9. package/dist/components/pdf-editor/pdf-editor.d.ts +1 -0
  10. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  11. package/dist/components/pdf-editor/pdf-editor.js +2 -2
  12. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  13. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts +1 -0
  14. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts.map +1 -1
  15. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +2 -2
  16. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  17. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts +1 -0
  18. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  19. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +4 -1
  20. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  21. package/dist/components/pdf-view/pdf-view-field-container.d.ts +1 -0
  22. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  23. package/dist/components/pdf-view/pdf-view-field-container.js +5 -4
  24. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  25. package/dist/components/pdf-view/pdf-view.d.ts +2 -0
  26. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  27. package/dist/components/pdf-view/pdf-view.js +29 -5
  28. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  29. package/dist/constants/pdf-editor.constants.d.ts +4 -0
  30. package/dist/constants/pdf-editor.constants.d.ts.map +1 -1
  31. package/dist/constants/pdf-editor.constants.js +4 -0
  32. package/dist/constants/pdf-editor.constants.js.map +1 -1
  33. package/dist/hooks/index.d.ts +1 -0
  34. package/dist/hooks/index.d.ts.map +1 -1
  35. package/dist/hooks/index.js +1 -0
  36. package/dist/hooks/index.js.map +1 -1
  37. package/dist/hooks/useFieldDrag.d.ts +2 -1
  38. package/dist/hooks/useFieldDrag.d.ts.map +1 -1
  39. package/dist/hooks/useFieldDrag.js +7 -6
  40. package/dist/hooks/useFieldDrag.js.map +1 -1
  41. package/dist/hooks/useFieldResize.d.ts +2 -1
  42. package/dist/hooks/useFieldResize.d.ts.map +1 -1
  43. package/dist/hooks/useFieldResize.js +7 -3
  44. package/dist/hooks/useFieldResize.js.map +1 -1
  45. package/dist/hooks/useZoom.d.ts +11 -0
  46. package/dist/hooks/useZoom.d.ts.map +1 -0
  47. package/dist/hooks/useZoom.js +14 -0
  48. package/dist/hooks/useZoom.js.map +1 -0
  49. package/dist/styles/pdf-canvas.css +24 -5
  50. package/dist/styles/pdf-editor.css +1 -0
  51. package/dist/styles/pdf-field-overlay.css +14 -0
  52. package/dist/styles/variables.css +10 -1
  53. package/dist/utils/field/field-drag.utils.d.ts +4 -2
  54. package/dist/utils/field/field-drag.utils.d.ts.map +1 -1
  55. package/dist/utils/field/field-drag.utils.js +14 -10
  56. package/dist/utils/field/field-drag.utils.js.map +1 -1
  57. package/dist/utils/field/field-reading-order.utils.d.ts +15 -0
  58. package/dist/utils/field/field-reading-order.utils.d.ts.map +1 -0
  59. package/dist/utils/field/field-reading-order.utils.js +33 -0
  60. package/dist/utils/field/field-reading-order.utils.js.map +1 -0
  61. package/dist/utils/field/field-resize.utils.d.ts +4 -2
  62. package/dist/utils/field/field-resize.utils.d.ts.map +1 -1
  63. package/dist/utils/field/field-resize.utils.js +12 -10
  64. package/dist/utils/field/field-resize.utils.js.map +1 -1
  65. package/dist/utils/field/get-field-bounds-style.utils.d.ts +7 -0
  66. package/dist/utils/field/get-field-bounds-style.utils.d.ts.map +1 -1
  67. package/dist/utils/field/get-field-bounds-style.utils.js +5 -5
  68. package/dist/utils/field/get-field-bounds-style.utils.js.map +1 -1
  69. package/dist/utils/field/index.d.ts +1 -0
  70. package/dist/utils/field/index.d.ts.map +1 -1
  71. package/dist/utils/field/index.js +1 -0
  72. package/dist/utils/field/index.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/components/pdf-canvas/canvas-controls.tsx +183 -0
  75. package/src/components/pdf-canvas/pdf-canvas.tsx +64 -33
  76. package/src/components/pdf-editor/pdf-editor.tsx +12 -2
  77. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +3 -0
  78. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +5 -0
  79. package/src/components/pdf-view/pdf-view-field-container.tsx +6 -2
  80. package/src/components/pdf-view/pdf-view.tsx +78 -5
  81. package/src/constants/pdf-editor.constants.ts +5 -0
  82. package/src/hooks/index.ts +1 -0
  83. package/src/hooks/useFieldDrag.ts +10 -5
  84. package/src/hooks/useFieldResize.ts +16 -2
  85. package/src/hooks/useZoom.ts +25 -0
  86. package/src/styles/pdf-canvas.css +24 -5
  87. package/src/styles/pdf-editor.css +1 -0
  88. package/src/styles/pdf-field-overlay.css +14 -0
  89. package/src/styles/variables.css +10 -1
  90. package/src/utils/field/__tests__/field-reading-order.utils.test.ts +62 -0
  91. package/src/utils/field/field-drag.utils.ts +14 -8
  92. package/src/utils/field/field-reading-order.utils.ts +43 -0
  93. package/src/utils/field/field-resize.utils.ts +12 -8
  94. package/src/utils/field/get-field-bounds-style.utils.ts +17 -5
  95. package/src/utils/field/index.ts +1 -0
@@ -10,6 +10,7 @@ interface UseFieldResizeOptions {
10
10
  newHeight: number,
11
11
  pageNumber: number,
12
12
  ) => void;
13
+ zoom?: number;
13
14
  }
14
15
 
15
16
  export interface UseFieldResizeReturn {
@@ -24,6 +25,7 @@ export interface UseFieldResizeReturn {
24
25
  export const useFieldResize = ({
25
26
  onFieldResize,
26
27
  pdfWrapperRef,
28
+ zoom = 1,
27
29
  }: UseFieldResizeOptions): UseFieldResizeReturn => {
28
30
  const resizeStartDataRef = useRef<{
29
31
  field: PdfField;
@@ -34,12 +36,17 @@ export const useFieldResize = ({
34
36
  page: number;
35
37
  } | null>(null);
36
38
  const onFieldResizeRef = useRef(onFieldResize);
39
+ const zoomRef = useRef(zoom);
37
40
  const [isResizing, setIsResizing] = useState(false);
38
41
 
39
42
  useEffect(() => {
40
43
  onFieldResizeRef.current = onFieldResize;
41
44
  }, [onFieldResize]);
42
45
 
46
+ useEffect(() => {
47
+ zoomRef.current = zoom;
48
+ }, [zoom]);
49
+
43
50
  useEffect(() => {
44
51
  if (!isResizing) {
45
52
  return;
@@ -54,7 +61,14 @@ export const useFieldResize = ({
54
61
  }
55
62
 
56
63
  const { field, ...resizeStartData } = resizeStartDataRef.current;
57
- handleFieldResize(e, field, resizeStartData, pdfWrapperRef, onFieldResizeRef.current);
64
+ handleFieldResize(
65
+ e,
66
+ field,
67
+ resizeStartData,
68
+ pdfWrapperRef,
69
+ onFieldResizeRef.current,
70
+ zoomRef.current,
71
+ );
58
72
  };
59
73
 
60
74
  const handleMouseUp = () => {
@@ -78,7 +92,7 @@ export const useFieldResize = ({
78
92
  e.stopPropagation();
79
93
  e.preventDefault();
80
94
 
81
- const resizeStartData = handleFieldResizeStart(e, field, pdfWrapperRef);
95
+ const resizeStartData = handleFieldResizeStart(e, field, pdfWrapperRef, zoom);
82
96
  if (resizeStartData) {
83
97
  resizeStartDataRef.current = {
84
98
  field,
@@ -0,0 +1,25 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { ZOOM_DEFAULT, ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from '../constants';
3
+
4
+ export interface UseZoomReturn {
5
+ zoom: number;
6
+ zoomIn(): void;
7
+ zoomOut(): void;
8
+ resetZoom(): void;
9
+ }
10
+
11
+ const clampZoom = (value: number) =>
12
+ Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(value * 100) / 100));
13
+
14
+ /**
15
+ * Manages a page zoom factor (1 = 100%), clamped to [ZOOM_MIN, ZOOM_MAX] in ZOOM_STEP increments.
16
+ */
17
+ export const useZoom = (initialZoom: number = ZOOM_DEFAULT): UseZoomReturn => {
18
+ const [zoom, setZoom] = useState<number>(initialZoom);
19
+
20
+ const zoomIn = useCallback(() => setZoom(prev => clampZoom(prev + ZOOM_STEP)), []);
21
+ const zoomOut = useCallback(() => setZoom(prev => clampZoom(prev - ZOOM_STEP)), []);
22
+ const resetZoom = useCallback(() => setZoom(ZOOM_DEFAULT), []);
23
+
24
+ return { zoom, zoomIn, zoomOut, resetZoom };
25
+ };
@@ -1,11 +1,30 @@
1
- .dte-pdf-canvas-container {
2
- overflow: auto;
3
- position: relative;
4
- }
5
-
6
1
  .dte-pdf-wrapper {
7
2
  position: relative;
8
3
  width: 100%;
9
4
  margin-inline-start: auto;
10
5
  margin-inline-end: auto;
11
6
  }
7
+
8
+ .dte-pdf-canvas-controls {
9
+ z-index: 1;
10
+ position: sticky;
11
+ top: 0;
12
+ left: 0;
13
+ background: var(--bg-color-main, #fafafa);
14
+ box-shadow: 0 2px 4px rgb(0 0 0 / 15%);
15
+ padding: var(--spacing-half) var(--spacing-1);
16
+ }
17
+
18
+ .dte-pdf-zoom-controls-value {
19
+ min-width: 44px;
20
+ text-align: center;
21
+ cursor: pointer;
22
+ user-select: none;
23
+ }
24
+
25
+ .dte-pdf-zoom-controls,
26
+ .dte-pdf-field-navigation-controls {
27
+ border-radius: var(--radius-1, 4px);
28
+ padding: var(--spacing-half);
29
+ background-color: var(--bg-color-secondary, #eeeeee);
30
+ }
@@ -11,5 +11,6 @@
11
11
 
12
12
  .dte-pdf-editor-content-container {
13
13
  max-width: 100%;
14
+ min-width: 0;
14
15
  overflow: auto;
15
16
  }
@@ -101,6 +101,15 @@
101
101
  line-height: 1;
102
102
  }
103
103
 
104
+ .dte-pdf-field-content input[type='checkbox']:focus,
105
+ .dte-pdf-field-content input[type='checkbox']:active,
106
+ .dte-pdf-field-content input[type='radio']:active,
107
+ .dte-pdf-field-content input[type='radio']:focus {
108
+ box-shadow:
109
+ 0 0 0 2px var(--border-color-active, #2270ee) inset,
110
+ 0 0 0 3px var(--color-neutral-0, #ffffff) inset;
111
+ }
112
+
104
113
  /*
105
114
  * Printed PDF text modes (no ellipsis — hard clip like PdfSharp).
106
115
  */
@@ -169,6 +178,11 @@
169
178
  resize: none;
170
179
  }
171
180
 
181
+ .dte-pdf-field-fillable:focus {
182
+ outline: 2px solid var(--border-color-active, #2270ee);
183
+ outline-offset: 2px;
184
+ }
185
+
172
186
  .dte-pdf-field-generic-table {
173
187
  padding: var(--spacing-0, 0);
174
188
  margin: var(--spacing-0, 0);
@@ -1,6 +1,6 @@
1
1
  @import '@servicetitan/tokens/dist/tokens.css';
2
2
 
3
- .dte-pdf-editor {
3
+ .dte-pdf-editor-theme {
4
4
  --menu-active-color: var(--color-blue-400, #2270ee);
5
5
  --menu-active-bg-light-color: var(--color-blue-100, #e0f2ff);
6
6
  --menu-hover-color: var(--color-neutral-100, #606162);
@@ -16,14 +16,23 @@
16
16
  --spacing-2: 16px;
17
17
  --spacing-3: 24px;
18
18
 
19
+ /* Radius */
20
+ --radius-0: 0;
21
+ --radius-1: 4px;
22
+ --radius-2: 8px;
23
+
19
24
  /* Colors - Neutral */
20
25
  --color-neutral-0: var(--color-neutral-0, #ffffff);
26
+ --color-neutral-30: var(--color-neutral-30, #f7f7f7);
27
+ --color-neutral-50: var(--color-neutral-50, #eeeeee);
21
28
  --color-neutral-70: var(--color-neutral-70, #737475);
22
29
  --color-neutral-200: var(--color-neutral-200, #6c6d70);
23
30
  --color-neutral-300: var(--color-neutral-300, #2d2e31);
24
31
 
25
32
  /* Background colors */
26
33
  --bg-color-light: var(--color-neutral-0, #ffffff);
34
+ --bg-color-main: var(--color-neutral-20, #fafafa);
35
+ --bg-color-secondary: var(--color-neutral-50, #eeeeee);
27
36
  --bg-color: var(--color-neutral-200, #f5f5f5);
28
37
  --bg-color-hover: #eaeaea;
29
38
 
@@ -0,0 +1,62 @@
1
+ import { sortFieldsByReadingOrder } from '../field-reading-order.utils';
2
+
3
+ interface TestField {
4
+ id: string;
5
+ page: number;
6
+ x: number;
7
+ y: number;
8
+ height: number;
9
+ }
10
+
11
+ const field = (id: string, page: number, x: number, y: number, height = 20): TestField => ({
12
+ id,
13
+ page,
14
+ x,
15
+ y,
16
+ height,
17
+ });
18
+
19
+ describe('sortFieldsByReadingOrder', () => {
20
+ const ids = (fields: TestField[]) => fields.map(f => f.id);
21
+
22
+ test('orders fields left-to-right within the same row', () => {
23
+ const fields = [field('right', 1, 300, 100), field('left', 1, 50, 100)];
24
+
25
+ expect(ids(sortFieldsByReadingOrder(fields))).toEqual(['left', 'right']);
26
+ });
27
+
28
+ test('orders fields top-to-bottom across rows', () => {
29
+ const fields = [field('bottom', 1, 50, 300), field('top', 1, 50, 50)];
30
+
31
+ expect(ids(sortFieldsByReadingOrder(fields))).toEqual(['top', 'bottom']);
32
+ });
33
+
34
+ test('keeps a field left-of-and-below another on the same row grouped correctly', () => {
35
+ // "left" starts slightly lower than "right" but they visually sit on the same row.
36
+ const fields = [field('right', 1, 500, 100), field('left', 1, 50, 105)];
37
+
38
+ expect(ids(sortFieldsByReadingOrder(fields))).toEqual(['left', 'right']);
39
+ });
40
+
41
+ test('does not merge a lower row into a taller row above it', () => {
42
+ const tall = field('tall', 1, 50, 50, 200);
43
+ const nextRow = field('next-row', 1, 500, 260);
44
+
45
+ expect(ids(sortFieldsByReadingOrder([nextRow, tall]))).toEqual(['tall', 'next-row']);
46
+ });
47
+
48
+ test('orders by page before position', () => {
49
+ const fields = [field('page2', 2, 0, 0), field('page1', 1, 500, 500)];
50
+
51
+ expect(ids(sortFieldsByReadingOrder(fields))).toEqual(['page1', 'page2']);
52
+ });
53
+
54
+ test('does not mutate the input array', () => {
55
+ const fields = [field('b', 1, 300, 100), field('a', 1, 50, 100)];
56
+ const copy = [...fields];
57
+
58
+ sortFieldsByReadingOrder(fields);
59
+
60
+ expect(fields).toEqual(copy);
61
+ });
62
+ });
@@ -14,24 +14,28 @@ interface DragOffset {
14
14
  * @param e - The drag event
15
15
  * @param field - The field being dragged
16
16
  * @param pdfWrapperRef - Reference to the PDF wrapper element
17
+ * @param zoom - The current zoom level of the PDF
17
18
  * @returns The drag offset object or null if wrapper is not available
18
19
  */
19
20
  export const handleFieldDragStart = (
20
21
  e: DragEvent<HTMLDivElement>,
21
22
  field: PdfField,
22
23
  pdfWrapperRef: RefObject<HTMLDivElement>,
24
+ zoom = 1,
23
25
  ): DragOffset | null => {
24
26
  if (!pdfWrapperRef.current) {
25
27
  return null;
26
28
  }
27
29
 
28
30
  /*
29
- * Calculate drag offset in page-relative coordinates
31
+ * Calculate drag offset in page-relative coordinates.
32
+ * DOM measurements reflect the current zoom, so divide by zoom to bring
33
+ * them into the same unzoomed reference space as the stored field.x/y.
30
34
  */
31
35
  const startPagePos = getPagePosition(field.page, pdfWrapperRef);
32
36
  const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
33
- const cursorXInPage = e.clientX - wrapperRect.left - startPagePos.left;
34
- const cursorYInPage = e.clientY - wrapperRect.top - startPagePos.top;
37
+ const cursorXInPage = (e.clientX - wrapperRect.left - startPagePos.left) / zoom;
38
+ const cursorYInPage = (e.clientY - wrapperRect.top - startPagePos.top) / zoom;
35
39
 
36
40
  const dragOffset: DragOffset = {
37
41
  x: cursorXInPage - field.x,
@@ -65,6 +69,7 @@ export const handleFieldDragStart = (
65
69
  * @param dragOffset - The drag offset from drag start
66
70
  * @param pdfWrapperRef - Reference to the PDF wrapper element
67
71
  * @param onFieldMove - Callback to update field position
72
+ * @param zoom - The current zoom level of the PDF
68
73
  */
69
74
  export const handleFieldDrag = (
70
75
  e: DragEvent<HTMLDivElement>,
@@ -72,6 +77,7 @@ export const handleFieldDrag = (
72
77
  dragOffset: DragOffset,
73
78
  pdfWrapperRef: RefObject<HTMLDivElement>,
74
79
  onFieldMove: (fieldId: string, newX: number, newY: number, pageNumber: number) => void,
80
+ zoom = 1,
75
81
  ): void => {
76
82
  if (!pdfWrapperRef.current || e.clientX === 0 || e.clientY === 0) {
77
83
  return;
@@ -82,9 +88,9 @@ export const handleFieldDrag = (
82
88
  const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
83
89
  const pageDimensions = getPageDimensions(currentPage, pdfWrapperRef);
84
90
 
85
- // Calculate position in page coordinates
86
- const x = e.clientX - wrapperRect.left - currentPagePos.left - dragOffset.x;
87
- const y = e.clientY - wrapperRect.top - currentPagePos.top - dragOffset.y;
91
+ // Calculate position in page (unzoomed reference) coordinates
92
+ const x = (e.clientX - wrapperRect.left - currentPagePos.left) / zoom - dragOffset.x;
93
+ const y = (e.clientY - wrapperRect.top - currentPagePos.top) / zoom - dragOffset.y;
88
94
 
89
95
  /*
90
96
  * Clamp x and y to stay within page boundaries.
@@ -92,8 +98,8 @@ export const handleFieldDrag = (
92
98
  */
93
99
  const minX = 0;
94
100
  const minY = 0;
95
- const maxX = Math.max(0, pageDimensions.width - field.width);
96
- const maxY = Math.max(0, pageDimensions.height - field.height);
101
+ const maxX = Math.max(0, pageDimensions.width / zoom - field.width);
102
+ const maxY = Math.max(0, pageDimensions.height / zoom - field.height);
97
103
 
98
104
  const clampedX = Math.max(minX, Math.min(maxX, x));
99
105
  const clampedY = Math.max(minY, Math.min(maxY, y));
@@ -0,0 +1,43 @@
1
+ interface ReadingOrderField {
2
+ page: number;
3
+ x: number;
4
+ y: number;
5
+ height: number;
6
+ }
7
+
8
+ /**
9
+ * Orders fields the way a person visually scans a page: top row first,
10
+ * left-to-right within a row, then down to the next row - rather than a
11
+ * strict y-then-x sort, which misorders fields that sit on the same visual
12
+ * row but don't share an exact y coordinate.
13
+ */
14
+ export const sortFieldsByReadingOrder = <T extends ReadingOrderField>(fields: T[]): T[] => {
15
+ const byPosition = [...fields].sort((a, b) => a.page - b.page || a.y - b.y || a.x - b.x);
16
+
17
+ const rows: T[][] = [];
18
+ let currentRow: T[] = [];
19
+ let currentPage = NaN;
20
+ let rowBottom = -Infinity;
21
+
22
+ for (const field of byPosition) {
23
+ const startsNewRow = field.page !== currentPage || field.y >= rowBottom;
24
+
25
+ if (startsNewRow) {
26
+ if (currentRow.length) {
27
+ rows.push(currentRow);
28
+ }
29
+ currentRow = [field];
30
+ currentPage = field.page;
31
+ rowBottom = field.y + field.height;
32
+ } else {
33
+ currentRow.push(field);
34
+ rowBottom = Math.max(rowBottom, field.y + field.height);
35
+ }
36
+ }
37
+
38
+ if (currentRow.length) {
39
+ rows.push(currentRow);
40
+ }
41
+
42
+ return rows.flatMap(row => row.sort((a, b) => a.x - b.x));
43
+ };
@@ -16,12 +16,14 @@ interface ResizeStartData {
16
16
  * @param e - Mouse event
17
17
  * @param field - The field being resized
18
18
  * @param pdfWrapperRef - Reference to the PDF wrapper element
19
+ * @param zoom - The current zoom level of the PDF
19
20
  * @returns Resize start data or null if calculation fails
20
21
  */
21
22
  export const handleFieldResizeStart = (
22
23
  e: { clientX: number; clientY: number },
23
24
  field: PdfField,
24
25
  pdfWrapperRef: RefObject<HTMLDivElement>,
26
+ zoom = 1,
25
27
  ): ResizeStartData | null => {
26
28
  if (!pdfWrapperRef.current) {
27
29
  return null;
@@ -30,9 +32,9 @@ export const handleFieldResizeStart = (
30
32
  const pagePos = getPagePosition(field.page, pdfWrapperRef);
31
33
  const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
32
34
 
33
- // Calculate mouse position relative to the page
34
- const startX = e.clientX - wrapperRect.left - pagePos.left;
35
- const startY = e.clientY - wrapperRect.top - pagePos.top;
35
+ // Calculate mouse position relative to the page, in unzoomed reference coordinates
36
+ const startX = (e.clientX - wrapperRect.left - pagePos.left) / zoom;
37
+ const startY = (e.clientY - wrapperRect.top - pagePos.top) / zoom;
36
38
 
37
39
  return {
38
40
  startX,
@@ -51,6 +53,7 @@ export const handleFieldResizeStart = (
51
53
  * @param resizeStartData - Data from resize start
52
54
  * @param pdfWrapperRef - Reference to the PDF wrapper element
53
55
  * @param onFieldResize - Callback to update field dimensions
56
+ * @param zoom - The current zoom level of the PDF
54
57
  */
55
58
  export const handleFieldResize = (
56
59
  e: { clientX: number; clientY: number },
@@ -63,6 +66,7 @@ export const handleFieldResize = (
63
66
  newHeight: number,
64
67
  pageNumber: number,
65
68
  ) => void,
69
+ zoom = 1,
66
70
  ): void => {
67
71
  if (!pdfWrapperRef.current) {
68
72
  return;
@@ -72,9 +76,9 @@ export const handleFieldResize = (
72
76
  const pagePos = getPagePosition(resizeStartData.page, pdfWrapperRef);
73
77
  const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
74
78
 
75
- // Calculate mouse position relative to the page
76
- const mouseX = e.clientX - wrapperRect.left - pagePos.left;
77
- const mouseY = e.clientY - wrapperRect.top - pagePos.top;
79
+ // Calculate mouse position relative to the page, in unzoomed reference coordinates
80
+ const mouseX = (e.clientX - wrapperRect.left - pagePos.left) / zoom;
81
+ const mouseY = (e.clientY - wrapperRect.top - pagePos.top) / zoom;
78
82
 
79
83
  // Calculate delta from the start position
80
84
  const deltaX = mouseX - resizeStartData.startX;
@@ -103,8 +107,8 @@ export const handleFieldResize = (
103
107
  newHeight = Math.max(FIELD_CONSTANTS.minHeight, newHeight);
104
108
 
105
109
  // Clamp to page boundaries (ensure the field doesn't go outside the page)
106
- const maxWidth = pageDimensions.width - field.x;
107
- const maxHeight = pageDimensions.height - field.y;
110
+ const maxWidth = pageDimensions.width / zoom - field.x;
111
+ const maxHeight = pageDimensions.height / zoom - field.y;
108
112
  newWidth = Math.min(newWidth, maxWidth);
109
113
  newHeight = Math.min(newHeight, maxHeight);
110
114
 
@@ -6,6 +6,13 @@ interface FieldBoundsStyleOptions {
6
6
  pageOffset?: { left: number; top: number };
7
7
  backgroundColor?: string;
8
8
  borderColor?: string;
9
+ /**
10
+ * Current page zoom factor (1 = 100%). Field `x/y/width/height` are stored
11
+ * relative to the unzoomed reference page width, so they must be scaled by
12
+ * zoom for display while `pageOffset` (read live from the DOM) already
13
+ * reflects the zoomed page.
14
+ */
15
+ zoom?: number;
9
16
  }
10
17
 
11
18
  const getFieldTypeSpecificStyle = (field: PdfField): CSSProperties => {
@@ -39,14 +46,19 @@ export const getFieldBoundsStyle = (
39
46
  field: PdfField,
40
47
  options?: FieldBoundsStyleOptions,
41
48
  ): CSSProperties => {
42
- const { backgroundColor, borderColor, pageOffset = { left: 0, top: 0 } } = options ?? {};
49
+ const {
50
+ backgroundColor,
51
+ borderColor,
52
+ pageOffset = { left: 0, top: 0 },
53
+ zoom = 1,
54
+ } = options ?? {};
43
55
 
44
56
  return {
45
57
  position: 'absolute',
46
- left: pageOffset.left + field.x,
47
- top: pageOffset.top + field.y,
48
- width: field.width,
49
- height: field.height,
58
+ left: pageOffset.left + field.x * zoom,
59
+ top: pageOffset.top + field.y * zoom,
60
+ width: field.width * zoom,
61
+ height: field.height * zoom,
50
62
  display: 'flex',
51
63
  alignItems: 'flex-start',
52
64
  justifyContent: 'flex-start',
@@ -3,6 +3,7 @@ export * from './field-background-color.utils';
3
3
  export * from './field-coordinates.utils';
4
4
  export * from './field-drag.utils';
5
5
  export * from './field-placeholder-text.utils';
6
+ export * from './field-reading-order.utils';
6
7
  export * from './field-resize.utils';
7
8
  export * from './get-field-bounds-style.utils';
8
9
  export * from './get-field-font-size.utils';