@servicetitan/dte-pdf-editor 1.94.0 → 1.96.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 (145) hide show
  1. package/README.md +121 -4
  2. package/dist/components/field-config-panel/advanced-settings.d.ts +2 -0
  3. package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
  4. package/dist/components/field-config-panel/advanced-settings.js +5 -4
  5. package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
  6. package/dist/components/field-config-panel/e-sign-options-modal.d.ts +3 -0
  7. package/dist/components/field-config-panel/e-sign-options-modal.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/e-sign-options-modal.js +6 -4
  9. package/dist/components/field-config-panel/e-sign-options-modal.js.map +1 -1
  10. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +14 -1
  11. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  12. package/dist/components/field-config-panel/field-config-panel-overlay.js +41 -2
  13. package/dist/components/field-config-panel/field-config-panel-overlay.js.map +1 -1
  14. package/dist/components/field-config-panel/field-config-panel.d.ts +6 -0
  15. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  16. package/dist/components/field-config-panel/field-config-panel.js +17 -3
  17. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  18. package/dist/components/field-config-panel/formula-generator.d.ts +4 -0
  19. package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
  20. package/dist/components/field-config-panel/formula-generator.js +2 -2
  21. package/dist/components/field-config-panel/formula-generator.js.map +1 -1
  22. package/dist/components/field-config-panel/formula-modal.d.ts +5 -0
  23. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
  24. package/dist/components/field-config-panel/formula-modal.js +13 -6
  25. package/dist/components/field-config-panel/formula-modal.js.map +1 -1
  26. package/dist/components/field-config-panel/formula-workspace.d.ts +2 -0
  27. package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
  28. package/dist/components/field-config-panel/formula-workspace.js +6 -4
  29. package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
  30. package/dist/components/pdf-editor/pdf-editor.d.ts +52 -1
  31. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  32. package/dist/components/pdf-editor/pdf-editor.js +2 -2
  33. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  34. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
  35. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +4 -6
  36. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
  37. package/dist/components/pdf-view/pdf-view-calculated.d.ts +3 -1
  38. package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -1
  39. package/dist/components/pdf-view/pdf-view-calculated.js +14 -4
  40. package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -1
  41. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  42. package/dist/components/pdf-view/pdf-view-fillable.js +27 -4
  43. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  44. package/dist/components/pdf-view/pdf-view.d.ts +18 -4
  45. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  46. package/dist/components/pdf-view/pdf-view.js +2 -2
  47. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  48. package/dist/components/shared/char-limit-indicator.d.ts +8 -0
  49. package/dist/components/shared/char-limit-indicator.d.ts.map +1 -0
  50. package/dist/components/shared/char-limit-indicator.js +9 -0
  51. package/dist/components/shared/char-limit-indicator.js.map +1 -0
  52. package/dist/components/shared/index.d.ts +1 -0
  53. package/dist/components/shared/index.d.ts.map +1 -1
  54. package/dist/components/shared/index.js +1 -0
  55. package/dist/components/shared/index.js.map +1 -1
  56. package/dist/constants/calculated.constants.d.ts +6 -0
  57. package/dist/constants/calculated.constants.d.ts.map +1 -1
  58. package/dist/constants/calculated.constants.js +6 -0
  59. package/dist/constants/calculated.constants.js.map +1 -1
  60. package/dist/hooks/usePinchZoom.d.ts.map +1 -1
  61. package/dist/hooks/usePinchZoom.js +1 -1
  62. package/dist/hooks/usePinchZoom.js.map +1 -1
  63. package/dist/interface/types.d.ts +37 -1
  64. package/dist/interface/types.d.ts.map +1 -1
  65. package/dist/interface/types.js.map +1 -1
  66. package/dist/styles/pdf-field-overlay.css +9 -2
  67. package/dist/styles/pdf-field.css +22 -1
  68. package/dist/utils/field/field-text-box.utils.d.ts +31 -0
  69. package/dist/utils/field/field-text-box.utils.d.ts.map +1 -0
  70. package/dist/utils/field/field-text-box.utils.js +102 -0
  71. package/dist/utils/field/field-text-box.utils.js.map +1 -0
  72. package/dist/utils/field/fit-field-text.utils.d.ts +13 -0
  73. package/dist/utils/field/fit-field-text.utils.d.ts.map +1 -0
  74. package/dist/utils/field/fit-field-text.utils.js +44 -0
  75. package/dist/utils/field/fit-field-text.utils.js.map +1 -0
  76. package/dist/utils/field/get-field-max-length.utils.d.ts +38 -0
  77. package/dist/utils/field/get-field-max-length.utils.d.ts.map +1 -0
  78. package/dist/utils/field/get-field-max-length.utils.js +166 -0
  79. package/dist/utils/field/get-field-max-length.utils.js.map +1 -0
  80. package/dist/utils/field/index.d.ts +2 -0
  81. package/dist/utils/field/index.d.ts.map +1 -1
  82. package/dist/utils/field/index.js +2 -0
  83. package/dist/utils/field/index.js.map +1 -1
  84. package/dist/utils/field/text-wrap.utils.d.ts +26 -0
  85. package/dist/utils/field/text-wrap.utils.d.ts.map +1 -0
  86. package/dist/utils/field/text-wrap.utils.js +53 -0
  87. package/dist/utils/field/text-wrap.utils.js.map +1 -0
  88. package/dist/utils/formula/business-days.utils.d.ts +32 -0
  89. package/dist/utils/formula/business-days.utils.d.ts.map +1 -0
  90. package/dist/utils/formula/business-days.utils.js +148 -0
  91. package/dist/utils/formula/business-days.utils.js.map +1 -0
  92. package/dist/utils/formula/caret.utils.js +1 -1
  93. package/dist/utils/formula/caret.utils.js.map +1 -1
  94. package/dist/utils/formula/evaluate-formula.utils.d.ts +25 -6
  95. package/dist/utils/formula/evaluate-formula.utils.d.ts.map +1 -1
  96. package/dist/utils/formula/evaluate-formula.utils.js +61 -101
  97. package/dist/utils/formula/evaluate-formula.utils.js.map +1 -1
  98. package/dist/utils/formula/index.d.ts +1 -0
  99. package/dist/utils/formula/index.d.ts.map +1 -1
  100. package/dist/utils/formula/index.js +1 -0
  101. package/dist/utils/formula/index.js.map +1 -1
  102. package/dist/utils/formula/schedule-bindings.utils.d.ts +12 -0
  103. package/dist/utils/formula/schedule-bindings.utils.d.ts.map +1 -0
  104. package/dist/utils/formula/schedule-bindings.utils.js +17 -0
  105. package/dist/utils/formula/schedule-bindings.utils.js.map +1 -0
  106. package/package.json +2 -1
  107. package/src/__tests__/field-types/date-fields.test.ts +29 -3
  108. package/src/__tests__/field-types/e-sign-fields.test.ts +2 -2
  109. package/src/__tests__/field-types/fillable-fields.test.ts +2 -2
  110. package/src/components/field-config-panel/advanced-settings.tsx +16 -3
  111. package/src/components/field-config-panel/e-sign-options-modal.tsx +13 -2
  112. package/src/components/field-config-panel/field-config-panel-overlay.tsx +97 -1
  113. package/src/components/field-config-panel/field-config-panel.tsx +28 -0
  114. package/src/components/field-config-panel/formula-generator.tsx +8 -1
  115. package/src/components/field-config-panel/formula-modal.tsx +19 -4
  116. package/src/components/field-config-panel/formula-workspace.tsx +31 -6
  117. package/src/components/pdf-editor/pdf-editor.tsx +82 -0
  118. package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +41 -28
  119. package/src/components/pdf-view/pdf-view-calculated.tsx +28 -3
  120. package/src/components/pdf-view/pdf-view-fillable.tsx +82 -36
  121. package/src/components/pdf-view/pdf-view.tsx +23 -3
  122. package/src/components/shared/char-limit-indicator.tsx +23 -0
  123. package/src/components/shared/index.ts +1 -0
  124. package/src/constants/calculated.constants.ts +7 -0
  125. package/src/hooks/usePinchZoom.ts +1 -6
  126. package/src/interface/types.ts +47 -0
  127. package/src/styles/pdf-field-overlay.css +9 -2
  128. package/src/styles/pdf-field.css +22 -1
  129. package/src/utils/field/__tests__/field-background-color.utils.test.ts +8 -8
  130. package/src/utils/field/__tests__/fit-field-text.utils.test.ts +224 -0
  131. package/src/utils/field/__tests__/get-field-bounds-style.utils.test.ts +16 -0
  132. package/src/utils/field/__tests__/get-field-max-length.utils.test.ts +751 -0
  133. package/src/utils/field/__tests__/text-wrap.utils.test.ts +181 -0
  134. package/src/utils/field/field-text-box.utils.ts +143 -0
  135. package/src/utils/field/fit-field-text.utils.ts +61 -0
  136. package/src/utils/field/get-field-max-length.utils.ts +211 -0
  137. package/src/utils/field/index.ts +2 -0
  138. package/src/utils/field/text-wrap.utils.ts +78 -0
  139. package/src/utils/formula/__tests__/evaluate-formula.utils.test.ts +198 -16
  140. package/src/utils/formula/__tests__/schedule-bindings.utils.test.ts +73 -0
  141. package/src/utils/formula/business-days.utils.ts +188 -0
  142. package/src/utils/formula/caret.utils.ts +1 -1
  143. package/src/utils/formula/evaluate-formula.utils.ts +94 -114
  144. package/src/utils/formula/index.ts +1 -0
  145. package/src/utils/formula/schedule-bindings.utils.ts +27 -0
@@ -1,8 +1,15 @@
1
1
  import { FC, useMemo } from 'react';
2
- import { type DataModelValues, type PdfField, type PdfViewMode } from '../../interface/types';
2
+ import {
3
+ type BusinessCalendar,
4
+ type DataModelValues,
5
+ type FieldScheduleBinding,
6
+ type PdfField,
7
+ type PdfViewMode,
8
+ } from '../../interface/types';
3
9
  import {
4
10
  evaluateFormula,
5
11
  formatCalculatedResult,
12
+ getFieldDateCalcOptions,
6
13
  getFieldTextClipClassName,
7
14
  resolvePdfDataValues,
8
15
  } from '../../utils';
@@ -11,18 +18,26 @@ interface PdfViewCalculatedProps {
11
18
  field: PdfField;
12
19
  data?: DataModelValues;
13
20
  holidays?: string[];
21
+ fieldCalendars?: Record<string, BusinessCalendar>;
22
+ scheduleBindings?: FieldScheduleBinding[];
14
23
  viewMode?: PdfViewMode;
15
24
  }
16
25
 
17
26
  export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({
18
27
  data,
19
28
  field,
29
+ fieldCalendars,
20
30
  holidays,
31
+ scheduleBindings,
21
32
  viewMode,
22
33
  }) => {
23
34
  const displayValue = useMemo(() => {
24
35
  if (field.formula?.tokens?.length) {
25
- const value = evaluateFormula(field.formula, data, holidays);
36
+ const value = evaluateFormula(
37
+ field.formula,
38
+ data,
39
+ getFieldDateCalcOptions(field.id, scheduleBindings, holidays, fieldCalendars),
40
+ );
26
41
  if (value === null) {
27
42
  return '';
28
43
  }
@@ -32,7 +47,17 @@ export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({
32
47
  return resolvePdfDataValues(data, field.path) ?? '';
33
48
  }
34
49
  return field.label ?? '';
35
- }, [data, field.formula, field.formulaFormat, field.label, field.path, holidays]);
50
+ }, [
51
+ data,
52
+ field.formula,
53
+ field.formulaFormat,
54
+ field.id,
55
+ field.label,
56
+ field.path,
57
+ fieldCalendars,
58
+ holidays,
59
+ scheduleBindings,
60
+ ]);
36
61
 
37
62
  const isPreview = viewMode === 'preview';
38
63
  const shouldShowPreviewFallback = isPreview && displayValue === '';
@@ -1,4 +1,4 @@
1
- import { FC } from 'react';
1
+ import { ChangeEvent, FC, Fragment } from 'react';
2
2
  import {
3
3
  type DataChangePayload,
4
4
  type DataModelValues,
@@ -7,12 +7,15 @@ import {
7
7
  } from '../../interface/types';
8
8
  import {
9
9
  dateToUtcZero,
10
+ fitFieldTextChange,
10
11
  getDropdownFieldItems,
12
+ getFieldMaxLength,
11
13
  getFieldPlaceholderText,
12
14
  getFieldTextClipClassName,
13
15
  getFillablePaintedLabel,
14
16
  toDateInputValue,
15
17
  } from '../../utils';
18
+ import { CharLimitIndicator } from '../shared';
16
19
 
17
20
  interface PdfViewFillableProps {
18
21
  field: PdfField;
@@ -43,6 +46,33 @@ const fillableTextStyles = {
43
46
  margin: 0,
44
47
  } as const;
45
48
 
49
+ type FillableTextChangeEvent = ChangeEvent<HTMLInputElement | HTMLTextAreaElement>;
50
+
51
+ /**
52
+ * Passes on a text change, cut back to what fits the field. A change that adds only overflow is
53
+ * dropped; either way the caret stays where the user was typing, as with a native `maxLength`.
54
+ */
55
+ const applyFittedChange = (
56
+ field: PdfField,
57
+ value: string,
58
+ event: FillableTextChangeEvent,
59
+ onChange: (value: string) => void,
60
+ ) => {
61
+ const { selectionStart, value: nextValue } = event.target;
62
+ const fittedValue = fitFieldTextChange(field, value, nextValue);
63
+
64
+ if (fittedValue !== value) {
65
+ onChange(fittedValue);
66
+ }
67
+
68
+ if (fittedValue !== nextValue && selectionStart != null) {
69
+ const caret = fittedValue.length - (nextValue.length - selectionStart);
70
+ const input = event.target;
71
+ // after React has put the fitted value back into the control, which moves the caret
72
+ queueMicrotask(() => input.setSelectionRange(caret, caret));
73
+ }
74
+ };
75
+
46
76
  export const FillableTextInput: FC<FillableTextInputProps> = ({
47
77
  disabled,
48
78
  field,
@@ -50,19 +80,24 @@ export const FillableTextInput: FC<FillableTextInputProps> = ({
50
80
  readOnly,
51
81
  value,
52
82
  }) => {
83
+ const maxLength = getFieldMaxLength(field, value);
84
+
53
85
  return (
54
- <input
55
- readOnly={readOnly}
56
- type="text"
57
- className={`dte-pdf-field-fillable ${getFieldTextClipClassName(field)}`}
58
- style={fillableTextStyles}
59
- disabled={disabled}
60
- id={field.path}
61
- name={field.path}
62
- value={value}
63
- onChange={e => onChange(e.target.value)}
64
- placeholder={getFieldPlaceholderText(field)}
65
- />
86
+ <Fragment>
87
+ <input
88
+ readOnly={readOnly}
89
+ type="text"
90
+ className={`dte-pdf-field-fillable ${getFieldTextClipClassName(field)}`}
91
+ style={fillableTextStyles}
92
+ disabled={disabled}
93
+ id={field.path}
94
+ name={field.path}
95
+ value={value}
96
+ onChange={e => applyFittedChange(field, value, e, onChange)}
97
+ placeholder={getFieldPlaceholderText(field)}
98
+ />
99
+ <CharLimitIndicator maxLength={maxLength} value={value} />
100
+ </Fragment>
66
101
  );
67
102
  };
68
103
 
@@ -73,18 +108,23 @@ export const FillableTextareaInput: FC<FillableTextInputProps> = ({
73
108
  readOnly,
74
109
  value,
75
110
  }) => {
111
+ const maxLength = getFieldMaxLength(field, value);
112
+
76
113
  return (
77
- <textarea
78
- readOnly={readOnly}
79
- className={`dte-pdf-field-fillable ${getFieldTextClipClassName(field)}`}
80
- style={{ ...fillableTextStyles, resize: 'none' }}
81
- disabled={disabled}
82
- id={field.path}
83
- name={field.path}
84
- value={value}
85
- onChange={e => onChange(e.target.value)}
86
- placeholder={getFieldPlaceholderText(field)}
87
- />
114
+ <Fragment>
115
+ <textarea
116
+ readOnly={readOnly}
117
+ className={`dte-pdf-field-fillable ${getFieldTextClipClassName(field)}`}
118
+ style={{ ...fillableTextStyles, resize: 'none' }}
119
+ disabled={disabled}
120
+ id={field.path}
121
+ name={field.path}
122
+ value={value}
123
+ onChange={e => applyFittedChange(field, value, e, onChange)}
124
+ placeholder={getFieldPlaceholderText(field)}
125
+ />
126
+ <CharLimitIndicator maxLength={maxLength} value={value} />
127
+ </Fragment>
88
128
  );
89
129
  };
90
130
 
@@ -193,19 +233,25 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
193
233
  }
194
234
 
195
235
  if (field.subType === 'number') {
236
+ const numberValue = resolvedValue == null ? '' : String(resolvedValue);
237
+ const maxLength = getFieldMaxLength(field, numberValue);
238
+
196
239
  return (
197
- <input
198
- readOnly={readOnly}
199
- type="number"
200
- className={`dte-pdf-field-fillable ${getFieldTextClipClassName(field)}`}
201
- style={fillableTextStyles}
202
- disabled={!isFillable}
203
- id={field.path}
204
- name={field.path}
205
- value={resolvedValue ?? ''}
206
- onChange={e => handleDataChange(e.target.value)}
207
- placeholder={getFieldPlaceholderText(field)}
208
- />
240
+ <Fragment>
241
+ <input
242
+ readOnly={readOnly}
243
+ type="number"
244
+ className={`dte-pdf-field-fillable ${getFieldTextClipClassName(field)}`}
245
+ style={fillableTextStyles}
246
+ disabled={!isFillable}
247
+ id={field.path}
248
+ name={field.path}
249
+ value={numberValue}
250
+ onChange={e => applyFittedChange(field, numberValue, e, handleDataChange)}
251
+ placeholder={getFieldPlaceholderText(field)}
252
+ />
253
+ <CharLimitIndicator maxLength={maxLength} value={numberValue} />
254
+ </Fragment>
209
255
  );
210
256
  }
211
257
 
@@ -3,8 +3,10 @@ import { FC, ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react
3
3
  import { BASE_PAGE_WIDTH } from '../../constants';
4
4
  import { usePinchZoom, useZoom } from '../../hooks';
5
5
  import {
6
+ type BusinessCalendar,
6
7
  type DataChangePayload,
7
8
  type DataModelValues,
9
+ type FieldScheduleBinding,
8
10
  FieldTypeEnum,
9
11
  type PdfField,
10
12
  type PdfViewMode,
@@ -47,11 +49,25 @@ export interface PdfViewProps {
47
49
  */
48
50
  makeFillableFieldsReadonly?: boolean;
49
51
  /**
50
- * UTC date strings (e.g. `'2026-03-17T00:00:00Z'`) treated as non-business days.
51
- * When provided, date-based calculated fields skip weekends and these holidays
52
- * when adding/subtracting days or computing date differences.
52
+ * Holidays of the default business-day calendar, as date strings (`'2026-07-03'`,
53
+ * `'2026-03-17T00:00:00Z'`). Date calculated fields without their own calendar skip
54
+ * Saturday, Sunday and these days when adding/subtracting days or computing date
55
+ * differences, within the current year ± 1.
53
56
  */
54
57
  holidays?: string[];
58
+ /**
59
+ * Calendars of the date calculated fields bound to a business-day schedule, by field id
60
+ * (`PdfField.id`), as DTE's `POST api/holidays/calendar` resolves them. Listed fields use
61
+ * only their calendar; the others use the default one (`holidays`).
62
+ */
63
+ fieldCalendars?: Record<string, BusinessCalendar>;
64
+ /**
65
+ * Date calculated fields whose custom business-day exclusion toggle is on, and the schedule
66
+ * each uses (DTE's bindings, `PdfField.id` as `fieldKey`). A field with no entry here uses
67
+ * plain calendar-day arithmetic — no weekend or holiday skipping — regardless of `holidays`
68
+ * or `fieldCalendars`.
69
+ */
70
+ scheduleBindings?: FieldScheduleBinding[];
55
71
  /**
56
72
  * fillingBy defines the list of recipient names
57
73
  * who are allowed to fill this field.
@@ -71,6 +87,7 @@ export const PdfView: FC<PdfViewProps> = ({
71
87
  enableInputNavigation = true,
72
88
  errorPlaceholder,
73
89
  errors = {},
90
+ fieldCalendars,
74
91
  fields,
75
92
  fillingBy,
76
93
  holidays,
@@ -81,6 +98,7 @@ export const PdfView: FC<PdfViewProps> = ({
81
98
  onLoadSuccess,
82
99
  pdfUrl,
83
100
  recipients,
101
+ scheduleBindings,
84
102
  viewMode,
85
103
  zoomProps,
86
104
  zoomable = true,
@@ -280,6 +298,8 @@ export const PdfView: FC<PdfViewProps> = ({
280
298
  field={field}
281
299
  data={viewMode === 'preview' ? previewData : data}
282
300
  holidays={holidays}
301
+ fieldCalendars={fieldCalendars}
302
+ scheduleBindings={scheduleBindings}
283
303
  viewMode={viewMode}
284
304
  />
285
305
  )}
@@ -0,0 +1,23 @@
1
+ import { FC } from 'react';
2
+
3
+ interface CharLimitIndicatorProps {
4
+ maxLength?: number;
5
+ value: string;
6
+ }
7
+
8
+ export const CharLimitIndicator: FC<CharLimitIndicatorProps> = ({ maxLength, value }) => {
9
+ if (maxLength == null) {
10
+ return null;
11
+ }
12
+
13
+ const filledCount = value.length;
14
+
15
+ return (
16
+ <span
17
+ className={`dte-pdf-field-char-count${filledCount >= maxLength ? ' --at-limit' : ''}`}
18
+ aria-live="polite"
19
+ >
20
+ {filledCount}/{maxLength}
21
+ </span>
22
+ );
23
+ };
@@ -1 +1,2 @@
1
1
  export * from './inline-editable';
2
+ export * from './char-limit-indicator';
@@ -4,4 +4,11 @@ export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY';
4
4
 
5
5
  export const MAX_DATE_CALC_DAYS = 365;
6
6
 
7
+ /**
8
+ * Years either side of the current one that the default business-day calendar covers, as DTE's
9
+ * (`HolidayCheckService.HolidayYearsWindow`): outside them its holidays are unknown, so date
10
+ * arithmetic is blank.
11
+ */
12
+ export const DEFAULT_CALENDAR_YEARS = 1;
13
+
7
14
  export const MAX_DATE_DISPLAY_RANGE_YEARS = 2;
@@ -1,10 +1,5 @@
1
1
  import { RefObject, useEffect, useRef } from 'react';
2
- import {
3
- clampZoom,
4
- getPinchZoomValue,
5
- getTouchDistance,
6
- getWheelZoomFactor,
7
- } from '../utils';
2
+ import { clampZoom, getPinchZoomValue, getTouchDistance, getWheelZoomFactor } from '../utils';
8
3
 
9
4
  interface UsePinchZoomOptions {
10
5
  enabled: boolean;
@@ -86,6 +86,14 @@ export interface ESignDateSignedOffsetSettings {
86
86
  rules:
87
87
  | [ESignDateSignedOffsetSettingsFinalRule]
88
88
  | [...ESignDateSignedOffsetSettingsRule[], ESignDateSignedOffsetSettingsFinalRule];
89
+ /**
90
+ * Whether the day offset skips weekends/holidays per a business-day schedule, same toggle as a
91
+ * calculated field's (`FieldScheduleBinding`) but stored here instead, since - like `rules` -
92
+ * it applies to every `dateSignedOffset` field in the document, not one field at a time.
93
+ */
94
+ businessCalendarEnabled?: boolean;
95
+ /** The schedule to use while `businessCalendarEnabled` is on; undefined for the default schedule. */
96
+ scheduleId?: number;
89
97
  }
90
98
 
91
99
  export interface FillableDropdownItem {
@@ -246,6 +254,45 @@ export interface StructuredFormula {
246
254
  tokens: FormulaToken[];
247
255
  }
248
256
 
257
+ /**
258
+ * Days a date calculated field skips, as DTE resolves a business-day schedule
259
+ * (`POST api/holidays/calendar`). Date arithmetic outside `firstYear`–`lastYear` is blank.
260
+ */
261
+ export interface BusinessCalendar {
262
+ /** Excluded weekend days, as `Date.getUTCDay()` numbers them: 0 = Sunday, 6 = Saturday. */
263
+ weekendDays: number[];
264
+ /** Holiday dates, already moved to their observed days, as `YYYY-MM-DD`. */
265
+ holidays: string[];
266
+ firstYear: number;
267
+ lastYear: number;
268
+ }
269
+
270
+ /**
271
+ * Re-exported from `@servicetitan/dte-shared` so existing imports from this path keep
272
+ * working: the schedule editor UI (`ScheduleDrawer`/`SchedulePicker`) is shared with
273
+ * `@servicetitan/dte-unlayer` and lives there now, these types are its domain model.
274
+ */
275
+ export type {
276
+ BusinessCalendarRules,
277
+ BusinessDaySchedule,
278
+ HolidayCatalog,
279
+ KnownHoliday,
280
+ SaveScheduleRequest,
281
+ WeekendDay,
282
+ } from '@servicetitan/dte-shared';
283
+
284
+ /**
285
+ * A date calculated field whose custom business-day exclusion toggle is on, and the schedule it
286
+ * uses (DTE's bindings, full-replace via `PUT api/holidays/templates/{id}/bindings`). A field with
287
+ * no entry here uses the default calendar with the toggle off (plain calendar-day arithmetic).
288
+ */
289
+ export interface FieldScheduleBinding {
290
+ /** `PdfField.id`. */
291
+ fieldKey: string;
292
+ /** One of the tenant's schedules, or undefined for the default schedule. */
293
+ scheduleId?: number;
294
+ }
295
+
249
296
  /** Format options for a calculated field with a numeric result (number, currency, percent) */
250
297
  export interface CalculatedFormatForNumber {
251
298
  resultType: 'number' | 'currency' | 'percent';
@@ -12,10 +12,10 @@
12
12
  }
13
13
 
14
14
  .dte-pdf-field-error-message {
15
- top: 100%;
15
+ bottom: -4px;
16
+ transform: translateY(100%);
16
17
  min-width: 100px;
17
18
  position: absolute;
18
- margin-top: var(--spacing-1, 8px);
19
19
  color: var(--danger);
20
20
  font-size: var(--typescale-small, 10px);
21
21
  }
@@ -105,6 +105,13 @@
105
105
  }
106
106
  }
107
107
 
108
+ textarea.dte-pdf-field-fillable,
109
+ input[type='text'].dte-pdf-field-fillable,
110
+ input[type='number'].dte-pdf-field-fillable {
111
+ font-family: inherit;
112
+ font-weight: inherit;
113
+ }
114
+
108
115
  .dte-pdf-field-generic-table {
109
116
  padding: var(--spacing-0, 0);
110
117
  margin: var(--spacing-0, 0);
@@ -47,7 +47,8 @@
47
47
  background-color: var(--color-neutral-0, #ffffff);
48
48
  border-radius: 4px;
49
49
  padding: 1px var(--spacing-half, 4px);
50
- top: -10px;
50
+ top: 0;
51
+ transform: translateY(-50%);
51
52
  left: 3px;
52
53
  font-size: var(--typescale-small, 10px);
53
54
  line-height: 1;
@@ -63,7 +64,27 @@
63
64
  }
64
65
  }
65
66
 
67
+ .dte-pdf-field-char-count {
68
+ position: absolute;
69
+ right: 3px;
70
+ bottom: 0;
71
+ transform: translateY(50%);
72
+ padding: 1px var(--spacing-half, 4px);
73
+ border-radius: 4px;
74
+ background-color: var(--color-neutral-0, #ffffff);
75
+ box-shadow: 0 0 0 1px var(--pdf-field-color);
76
+ color: var(--pdf-field-color-dark);
77
+ font-size: var(--typescale-small, 10px);
78
+ user-select: none;
79
+
80
+ &.--at-limit {
81
+ color: var(--danger, #dc3545);
82
+ box-shadow: 0 0 0 1px var(--danger, #dc3545);
83
+ }
84
+ }
85
+
66
86
  input::placeholder,
87
+ textarea::placeholder,
67
88
  input[type='date'],
68
89
  input[type='date']::-webkit-calendar-picker-indicator {
69
90
  color: var(--pdf-field-color);
@@ -8,19 +8,19 @@ describe('getFieldColor', () => {
8
8
  expect(subject('signer1', { signer1: '#ff0000', signer2: '#00ff00' })).toBe('#ff0000');
9
9
  });
10
10
 
11
- test('returns "none" when recipient is undefined', () => {
12
- expect(subject(undefined, { signer1: '#ff0000' })).toBe('none');
11
+ test('returns undefined when recipient is undefined', () => {
12
+ expect(subject(undefined, { signer1: '#ff0000' })).toBeUndefined();
13
13
  });
14
14
 
15
- test('returns "none" when recipient is empty string', () => {
16
- expect(subject('', { signer1: '#ff0000' })).toBe('none');
15
+ test('returns undefined when recipient is empty string', () => {
16
+ expect(subject('', { signer1: '#ff0000' })).toBeUndefined();
17
17
  });
18
18
 
19
- test('returns "none" when recipient is not in the color map', () => {
20
- expect(subject('unknown', { signer1: '#ff0000' })).toBe('none');
19
+ test('returns undefined when recipient is not in the color map', () => {
20
+ expect(subject('unknown', { signer1: '#ff0000' })).toBeUndefined();
21
21
  });
22
22
 
23
- test('returns "none" when color map is empty', () => {
24
- expect(subject('signer1', {})).toBe('none');
23
+ test('returns undefined when color map is empty', () => {
24
+ expect(subject('signer1', {})).toBeUndefined();
25
25
  });
26
26
  });