@servicetitan/dte-pdf-editor 1.92.3 → 1.93.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 (130) hide show
  1. package/README.md +0 -2
  2. package/dist/components/field-config-panel/dropdown-field-items.d.ts +9 -0
  3. package/dist/components/field-config-panel/dropdown-field-items.d.ts.map +1 -0
  4. package/dist/components/field-config-panel/dropdown-field-items.js +60 -0
  5. package/dist/components/field-config-panel/dropdown-field-items.js.map +1 -0
  6. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +1 -1
  7. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/field-config-panel.d.ts +1 -1
  9. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  10. package/dist/components/field-config-panel/field-config-panel.js +5 -1
  11. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  12. package/dist/components/field-config-panel/fillable-default-config.d.ts +19 -0
  13. package/dist/components/field-config-panel/fillable-default-config.d.ts.map +1 -0
  14. package/dist/components/field-config-panel/fillable-default-config.js +77 -0
  15. package/dist/components/field-config-panel/fillable-default-config.js.map +1 -0
  16. package/dist/components/pdf-editor/pdf-editor.js +1 -1
  17. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  18. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +1 -1
  19. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  20. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts +9 -0
  21. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
  22. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +39 -12
  23. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
  24. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  25. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +4 -4
  26. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  27. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  28. package/dist/components/pdf-view/pdf-view-field-container.js +7 -20
  29. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  30. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  31. package/dist/components/pdf-view/pdf-view-fillable.js +6 -2
  32. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  33. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  34. package/dist/components/pdf-view/pdf-view.js +9 -2
  35. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  36. package/dist/constants/field.constants.d.ts +1 -0
  37. package/dist/constants/field.constants.d.ts.map +1 -1
  38. package/dist/constants/field.constants.js +9 -0
  39. package/dist/constants/field.constants.js.map +1 -1
  40. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  41. package/dist/hooks/usePdfFieldDnD.js +6 -0
  42. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  43. package/dist/hooks/usePdfFieldSelection.d.ts +1 -1
  44. package/dist/hooks/usePdfFieldSelection.d.ts.map +1 -1
  45. package/dist/hooks/usePdfFieldSelection.js +3 -1
  46. package/dist/hooks/usePdfFieldSelection.js.map +1 -1
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/interface/types.d.ts +46 -1
  52. package/dist/interface/types.d.ts.map +1 -1
  53. package/dist/interface/types.js.map +1 -1
  54. package/dist/styles/index.css +1 -0
  55. package/dist/styles/inline-editable.css +2 -0
  56. package/dist/styles/pdf-canvas.css +2 -1
  57. package/dist/styles/pdf-field-overlay.css +2 -111
  58. package/dist/styles/pdf-field.css +111 -0
  59. package/dist/styles/variables.css +1 -0
  60. package/dist/utils/data-model/index.d.ts +1 -0
  61. package/dist/utils/data-model/index.d.ts.map +1 -1
  62. package/dist/utils/data-model/index.js +1 -0
  63. package/dist/utils/data-model/index.js.map +1 -1
  64. package/dist/utils/data-model/resolve-fields-default-values.d.ts +3 -0
  65. package/dist/utils/data-model/resolve-fields-default-values.d.ts.map +1 -0
  66. package/dist/utils/data-model/resolve-fields-default-values.js +19 -0
  67. package/dist/utils/data-model/resolve-fields-default-values.js.map +1 -0
  68. package/dist/utils/exposes-utils.d.ts +2 -0
  69. package/dist/utils/exposes-utils.d.ts.map +1 -0
  70. package/dist/utils/exposes-utils.js +2 -0
  71. package/dist/utils/exposes-utils.js.map +1 -0
  72. package/dist/utils/field/field-background-color.utils.d.ts +1 -1
  73. package/dist/utils/field/field-background-color.utils.d.ts.map +1 -1
  74. package/dist/utils/field/field-background-color.utils.js +1 -1
  75. package/dist/utils/field/field-background-color.utils.js.map +1 -1
  76. package/dist/utils/field/get-dropdown-field-items.utils.d.ts +3 -0
  77. package/dist/utils/field/get-dropdown-field-items.utils.d.ts.map +1 -0
  78. package/dist/utils/field/get-dropdown-field-items.utils.js +7 -0
  79. package/dist/utils/field/get-dropdown-field-items.utils.js.map +1 -0
  80. package/dist/utils/field/get-field-bounds-style.utils.d.ts +1 -2
  81. package/dist/utils/field/get-field-bounds-style.utils.d.ts.map +1 -1
  82. package/dist/utils/field/get-field-bounds-style.utils.js +9 -4
  83. package/dist/utils/field/get-field-bounds-style.utils.js.map +1 -1
  84. package/dist/utils/field/get-fillable-default-value.utils.d.ts +29 -0
  85. package/dist/utils/field/get-fillable-default-value.utils.d.ts.map +1 -0
  86. package/dist/utils/field/get-fillable-default-value.utils.js +59 -0
  87. package/dist/utils/field/get-fillable-default-value.utils.js.map +1 -0
  88. package/dist/utils/field/index.d.ts +2 -0
  89. package/dist/utils/field/index.d.ts.map +1 -1
  90. package/dist/utils/field/index.js +2 -0
  91. package/dist/utils/field/index.js.map +1 -1
  92. package/dist/utils/recipients/map-colors.js +23 -23
  93. package/dist/utils/recipients/map-colors.js.map +1 -1
  94. package/package.json +1 -1
  95. package/src/__tests__/field-types/e-sign-fields.test.ts +3 -3
  96. package/src/__tests__/field-types/fillable-fields.test.ts +142 -6
  97. package/src/components/field-config-panel/dropdown-field-items.tsx +138 -0
  98. package/src/components/field-config-panel/field-config-panel-overlay.tsx +1 -1
  99. package/src/components/field-config-panel/field-config-panel.tsx +16 -1
  100. package/src/components/field-config-panel/fillable-default-config.tsx +155 -0
  101. package/src/components/pdf-editor/pdf-editor.tsx +1 -1
  102. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +1 -1
  103. package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +66 -3
  104. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +6 -3
  105. package/src/components/pdf-view/pdf-view-field-container.tsx +21 -41
  106. package/src/components/pdf-view/pdf-view-fillable.tsx +25 -2
  107. package/src/components/pdf-view/pdf-view.tsx +11 -3
  108. package/src/constants/field.constants.ts +10 -0
  109. package/src/hooks/usePdfFieldDnD.ts +6 -0
  110. package/src/hooks/usePdfFieldSelection.ts +8 -2
  111. package/src/index.ts +1 -0
  112. package/src/interface/types.ts +36 -1
  113. package/src/styles/index.css +1 -0
  114. package/src/styles/inline-editable.css +2 -0
  115. package/src/styles/pdf-canvas.css +2 -1
  116. package/src/styles/pdf-field-overlay.css +2 -111
  117. package/src/styles/pdf-field.css +111 -0
  118. package/src/styles/variables.css +1 -0
  119. package/src/utils/data-model/index.ts +1 -0
  120. package/src/utils/data-model/resolve-fields-default-values.ts +19 -0
  121. package/src/utils/exposes-utils.ts +1 -0
  122. package/src/utils/field/__tests__/field-background-color.utils.test.ts +3 -3
  123. package/src/utils/field/__tests__/get-field-bounds-style.utils.test.ts +0 -1
  124. package/src/utils/field/__tests__/get-fillable-default-value.utils.test.ts +137 -0
  125. package/src/utils/field/field-background-color.utils.ts +1 -1
  126. package/src/utils/field/get-dropdown-field-items.utils.ts +14 -0
  127. package/src/utils/field/get-field-bounds-style.utils.ts +11 -11
  128. package/src/utils/field/get-fillable-default-value.utils.ts +70 -0
  129. package/src/utils/field/index.ts +2 -0
  130. package/src/utils/recipients/map-colors.ts +23 -23
@@ -11,70 +11,15 @@
11
11
  }
12
12
  }
13
13
 
14
- /* Editor-only chrome. Layout metrics come from getFieldBoundsStyle. */
15
- .dte-pdf-field-editor {
16
- outline: 1px dashed #999;
17
- outline-offset: 0;
18
- border: none;
19
- pointer-events: auto;
20
- border-radius: 4px;
21
-
22
- &.--edit-mode {
23
- overflow: unset !important;
24
- }
25
-
26
- &.--selected {
27
- outline: 2px solid var(--border-color-active);
28
- cursor: move;
29
- }
30
-
31
- &.--unselected {
32
- cursor: pointer;
33
- }
34
-
35
- &.--dragging {
36
- opacity: 0.5;
37
- }
38
-
39
- &.--colored:not(.--selected) {
40
- outline-color: var(--border-color-active) !important;
41
- box-shadow: 0 0 2px 0 var(--border-color-active);
42
- }
43
-
44
- &.--error {
45
- outline-color: var(--danger) !important;
46
- box-shadow: 0 0 4px 0 var(--danger);
47
- }
48
- }
49
-
50
14
  .dte-pdf-field-error-message {
51
15
  top: 100%;
52
16
  min-width: 100px;
53
17
  position: absolute;
54
- margin-top: var(--spacing-half, 4px);
18
+ margin-top: var(--spacing-1, 8px);
55
19
  color: var(--danger);
56
20
  font-size: var(--typescale-small, 10px);
57
21
  }
58
22
 
59
- .dte-pdf-field-label {
60
- position: absolute;
61
- display: flex;
62
- align-items: center;
63
- gap: var(--spacing-half, 4px);
64
- background-color: var(--color-neutral-0, #ffffff);
65
- border-radius: 4px;
66
- padding: 1px var(--spacing-half, 4px);
67
- top: -10px;
68
- left: 3px;
69
- font-size: var(--typescale-small, 10px);
70
- line-height: 1;
71
- white-space: nowrap;
72
- color: var(--menu-active-color, #2270ee);
73
- border-width: 1px;
74
- border-style: solid;
75
- border-color: inherit;
76
- }
77
-
78
23
  .dte-pdf-field-resize-handle {
79
24
  position: absolute;
80
25
  bottom: 0;
@@ -91,59 +36,14 @@
91
36
  }
92
37
  }
93
38
 
94
- .dte-add-field-button {
95
- display: none;
96
- position: absolute;
97
- bottom: -20px;
98
- left: -10px;
99
- margin-left: 50%;
100
- padding: var(--spacing-0, 0);
101
- width: 20px;
102
- height: 20px;
103
-
104
- .dte-pdf-field-editor:hover & {
105
- display: block;
106
- }
107
- }
108
-
109
- .dte-pdf-field-view {
110
- border: none;
111
- outline: none;
112
- cursor: default;
113
- pointer-events: auto;
114
-
115
- &.--fillable,
116
- &.--eSign {
117
- border-style: dashed;
118
- border-width: 1px;
119
- border-radius: 4px;
120
-
121
- &:focus {
122
- outline: 2px solid var(--border-color-active);
123
- }
124
- }
125
- }
126
-
127
39
  .dte-pdf-field-content {
128
40
  width: 100%;
129
41
  min-width: 0;
130
42
  min-height: 0;
131
43
  overflow: hidden;
132
44
  line-height: 1;
133
-
134
- input[type='checkbox']:focus,
135
- input[type='checkbox']:active,
136
- input[type='radio']:active,
137
- input[type='radio']:focus {
138
- box-shadow:
139
- 0 0 0 2px var(--border-color-active, #2270ee) inset,
140
- 0 0 0 3px var(--color-neutral-0, #ffffff) inset;
141
- }
142
45
  }
143
46
 
144
- /*
145
- * Printed PDF text modes (no ellipsis — hard clip like PdfSharp).
146
- */
147
47
  .dte-pdf-field-text--single-line {
148
48
  width: 100%;
149
49
  min-width: 0;
@@ -201,16 +101,7 @@
201
101
  }
202
102
 
203
103
  &:focus {
204
- outline: 2px solid var(--border-color-active, #2270ee);
205
- outline-offset: 2px;
206
- }
207
-
208
- .dte-pdf-field-editor & {
209
- pointer-events: none;
210
- }
211
-
212
- .dte-pdf-field-view & {
213
- pointer-events: auto;
104
+ outline: none;
214
105
  }
215
106
  }
216
107
 
@@ -0,0 +1,111 @@
1
+ .dte-pdf-field-container {
2
+ --pdf-field-color: var(--color-blue-500, #0265dc);
3
+ --pdf-field-color-dark: var(--color-blue-500, #0265dc);
4
+ --pdf-field-opacity-color: #3d95ff1f;
5
+
6
+ pointer-events: auto;
7
+ border-radius: 4px;
8
+
9
+ &.--edit {
10
+ color: var(--pdf-field-color);
11
+ background-color: var(--pdf-field-opacity-color);
12
+ outline: 1px dashed var(--pdf-field-color);
13
+ }
14
+
15
+ &.--view {
16
+ &.--fillable,
17
+ &.--eSign {
18
+ color: var(--pdf-field-color);
19
+ background-color: var(--pdf-field-opacity-color);
20
+ outline: 1px dashed var(--pdf-field-color);
21
+
22
+ &:focus-within {
23
+ box-shadow:
24
+ 0 0 0 3px white,
25
+ 0 0 0 5px var(--pdf-field-color-dark) !important;
26
+ }
27
+
28
+ input[type='checkbox'],
29
+ select {
30
+ outline: none;
31
+ }
32
+
33
+ &:has(input:disabled),
34
+ &:has(select:disabled) {
35
+ --pdf-field-color: #707070 !important;
36
+ --pdf-field-color-dark: #707070 !important;
37
+ --pdf-field-opacity-color: #7070701f !important;
38
+ }
39
+ }
40
+ }
41
+
42
+ .dte-pdf-field-label {
43
+ position: absolute;
44
+ display: flex;
45
+ align-items: center;
46
+ gap: var(--spacing-half, 4px);
47
+ background-color: var(--color-neutral-0, #ffffff);
48
+ border-radius: 4px;
49
+ padding: 1px var(--spacing-half, 4px);
50
+ top: -10px;
51
+ left: 3px;
52
+ font-size: var(--typescale-small, 10px);
53
+ line-height: 1;
54
+ white-space: nowrap;
55
+ color: var(--pdf-field-color-dark);
56
+ border-width: 1px;
57
+ border-style: solid;
58
+ border-color: inherit;
59
+ font-weight: var(--font-weight-bold, 700);
60
+
61
+ span {
62
+ font-weight: var(--font-weight-regular, 400);
63
+ }
64
+ }
65
+
66
+ input::placeholder,
67
+ input[type='date'],
68
+ input[type='date']::-webkit-calendar-picker-indicator {
69
+ color: var(--pdf-field-color);
70
+ }
71
+
72
+ &.--selected {
73
+ box-shadow:
74
+ 0 0 0 3px white,
75
+ 0 0 0 5px var(--pdf-field-color-dark);
76
+ cursor: move;
77
+ }
78
+
79
+ &.--colored:not(.--selected) {
80
+ box-shadow: 0 0 3px 1px #616e7b;
81
+ }
82
+
83
+ &.--unselected {
84
+ cursor: pointer;
85
+ }
86
+
87
+ &.--dragging {
88
+ opacity: 0.5;
89
+ }
90
+
91
+ &.--error {
92
+ --pdf-field-color: var(--danger, #dc3545) !important;
93
+ --pdf-field-color-dark: var(--red, #0265dc) !important;
94
+ --pdf-field-opacity-color: var(--danger-opacity, #dc35451f) !important;
95
+ }
96
+
97
+ .dte-add-field-button {
98
+ display: none;
99
+ position: absolute;
100
+ bottom: -20px;
101
+ left: -10px;
102
+ margin-left: 50%;
103
+ padding: var(--spacing-0, 0);
104
+ width: 20px;
105
+ height: 20px;
106
+ }
107
+
108
+ &:hover .dte-add-field-button {
109
+ display: block;
110
+ }
111
+ }
@@ -44,6 +44,7 @@
44
44
  /* Colors - Semantic */
45
45
  --success: #28a745;
46
46
  --danger: #dc3545;
47
+ --danger-opacity: #dc35451f;
47
48
  --green: #28a745;
48
49
  --red: #dc3545;
49
50
  --white: var(--color-neutral-0, #ffffff);
@@ -1,2 +1,3 @@
1
1
  export * from './extract-fields.utils';
2
2
  export * from './resolve-values.utils';
3
+ export * from './resolve-fields-default-values';
@@ -0,0 +1,19 @@
1
+ import { DataModelValues, PdfField } from '../../interface/types';
2
+ import { getTodayDateInputValue } from '../field';
3
+
4
+ export const resolveFieldsDefaultValues = (fields: PdfField[]): DataModelValues => {
5
+ const defaultValues: Record<string, any> = {};
6
+ for (const field of fields) {
7
+ if (field.default !== undefined) {
8
+ if (field.default.type === 'radio') {
9
+ defaultValues[field.path!] = field.id;
10
+ } else if (field.default.type === 'today') {
11
+ defaultValues[field.path!] = getTodayDateInputValue();
12
+ } else {
13
+ defaultValues[field.path!] = field.default.value;
14
+ }
15
+ }
16
+ }
17
+
18
+ return defaultValues;
19
+ };
@@ -0,0 +1 @@
1
+ export * from './data-model/resolve-fields-default-values';
@@ -1,8 +1,8 @@
1
- import { getFieldBackgroundColor } from '../field-background-color.utils';
1
+ import { getFieldColor } from '../field-background-color.utils';
2
2
 
3
- describe('getFieldBackgroundColor', () => {
3
+ describe('getFieldColor', () => {
4
4
  const subject = (recipient: string | undefined, recipientsColors: Record<string, string>) =>
5
- getFieldBackgroundColor(recipient, recipientsColors);
5
+ getFieldColor(recipient, recipientsColors);
6
6
 
7
7
  test('returns color for recipient when present in map', () => {
8
8
  expect(subject('signer1', { signer1: '#ff0000', signer2: '#00ff00' })).toBe('#ff0000');
@@ -34,7 +34,6 @@ describe('getFieldBoundsStyle', () => {
34
34
  padding: 4,
35
35
  alignItems: 'flex-start',
36
36
  justifyContent: 'flex-start',
37
- overflow: 'hidden',
38
37
  lineHeight: 1,
39
38
  });
40
39
  });
@@ -0,0 +1,137 @@
1
+ import { FieldTypeEnum, type FillableDefault, type PdfField } from '../../../interface/types';
2
+ import {
3
+ getFillableEffectiveValue,
4
+ getTodayDateInputValue,
5
+ isRadioDefaultSelected,
6
+ isRadioOptionChecked,
7
+ resolveFillableDefaultValue,
8
+ } from '../get-fillable-default-value.utils';
9
+
10
+ const buildField = (overrides: Partial<PdfField> = {}): PdfField => ({
11
+ id: 'field-1',
12
+ type: FieldTypeEnum.fillable,
13
+ subType: 'text',
14
+ x: 0,
15
+ y: 0,
16
+ page: 1,
17
+ label: 'Field Label',
18
+ width: 100,
19
+ height: 25,
20
+ ...overrides,
21
+ });
22
+
23
+ describe('getTodayDateInputValue', () => {
24
+ test('matches the local calendar date in YYYY-MM-DD form', () => {
25
+ const now = new Date();
26
+ const expected = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(
27
+ now.getDate(),
28
+ ).padStart(2, '0')}`;
29
+
30
+ expect(getTodayDateInputValue()).toBe(expected);
31
+ });
32
+ });
33
+
34
+ describe('resolveFillableDefaultValue', () => {
35
+ test('returns undefined when there is no default (pre-existing fields)', () => {
36
+ expect(resolveFillableDefaultValue(undefined)).toBeUndefined();
37
+ });
38
+
39
+ test('resolves a text default', () => {
40
+ expect(resolveFillableDefaultValue({ type: 'text', value: 'hello' })).toBe('hello');
41
+ });
42
+
43
+ test('resolves a number default', () => {
44
+ expect(resolveFillableDefaultValue({ type: 'number', value: 42 })).toBe(42);
45
+ });
46
+
47
+ test('resolves a checkbox default', () => {
48
+ expect(resolveFillableDefaultValue({ type: 'checkbox', value: true })).toBe(true);
49
+ });
50
+
51
+ test('resolves a fixed date default as-is', () => {
52
+ expect(resolveFillableDefaultValue({ type: 'date', value: '2026-03-15' })).toBe('2026-03-15');
53
+ });
54
+
55
+ test('resolves the today sentinel to the current local date, not a frozen date', () => {
56
+ expect(resolveFillableDefaultValue({ type: 'today' })).toBe(getTodayDateInputValue());
57
+ });
58
+
59
+ test('resolves radio to undefined - selection lives on the option field, not a scalar', () => {
60
+ expect(resolveFillableDefaultValue({ type: 'radio' })).toBeUndefined();
61
+ });
62
+ });
63
+
64
+ describe('getFillableEffectiveValue', () => {
65
+ const field = buildField({ default: { type: 'text', value: 'default text' } });
66
+
67
+ test('with a configured default and no stored value, returns the default', () => {
68
+ expect(getFillableEffectiveValue(field, undefined)).toBe('default text');
69
+ });
70
+
71
+ test('without a configured default and no stored value, returns undefined (starts blank)', () => {
72
+ expect(getFillableEffectiveValue(buildField(), undefined)).toBeUndefined();
73
+ });
74
+
75
+ test('an explicit stored value overrides the default', () => {
76
+ expect(getFillableEffectiveValue(field, 'typed by recipient')).toBe('typed by recipient');
77
+ });
78
+
79
+ test('an explicit empty string is a real stored answer and is not re-defaulted', () => {
80
+ expect(getFillableEffectiveValue(field, '')).toBe('');
81
+ });
82
+
83
+ test('an explicit false is a real stored answer and is not re-defaulted', () => {
84
+ const checkboxField = buildField({
85
+ subType: 'checkbox',
86
+ default: { type: 'checkbox', value: true },
87
+ });
88
+ expect(getFillableEffectiveValue(checkboxField, false)).toBe(false);
89
+ });
90
+
91
+ test('an explicit 0 is a real stored answer and is not re-defaulted', () => {
92
+ const numberField = buildField({ subType: 'number', default: { type: 'number', value: 100 } });
93
+ expect(getFillableEffectiveValue(numberField, 0)).toBe(0);
94
+ });
95
+
96
+ test('a date field defaults to today, resolved fresh rather than frozen at authoring time', () => {
97
+ const dateField = buildField({ subType: 'date', default: { type: 'today' } });
98
+ expect(getFillableEffectiveValue(dateField, undefined)).toBe(getTodayDateInputValue());
99
+ });
100
+ });
101
+
102
+ describe('isRadioDefaultSelected', () => {
103
+ test('true when this option field carries the radio default flag', () => {
104
+ expect(isRadioDefaultSelected(buildField({ subType: 'radio', default: { type: 'radio' } }))).toBe(
105
+ true,
106
+ );
107
+ });
108
+
109
+ test('false when this option has no default', () => {
110
+ expect(isRadioDefaultSelected(buildField({ subType: 'radio' }))).toBe(false);
111
+ });
112
+
113
+ test('false for a mismatched default type (defensive: should never happen post-validation)', () => {
114
+ const mismatched = { type: 'text', value: 'x' } as unknown as FillableDefault;
115
+ expect(isRadioDefaultSelected(buildField({ subType: 'radio', default: mismatched }))).toBe(false);
116
+ });
117
+ });
118
+
119
+ describe('isRadioOptionChecked', () => {
120
+ const defaultOption = buildField({ id: 'opt-yes', subType: 'radio', default: { type: 'radio' } });
121
+ const otherOption = buildField({ id: 'opt-no', subType: 'radio' });
122
+
123
+ test('the flagged option checks itself when the group has no stored answer yet', () => {
124
+ expect(isRadioOptionChecked(defaultOption, undefined)).toBe(true);
125
+ expect(isRadioOptionChecked(otherOption, undefined)).toBe(false);
126
+ });
127
+
128
+ test('a stored selection of a different option overrides the default flag', () => {
129
+ // The recipient picked "opt-no" - the group's stored value is now that option's id.
130
+ expect(isRadioOptionChecked(defaultOption, 'opt-no')).toBe(false);
131
+ expect(isRadioOptionChecked(otherOption, 'opt-no')).toBe(true);
132
+ });
133
+
134
+ test('a stored selection confirming the defaulted option still checks it, not by the default flag', () => {
135
+ expect(isRadioOptionChecked(defaultOption, 'opt-yes')).toBe(true);
136
+ });
137
+ });
@@ -1,4 +1,4 @@
1
- export const getFieldBackgroundColor = (
1
+ export const getFieldColor = (
2
2
  recipient: string | undefined,
3
3
  recipientsColors: Record<string, string>,
4
4
  ): string => {
@@ -0,0 +1,14 @@
1
+ import { type FillableDropdownItem, type PdfField } from '../../interface/types';
2
+
3
+ export const getDropdownFieldItems = (
4
+ field: Pick<PdfField, 'dropdownItems'>,
5
+ ): FillableDropdownItem[] => {
6
+ if (!Array.isArray(field.dropdownItems)) {
7
+ return [];
8
+ }
9
+
10
+ return field.dropdownItems.filter(
11
+ (item): item is FillableDropdownItem =>
12
+ !!item && typeof item.value === 'string' && typeof item.label === 'string',
13
+ );
14
+ };
@@ -4,8 +4,7 @@ import { getFieldTypographyStyle } from './get-field-typography.utils';
4
4
 
5
5
  interface FieldBoundsStyleOptions {
6
6
  pageOffset?: { left: number; top: number };
7
- backgroundColor?: string;
8
- borderColor?: string;
7
+ color?: string;
9
8
  /**
10
9
  * Current page zoom factor (1 = 100%). Field `x/y/width/height` are stored
11
10
  * relative to the unzoomed reference page width, so they must be scaled by
@@ -46,12 +45,15 @@ export const getFieldBoundsStyle = (
46
45
  field: PdfField,
47
46
  options?: FieldBoundsStyleOptions,
48
47
  ): CSSProperties => {
49
- const {
50
- backgroundColor,
51
- borderColor,
52
- pageOffset = { left: 0, top: 0 },
53
- zoom = 1,
54
- } = options ?? {};
48
+ const { color, pageOffset = { left: 0, top: 0 }, zoom = 1 } = options ?? {};
49
+
50
+ const colors = color
51
+ ? {
52
+ '--pdf-field-color': color,
53
+ '--pdf-field-color-dark': color,
54
+ '--pdf-field-opacity-color': `${color}1f`,
55
+ }
56
+ : {};
55
57
 
56
58
  return {
57
59
  position: 'absolute',
@@ -67,11 +69,9 @@ export const getFieldBoundsStyle = (
67
69
  margin: 0,
68
70
  minWidth: 0,
69
71
  minHeight: 0,
70
- overflow: 'hidden',
71
72
  lineHeight: 1,
72
73
  ...getFieldTypeSpecificStyle(field),
73
74
  ...getFieldTypographyStyle(field),
74
- ...(backgroundColor ? { backgroundColor } : {}),
75
- ...(borderColor ? { borderColor } : {}),
75
+ ...colors,
76
76
  };
77
77
  };
@@ -0,0 +1,70 @@
1
+ import { type FillableDefault, type PdfField } from '../../interface/types';
2
+
3
+ /** Today's calendar date in the browser's own local timezone, as `YYYY-MM-DD`. */
4
+ export const getTodayDateInputValue = (): string => {
5
+ const today = new Date();
6
+ const month = String(today.getMonth() + 1).padStart(2, '0');
7
+ const day = String(today.getDate()).padStart(2, '0');
8
+ return `${today.getFullYear()}-${month}-${day}`;
9
+ };
10
+
11
+ /**
12
+ * Resolves a `FillableDefault` to the scalar it should display. `today` resolves dynamically to
13
+ * the current local date every time this is called - it is never frozen at authoring time.
14
+ * `radio` has no scalar of its own (selection is decided per sibling option field, not a value
15
+ * here) and resolves to `undefined`, same as no default at all.
16
+ */
17
+ export const resolveFillableDefaultValue = (
18
+ fillableDefault: FillableDefault | undefined,
19
+ ): string | number | boolean | undefined => {
20
+ switch (fillableDefault?.type) {
21
+ case 'text':
22
+ case 'number':
23
+ case 'checkbox':
24
+ case 'date':
25
+ case 'dropdown':
26
+ return fillableDefault.value;
27
+ case 'today':
28
+ return getTodayDateInputValue();
29
+ default:
30
+ return undefined;
31
+ }
32
+ };
33
+
34
+ /**
35
+ * Merges a stored value with the field's configured default for *display* only: a stored value -
36
+ * including an explicit empty string, `false`, or `0` - always wins, since those are real answers
37
+ * the recipient already gave. Only a path that is genuinely absent from the data object (`storedValue
38
+ * === undefined`) falls back to the default. `??` already has exactly this semantic (it only
39
+ * falls through on `null`/`undefined`), so this is intentionally not a truthiness check.
40
+ */
41
+ export const getFillableEffectiveValue = (
42
+ field: Pick<PdfField, 'default'>,
43
+ /*
44
+ * matches DataModelValues (Record<string, any>): a stored answer's runtime type varies by
45
+ * fillable subtype (string/number/boolean), same looseness callers already had via
46
+ * `data?.[field.path]`.
47
+ */
48
+ storedValue: any,
49
+ ): any => {
50
+ return storedValue ?? resolveFillableDefaultValue(field.default);
51
+ };
52
+
53
+ /** Whether this specific radio option field is the group's pre-selected default. */
54
+ export const isRadioDefaultSelected = (field: Pick<PdfField, 'default'>): boolean => {
55
+ return field.default?.type === 'radio';
56
+ };
57
+
58
+ /**
59
+ * Whether this radio option should render checked: a stored selection for the group (any option's
60
+ * id, resolved by the caller) always wins, even if it picked a *different* option than the
61
+ * default - that is what "override" means for a radio group. Only when the group's path is
62
+ * genuinely unanswered yet (`resolvedValue === undefined`) does this option's own default flag
63
+ * decide it.
64
+ */
65
+ export const isRadioOptionChecked = (
66
+ field: Pick<PdfField, 'id' | 'default'>,
67
+ resolvedValue: unknown,
68
+ ): boolean => {
69
+ return resolvedValue !== undefined ? resolvedValue === field.id : isRadioDefaultSelected(field);
70
+ };
@@ -11,3 +11,5 @@ export * from './get-field-text-clip.utils';
11
11
  export * from './get-field-type-option.utils';
12
12
  export * from './get-field-typography.utils';
13
13
  export * from './get-navigable-fields.utils';
14
+ export * from './get-dropdown-field-items.utils';
15
+ export * from './get-fillable-default-value.utils';
@@ -1,29 +1,29 @@
1
1
  import { type RecipientInfo } from '../../interface/types';
2
2
 
3
3
  const colors = [
4
- '#1F77B41f',
5
- '#FF7F0E1f',
6
- '#2CA02C1f',
7
- '#9467BD1f',
8
- '#8C564B1f',
9
- '#E377C21f',
10
- '#7F7F7F1f',
11
- '#BCBD221f',
12
- '#17BECF1f',
13
- '#D627281f',
14
- '#98DF8A1f',
15
- '#FFBB781f',
16
- '#C5B0D51f',
17
- '#C49C941f',
18
- '#F7B6D21f',
19
- '#C7C7C71f',
20
- '#DBDB8D1f',
21
- '#9EDAE51f',
22
- '#E5E5E51f',
23
- '#393B791f',
24
- '#5254A31f',
25
- '#6B6EBD1f',
26
- '#9C9EDA1f',
4
+ '#1F77B4',
5
+ '#FF7F0E',
6
+ '#2CA02C',
7
+ '#9467BD',
8
+ '#8C564B',
9
+ '#E377C2',
10
+ '#7F7F7F',
11
+ '#BCBD22',
12
+ '#17BECF',
13
+ '#D62728',
14
+ '#98DF8A',
15
+ '#FFBB78',
16
+ '#C5B0D5',
17
+ '#C49C94',
18
+ '#F7B6D2',
19
+ '#C7C7C7',
20
+ '#DBDB8D',
21
+ '#9EDAE5',
22
+ '#E5E5E5',
23
+ '#393B79',
24
+ '#5254A3',
25
+ '#6B6EBD',
26
+ '#9C9EDA',
27
27
  ];
28
28
 
29
29
  export const mapColorsToRecipients = (recipients?: RecipientInfo[]): Record<string, string> => {