@brftech/filex-core 0.42.1 → 0.43.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 (233) hide show
  1. package/README.md +14 -2
  2. package/dist/ArchiveViewer-BqU761Wk.js +149 -0
  3. package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
  4. package/dist/CsvViewer-ByHVK5ga.js +172 -0
  5. package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
  6. package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
  9. package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
  10. package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
  11. package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
  12. package/dist/MermaidViewer-CEDTD_pp.js +136 -0
  13. package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
  14. package/dist/PsdViewer-CZcxUpZO.js +121 -0
  15. package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
  16. package/dist/TiffViewer-BwI62CbQ.js +141 -0
  17. package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
  18. package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
  19. package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
  20. package/dist/filex-core.js +475 -219
  21. package/dist/filex-core.umd.cjs +165 -108
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/fonts/caveat-latin-ext.woff2 +0 -0
  24. package/dist/fonts/caveat-latin.woff2 +0 -0
  25. package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
  26. package/dist/fonts/dancing-script-latin.woff2 +0 -0
  27. package/dist/fonts/homemade-apple-latin.woff2 +0 -0
  28. package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
  29. package/dist/fonts/source-serif-4-latin.woff2 +0 -0
  30. package/dist/index-DuWnGuv5.js +35901 -0
  31. package/dist/index-DuWnGuv5.js.map +1 -0
  32. package/dist/index.d.ts +5241 -553
  33. package/dist/pdf-CaPYsOUo.js +21302 -0
  34. package/dist/pdf-CaPYsOUo.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/package.json +1 -1
  37. package/src/FileExplorer.vue +1207 -343
  38. package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
  39. package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
  40. package/src/assets/fonts/README.md +58 -0
  41. package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
  42. package/src/assets/fonts/caveat-latin.woff2 +0 -0
  43. package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
  44. package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
  45. package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
  46. package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
  47. package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
  48. package/src/components/AdvancedSearch.vue +49 -0
  49. package/src/components/Breadcrumb.vue +5 -5
  50. package/src/components/ChoiceButtons.vue +152 -0
  51. package/src/components/CommandPalette.vue +5 -5
  52. package/src/components/ConnectionGuideView.vue +1 -1
  53. package/src/components/ConnectionNotice.vue +59 -0
  54. package/src/components/ConnectionsPanel.vue +30 -568
  55. package/src/components/ContextMenu.vue +31 -5
  56. package/src/components/DataTable.vue +1331 -0
  57. package/src/components/FilePane.vue +3 -0
  58. package/src/components/FilterBar.vue +12 -5
  59. package/src/components/GalleryView.vue +50 -2
  60. package/src/components/GridView.vue +51 -3
  61. package/src/components/HomeView.vue +18 -6
  62. package/src/components/InspectorPanel.vue +134 -17
  63. package/src/components/ListView.vue +366 -900
  64. package/src/components/LogoMark.vue +57 -0
  65. package/src/components/NFSExportsPanel.vue +76 -28
  66. package/src/components/OnboardingTour.vue +19 -7
  67. package/src/components/OperationsCenter.vue +77 -1
  68. package/src/components/PendingOpsTray.vue +59 -5
  69. package/src/components/PresenceBar.vue +7 -7
  70. package/src/components/ProductVersion.vue +31 -0
  71. package/src/components/QuickLook.vue +10 -14
  72. package/src/components/RecentlyOpened.vue +47 -17
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -56
  76. package/src/components/SSHKeysPanel.vue +79 -26
  77. package/src/components/SideNav.vue +260 -96
  78. package/src/components/StarButton.vue +1 -0
  79. package/src/components/StorageFields.vue +74 -6
  80. package/src/components/StorageTags.vue +62 -0
  81. package/src/components/TabBar.vue +1 -1
  82. package/src/components/TagKindIcon.vue +56 -0
  83. package/src/components/TagPicker.vue +226 -40
  84. package/src/components/ThemePalette.vue +4 -4
  85. package/src/components/TimeZonePicker.vue +29 -10
  86. package/src/components/TokensPanel.vue +82 -27
  87. package/src/components/Toolbar.vue +28 -6
  88. package/src/components/plugin/PluginConfirmModal.vue +54 -0
  89. package/src/components/plugin/PluginInspectorSection.vue +149 -0
  90. package/src/components/plugin/PluginPageView.vue +339 -0
  91. package/src/components/plugin/PluginViewModal.vue +159 -0
  92. package/src/components/plugin/SurfaceConversation.vue +76 -0
  93. package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
  94. package/src/components/plugin/SurfaceRenderer.vue +299 -0
  95. package/src/components/plugin/SurfaceSections.vue +91 -0
  96. package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
  97. package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
  98. package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
  99. package/src/components/plugin/nodes/SurfaceList.vue +127 -0
  100. package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
  101. package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
  102. package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
  103. package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
  104. package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
  105. package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
  106. package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
  107. package/src/components/public/PublicLanguagePicker.vue +58 -0
  108. package/src/components/public/PublicLinkPage.vue +286 -0
  109. package/src/components/public/PublicLinkPreview.vue +85 -0
  110. package/src/components/public/PublicPinGate.vue +108 -0
  111. package/src/components/public/PublicRequestBody.vue +204 -0
  112. package/src/components/public/PublicShareBody.vue +188 -0
  113. package/src/components/public/PublicShell.vue +332 -0
  114. package/src/composables/useConnections.ts +8 -2
  115. package/src/composables/useFileApi.ts +183 -44
  116. package/src/composables/useLocale.ts +277 -41
  117. package/src/composables/useNFSExports.ts +8 -10
  118. package/src/composables/useOperations.ts +44 -3
  119. package/src/composables/usePendingOps.ts +59 -8
  120. package/src/composables/usePluginActions.ts +121 -0
  121. package/src/composables/usePluginSurface.ts +223 -0
  122. package/src/composables/usePublicBranding.ts +172 -0
  123. package/src/composables/usePublicLink.ts +573 -0
  124. package/src/composables/usePublicPage.ts +61 -0
  125. package/src/composables/useS3Keys.ts +17 -12
  126. package/src/composables/useSSHKeys.ts +8 -10
  127. package/src/composables/useTokens.ts +8 -12
  128. package/src/composables/useUploadChunked.ts +18 -8
  129. package/src/composables/useViewerFetch.ts +6 -3
  130. package/src/index.ts +404 -8
  131. package/src/lib/actionIcons.ts +46 -1
  132. package/src/lib/advSearch.ts +14 -7
  133. package/src/lib/appLock.ts +130 -0
  134. package/src/lib/connection.ts +110 -0
  135. package/src/lib/destinationTree.ts +19 -3
  136. package/src/lib/direction.ts +353 -0
  137. package/src/lib/dragGhost.ts +6 -0
  138. package/src/lib/dropLimits.ts +69 -0
  139. package/src/lib/errorWords.ts +394 -0
  140. package/src/lib/externalReach.ts +5 -1
  141. package/src/lib/fileFilters.ts +7 -2
  142. package/src/lib/fileIcons.ts +4 -0
  143. package/src/lib/internalPaths.ts +77 -0
  144. package/src/lib/listInput.ts +64 -0
  145. package/src/lib/listing.ts +58 -23
  146. package/src/lib/modalStack.ts +26 -0
  147. package/src/lib/nodeRow.ts +121 -0
  148. package/src/lib/pdfFieldRules.ts +126 -0
  149. package/src/lib/pdfFields.ts +589 -0
  150. package/src/lib/pdfFieldsGeom.ts +203 -0
  151. package/src/lib/pdfjsLoader.ts +51 -0
  152. package/src/lib/personName.ts +47 -0
  153. package/src/lib/pluginApplies.ts +123 -0
  154. package/src/lib/pluginLabel.ts +72 -0
  155. package/src/lib/pluginMenu.ts +253 -0
  156. package/src/lib/pluginPage.ts +58 -0
  157. package/src/lib/plural.ts +181 -0
  158. package/src/lib/popupLayer.ts +103 -0
  159. package/src/lib/prefs.ts +563 -0
  160. package/src/lib/productVersion.ts +35 -0
  161. package/src/lib/publicLayout.ts +39 -0
  162. package/src/lib/serviceGate.ts +104 -0
  163. package/src/lib/shareTtl.ts +25 -11
  164. package/src/lib/signFonts.ts +85 -0
  165. package/src/lib/signaturePad.ts +111 -0
  166. package/src/lib/sortOrder.ts +100 -38
  167. package/src/lib/star.ts +7 -1
  168. package/src/lib/surfaceCell.ts +41 -0
  169. package/src/lib/surfaceConditions.ts +156 -0
  170. package/src/lib/surfaceOpen.ts +106 -0
  171. package/src/lib/surfaceSteps.ts +68 -0
  172. package/src/lib/surfaceValues.ts +191 -0
  173. package/src/lib/symlink.ts +150 -0
  174. package/src/lib/tableColumns.ts +493 -0
  175. package/src/lib/tableEnv.ts +34 -0
  176. package/src/lib/tablePrefs.ts +84 -0
  177. package/src/lib/tags.ts +121 -12
  178. package/src/lib/themes.ts +286 -18
  179. package/src/lib/tour.ts +152 -0
  180. package/src/lib/trashEmpty.ts +111 -0
  181. package/src/lib/trashTimeLeft.ts +23 -0
  182. package/src/lib/uiLocales.ts +460 -0
  183. package/src/lib/viewPrefs.ts +696 -660
  184. package/src/lib/viewPrefsHttp.ts +78 -0
  185. package/src/locales/en.ts +522 -119
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +539 -155
  188. package/src/modals/ConvertModal.vue +62 -11
  189. package/src/modals/DestinationPickerModal.vue +59 -16
  190. package/src/modals/Modal.vue +113 -23
  191. package/src/modals/NewDocumentModal.vue +16 -6
  192. package/src/modals/NewFolderModal.vue +8 -0
  193. package/src/modals/PermissionsModal.vue +272 -163
  194. package/src/modals/PreviewModal.vue +119 -20
  195. package/src/modals/RenameModal.vue +37 -1
  196. package/src/styles/base.css +2895 -241
  197. package/src/styles/sign-fonts.css +118 -0
  198. package/src/styles/variables.css +60 -0
  199. package/src/types/Connections.ts +17 -0
  200. package/src/types/ExplorerConfig.ts +117 -1
  201. package/src/types/FileNode.ts +58 -0
  202. package/src/types/Plugins.ts +494 -0
  203. package/src/types/Public.ts +212 -0
  204. package/src/viewers/ArchiveViewer.vue +93 -48
  205. package/src/viewers/CsvViewer.vue +99 -53
  206. package/src/viewers/DrawioViewer.vue +35 -9
  207. package/src/viewers/EpubViewer.vue +7 -8
  208. package/src/viewers/IpynbViewer.vue +3 -1
  209. package/src/viewers/MermaidViewer.vue +4 -5
  210. package/src/viewers/PdfViewer.vue +8 -17
  211. package/src/viewers/PsdViewer.vue +2 -4
  212. package/src/viewers/TiffViewer.vue +4 -5
  213. package/src/viewers/Viewer3D.vue +1 -1
  214. package/LICENSE +0 -21
  215. package/dist/ArchiveViewer-CLNevfPE.js +0 -124
  216. package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
  217. package/dist/CsvViewer-K8XA3z7N.js +0 -141
  218. package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
  219. package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
  220. package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
  221. package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
  222. package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
  223. package/dist/MermaidViewer-DqucH_io.js +0 -137
  224. package/dist/MermaidViewer-DqucH_io.js.map +0 -1
  225. package/dist/PsdViewer-CwiVENmq.js +0 -122
  226. package/dist/PsdViewer-CwiVENmq.js.map +0 -1
  227. package/dist/TiffViewer-CpewSVmQ.js +0 -142
  228. package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
  229. package/dist/Viewer3D-CIrOn63I.js.map +0 -1
  230. package/dist/index-Dx0IbBcI.js +0 -26831
  231. package/dist/index-Dx0IbBcI.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
@@ -0,0 +1,589 @@
1
+ /**
2
+ * pdfFields — the `pdf-fields` node's rules, as pure data.
3
+ *
4
+ * What a field is, which fields a signer may touch, what the node sends
5
+ * back in each mode, and how the editor adds, copies and colours boxes —
6
+ * all DOM-free so the tests can hold the rules without pdf.js or a canvas.
7
+ * `SurfacePdfFields.vue` is the drawing; this is the meaning.
8
+ *
9
+ * Wire shapes follow docs/APP-PLUGINS-API.md → "Frontend needs (M3)":
10
+ * edit mode → `data.values[id]` = the whole `fields[]` array
11
+ * fill mode → `data.values[id]` = `{fields: [{id, value}]}` for the
12
+ * signer's own fields that hold a value
13
+ */
14
+ import type { PluginText } from '../types/Plugins';
15
+ import { clampFrac } from './pdfFieldsGeom';
16
+ import { isSignFontKey, type SignFontKey } from './signFonts';
17
+ import { normalizeRule, type PdfFieldRule } from './pdfFieldRules';
18
+
19
+ export const PDF_FIELD_TYPES = ['signature', 'initials', 'date', 'text', 'checkbox'] as const;
20
+ export type PdfFieldType = (typeof PDF_FIELD_TYPES)[number];
21
+
22
+ /**
23
+ * How a signature or initials box is given: a drawing (the pad's draw or
24
+ * upload), or a name typed in one of the shipped faces. Absent = drawn.
25
+ */
26
+ export type PdfSignatureStyle = 'drawn' | 'typed';
27
+
28
+ /** A box whose value is a picture of somebody's hand: signature, initials. */
29
+ export function isDrawnType(type: string | undefined): boolean {
30
+ return type === 'signature' || type === 'initials';
31
+ }
32
+
33
+ /**
34
+ * One line a plugin can print under a signature — the node's
35
+ * `stamp_lines[]` (docs/APP-PLUGINS-API.md → pdf-fields).
36
+ *
37
+ * ⚠⚠ The catalogue is the PLUGIN's, and so are the words: `examples` is how
38
+ * the plugin itself would write the line for each signer id (`'*'` for a box
39
+ * that belongs to anyone), so a card previews the plugin's wording instead
40
+ * of this package inventing one that the paper then contradicts. The VALUES
41
+ * printed on the day come from the plugin's signing record — never from
42
+ * anything typed on this screen.
43
+ */
44
+ export interface PdfStampLine {
45
+ id: string;
46
+ label: PluginText | string;
47
+ examples?: Record<string, PluginText | string>;
48
+ /** Printed when a box has made no choice of its own. */
49
+ default?: boolean;
50
+ }
51
+
52
+ /**
53
+ * A plugin's catalogue prop (`formats`, `stamp_lines`): the entries that
54
+ * carry an id, each once, in the plugin's order, with their label — and
55
+ * whatever else `more` reads off the raw entry.
56
+ */
57
+ function catalogue<T extends { id: string; label: PluginText | string }>(
58
+ raw: unknown,
59
+ more: (entry: T, src: Record<string, unknown>) => void,
60
+ ): T[] {
61
+ if (!Array.isArray(raw)) return [];
62
+ const out: T[] = [];
63
+ const seen = new Set<string>();
64
+ for (const src of raw as Array<Record<string, unknown>>) {
65
+ if (!src || typeof src !== 'object') continue;
66
+ const id = typeof src.id === 'string' ? src.id.trim() : '';
67
+ if (!id || seen.has(id)) continue;
68
+ seen.add(id);
69
+ const entry = { id, label: (src.label as PluginText | string) ?? id } as T;
70
+ more(entry, src);
71
+ out.push(entry);
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** The `stamp_lines` prop, dropped of anything that could not be offered. */
77
+ export function normalizeStampLines(raw: unknown): PdfStampLine[] {
78
+ return catalogue<PdfStampLine>(raw, (entry, l) => {
79
+ if (l.examples && typeof l.examples === 'object' && !Array.isArray(l.examples)) {
80
+ entry.examples = l.examples as Record<string, PluginText | string>;
81
+ }
82
+ if (l.default === true) entry.default = true;
83
+ });
84
+ }
85
+
86
+ /**
87
+ * The line ids a box prints, in the CATALOGUE's order: its own choice, or
88
+ * the catalogue's defaults when it has none. An explicit empty list is a
89
+ * choice (nothing under the signature) and is kept as one.
90
+ */
91
+ export function linesOf(field: Pick<PdfField, 'lines'>, catalogue: PdfStampLine[]): string[] {
92
+ if (Array.isArray(field.lines)) {
93
+ const want = new Set(field.lines);
94
+ return catalogue.filter((l) => want.has(l.id)).map((l) => l.id);
95
+ }
96
+ return catalogue.filter((l) => l.default).map((l) => l.id);
97
+ }
98
+
99
+ /** The plugin's own wording of one line for a box's signer (`'*'` = anyone). */
100
+ export function stampExample(line: PdfStampLine, assignee: string | undefined): PluginText | string {
101
+ const ex = line.examples ?? {};
102
+ return ex[assignee || '*'] ?? ex['*'] ?? '';
103
+ }
104
+
105
+ export interface PdfField {
106
+ id: string;
107
+ type: PdfFieldType;
108
+ /**
109
+ * v3 3.1 - what the field is CALLED ("Ad soyad", "Imza", "Tarih").
110
+ *
111
+ * The reason it exists: a signer was shown a whole document and told to
112
+ * find their boxes. A named field can be listed, asked for and reported -
113
+ * the label shows in the box, in the fill form and in the audit trail - and
114
+ * three boxes of the same type on one page stop being indistinguishable.
115
+ * Absent: the type's own name stands in, which is what every field placed
116
+ * before v3 has.
117
+ */
118
+ label?: string;
119
+ /** 1-based. */
120
+ page: number;
121
+ x: number;
122
+ y: number;
123
+ w: number;
124
+ h: number;
125
+ /** A signer id (`signers[].id`). Absent: anybody's. */
126
+ assignee?: string;
127
+ required?: boolean;
128
+ /**
129
+ * v3 3.3 - false while the box has been DEFINED but not yet put on the
130
+ * page.
131
+ *
132
+ * Naming a box and finding a place for it are two different jobs, and
133
+ * doing them in one screen is what made the wizard's hardest step: a
134
+ * palette of types, a rectangle drawn under the pointer and a strip of
135
+ * properties for whichever box happened to be selected. Defined first,
136
+ * placed after: the `define` screen writes boxes with `placed: false`,
137
+ * and `place` hands them out one at a time. Absent means placed, so every
138
+ * field written before this (and every other plugin's) reads as it did.
139
+ */
140
+ placed?: boolean;
141
+ value?: unknown;
142
+ /**
143
+ * `text` only: what the box accepts (`lib/pdfFieldRules`). Enforced while
144
+ * the person types and echoed back with the value, so the stamping plugin
145
+ * formats what it is handed instead of guessing from the string.
146
+ */
147
+ rule?: PdfFieldRule;
148
+ /**
149
+ * `date` only: which layout the date is written in, as one of the ids the
150
+ * surface offered in its `formats` prop (`DD.MM.YYYY`, `MM/DD/YYYY`,
151
+ * `YYYY-MM-DD` for the signing app). It is the date field's answer to what
152
+ * `rule` is for a text one, and it has to SURVIVE the round trip: a box
153
+ * whose layout is dropped here comes back to the plugin as "no layout" and
154
+ * is stamped in whatever that plugin defaults to — a form that asked for
155
+ * `12/31/2000` quietly starts printing `31.12.2000`.
156
+ *
157
+ * ⚠ Not narrowed to a fixed set on purpose: the catalogue belongs to the
158
+ * plugin, not to this package.
159
+ */
160
+ format?: string;
161
+ /**
162
+ * The face the words are drawn in — one of `lib/signFonts`' five keys. On
163
+ * a `signature`/`initials` field it is the typed-signature face (and only
164
+ * means something when `style` is `typed`); on a `text`/`date` one it is
165
+ * the face the stamper should use. Absent = the screen's default.
166
+ */
167
+ font?: SignFontKey;
168
+ /**
169
+ * `signature`/`initials` only: drawn by hand (absent) or a name typed in
170
+ * `font`. The requester decides; the signer's pad then offers exactly that.
171
+ */
172
+ style?: PdfSignatureStyle;
173
+ /**
174
+ * `signature`/`initials` only: which of the plugin's `stamp_lines` are
175
+ * printed under this signature, by id. Absent = the plugin's default set;
176
+ * `[]` = nothing under it.
177
+ */
178
+ lines?: string[];
179
+ }
180
+
181
+ export interface PdfSigner {
182
+ id: string;
183
+ label: PluginText | string;
184
+ color?: string;
185
+ }
186
+
187
+ /** A sensible first size per type, as fractions of a portrait page. */
188
+ export const DEFAULT_FIELD_SIZE: Record<PdfFieldType, { w: number; h: number }> = {
189
+ signature: { w: 0.28, h: 0.06 },
190
+ initials: { w: 0.1, h: 0.05 },
191
+ date: { w: 0.16, h: 0.035 },
192
+ text: { w: 0.26, h: 0.035 },
193
+ checkbox: { w: 0.03, h: 0.022 },
194
+ };
195
+
196
+ /** Six colours that read on both palettes; a signer without `color` takes one by position. */
197
+ export const SIGNER_PALETTE = ['#2563eb', '#16a34a', '#d97706', '#9333ea', '#dc2626', '#0891b2'];
198
+
199
+ export function isFieldType(v: unknown): v is PdfFieldType {
200
+ return typeof v === 'string' && (PDF_FIELD_TYPES as readonly string[]).includes(v);
201
+ }
202
+
203
+ /** The palette a surface allows: `types` narrowed to known ones, else every type. */
204
+ export function allowedTypes(raw: unknown): PdfFieldType[] {
205
+ if (!Array.isArray(raw)) return [...PDF_FIELD_TYPES];
206
+ const picked = raw.filter(isFieldType);
207
+ return picked.length ? PDF_FIELD_TYPES.filter((t) => picked.includes(t)) : [...PDF_FIELD_TYPES];
208
+ }
209
+
210
+ /** The wire's `fields[]`, dropped of anything a browser cannot draw. */
211
+ export function normalizeFields(raw: unknown): PdfField[] {
212
+ if (!Array.isArray(raw)) return [];
213
+ const out: PdfField[] = [];
214
+ const seen = new Set<string>();
215
+ for (const f of raw as Array<Record<string, unknown>>) {
216
+ if (!f || typeof f !== 'object') continue;
217
+ const id = String(f.id ?? '');
218
+ if (!id || seen.has(id) || !isFieldType(f.type)) continue;
219
+ seen.add(id);
220
+ const box = clampFrac({ x: Number(f.x), y: Number(f.y), w: Number(f.w), h: Number(f.h) });
221
+ const page = Math.max(1, Math.trunc(Number(f.page) || 1));
222
+ const field: PdfField = { id, type: f.type, page, ...box };
223
+ // ⚠ EXACTLY as typed: any script, inner spaces, and a trailing one.
224
+ // This array round-trips through the plugin while the name is being
225
+ // typed (`change` is debounced at 300 ms), so a trim here takes the
226
+ // space back off between two words. Only a name that is nothing at all
227
+ // is no name; one that is nothing but spaces is carried so the step can
228
+ // refuse it and say why.
229
+ if (typeof f.label === 'string' && f.label) field.label = f.label;
230
+ if (typeof f.assignee === 'string' && f.assignee) field.assignee = f.assignee;
231
+ if (f.required === true) field.required = true;
232
+ // ⚠ Only `false` is carried. "Placed" is the ordinary state and says
233
+ // nothing; a flag on every box would be noise on the wire and a second
234
+ // thing to keep true.
235
+ if (f.placed === false) field.placed = false;
236
+ if (f.value !== undefined && f.value !== null) field.value = f.value;
237
+ const rule = normalizeRule(f.rule);
238
+ if (rule) field.rule = rule;
239
+ if (typeof f.format === 'string' && f.format.trim()) field.format = f.format.trim();
240
+ if (isSignFontKey(f.font)) field.font = f.font;
241
+ // ⚠ Both ride only on a box whose value is a hand. They must survive the
242
+ // round trip (the wizard's state is rebuilt from what this posts), and
243
+ // they mean nothing on a date or a tick.
244
+ if (isDrawnType(f.type)) {
245
+ if (f.style === 'typed') field.style = 'typed';
246
+ if (Array.isArray(f.lines)) {
247
+ field.lines = [...new Set((f.lines as unknown[]).filter((x): x is string => typeof x === 'string' && !!x))].slice(0, 16);
248
+ }
249
+ }
250
+ out.push(field);
251
+ }
252
+ return out;
253
+ }
254
+
255
+ /** Is this box on the page yet? Absent `placed` means yes. */
256
+ export function isPlaced(f: PdfField): boolean {
257
+ return f.placed !== false;
258
+ }
259
+
260
+ /** The boxes still waiting for a place, in the order they were defined. */
261
+ export function unplacedFields(list: PdfField[]): PdfField[] {
262
+ return list.filter((f) => !isPlaced(f));
263
+ }
264
+
265
+ /**
266
+ * Define a box without placing it: a name, a type, whose it is — and a
267
+ * sensible rectangle it will WEAR once somebody drops it on a page, so
268
+ * placing is one click rather than a drawing exercise.
269
+ */
270
+ export function defineField(
271
+ list: PdfField[],
272
+ type: PdfFieldType,
273
+ opts: { label?: string; assignee?: string; required?: boolean } = {},
274
+ ): PdfField {
275
+ const size = DEFAULT_FIELD_SIZE[type];
276
+ const f: PdfField = {
277
+ id: newFieldId(type, list),
278
+ type,
279
+ page: 1,
280
+ x: 0.1,
281
+ y: 0.1,
282
+ w: size.w,
283
+ h: size.h,
284
+ placed: false,
285
+ };
286
+ if (opts.label?.trim()) f.label = opts.label.trim();
287
+ if (opts.assignee) f.assignee = opts.assignee;
288
+ if (opts.required) f.required = true;
289
+ return f;
290
+ }
291
+
292
+ /**
293
+ * One date layout a surface offers: the id that travels as `format`, the
294
+ * name to put on the button, and an EXAMPLE of a date written that way —
295
+ * `DD.MM.YYYY` tells somebody nothing that `31.12.2000` does not tell them
296
+ * better.
297
+ */
298
+ export interface PdfDateFormat {
299
+ id: string;
300
+ /** A `Text` from the plugin, as `signers[].label` is; resolve with `lib/pluginLabel`. */
301
+ label: PluginText | string;
302
+ example?: string;
303
+ /**
304
+ * The TWO ANSWERS that made this layout, when the plugin sends them: the
305
+ * arrangement (`DDMMYYYY`, `MMDDYY`, `YYYYMMDD`…) and the separator
306
+ * (`.`, `/`, `-`, a space), each with the plugin's own name for it.
307
+ * Present: the editor asks the two questions separately, which is what a
308
+ * grid of layouts really is. Absent: one row of whole layouts, as before.
309
+ */
310
+ order?: string;
311
+ orderLabel?: PluginText | string;
312
+ separator?: string;
313
+ separatorLabel?: PluginText | string;
314
+ }
315
+
316
+ /** The `formats` prop, dropped of anything that could not be drawn. */
317
+ export function normalizeFormats(raw: unknown): PdfDateFormat[] {
318
+ return catalogue<PdfDateFormat>(raw, (entry, f) => {
319
+ if (typeof f.example === 'string' && f.example.trim()) entry.example = f.example.trim();
320
+ if (typeof f.order === 'string' && f.order.trim()) entry.order = f.order.trim();
321
+ if (f.order_label) entry.orderLabel = f.order_label as PluginText | string;
322
+ // ⚠ NOT trimmed and NOT tested for truthiness: a single space is one of
323
+ // the separators offered, and both would throw it away.
324
+ if (typeof f.separator === 'string' && f.separator !== '') entry.separator = f.separator;
325
+ if (f.separator_label) entry.separatorLabel = f.separator_label as PluginText | string;
326
+ });
327
+ }
328
+
329
+ /** The three captions the date control needs, as the plugin words them. */
330
+ export interface PdfDateLabels {
331
+ order?: PluginText | string;
332
+ separator?: PluginText | string;
333
+ example?: PluginText | string;
334
+ }
335
+
336
+ /** The `date_labels` prop, with anything unusable dropped. */
337
+ export function normalizeDateLabels(raw: unknown): PdfDateLabels {
338
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
339
+ const src = raw as Record<string, unknown>;
340
+ const out: PdfDateLabels = {};
341
+ for (const k of ['order', 'separator', 'example'] as const) {
342
+ const v = src[k];
343
+ if (typeof v === 'string' ? v.trim() : v && typeof v === 'object') out[k] = v as PluginText | string;
344
+ }
345
+ return out;
346
+ }
347
+
348
+ /** One button in a date control: the value that travels, the plugin's name for it. */
349
+ export interface PdfDateChoice {
350
+ id: string;
351
+ label: PluginText | string;
352
+ }
353
+
354
+ /**
355
+ * The two questions a `formats` catalogue really asks, or null when it
356
+ * does not ask them — the order and the separator, each once, in the
357
+ * catalogue's own order.
358
+ *
359
+ * ⚠ Null when ANY entry is missing half of itself: half a grid drawn as
360
+ * two rows of buttons would offer combinations the plugin never listed,
361
+ * and a layout the plugin does not know is a date stamped in a shape it
362
+ * did not agree to.
363
+ */
364
+ export function dateChoices(
365
+ formats: PdfDateFormat[],
366
+ ): { orders: PdfDateChoice[]; separators: PdfDateChoice[] } | null {
367
+ if (!formats.length) return null;
368
+ const orders: PdfDateChoice[] = [];
369
+ const separators: PdfDateChoice[] = [];
370
+ for (const f of formats) {
371
+ if (f.order === undefined || f.separator === undefined) return null;
372
+ if (!orders.some((o) => o.id === f.order)) orders.push({ id: f.order, label: f.orderLabel ?? f.order });
373
+ if (!separators.some((s) => s.id === f.separator)) {
374
+ separators.push({ id: f.separator, label: f.separatorLabel ?? f.separator });
375
+ }
376
+ }
377
+ return orders.length && separators.length ? { orders, separators } : null;
378
+ }
379
+
380
+ /**
381
+ * The layout id for a pair of answers: the catalogue's own entry for it,
382
+ * or — when the plugin listed no such pair — the first entry with that
383
+ * arrangement, so changing the separator can never leave the box with a
384
+ * layout the plugin has never heard of.
385
+ */
386
+ export function formatFor(formats: PdfDateFormat[], order: string, separator: string): string {
387
+ const exact = formats.find((f) => f.order === order && f.separator === separator);
388
+ if (exact) return exact.id;
389
+ return formats.find((f) => f.order === order)?.id ?? formats[0]?.id ?? '';
390
+ }
391
+
392
+ /** The two answers behind a layout id, as the catalogue records them. */
393
+ export function splitFormat(
394
+ formats: PdfDateFormat[],
395
+ id: string,
396
+ ): { order: string; separator: string } | null {
397
+ const hit = formats.find((f) => f.id === id);
398
+ if (!hit || hit.order === undefined || hit.separator === undefined) return null;
399
+ return { order: hit.order, separator: hit.separator };
400
+ }
401
+
402
+ /** `DDMMYYYY` → ['DD','MM','YYYY']; anything else in the id is ignored. */
403
+ function orderTokens(order: string): string[] {
404
+ const out: string[] = [];
405
+ let i = 0;
406
+ while (i < order.length) {
407
+ if (order.startsWith('YYYY', i)) {
408
+ out.push('YYYY');
409
+ i += 4;
410
+ } else if (order.startsWith('YY', i)) {
411
+ out.push('YY');
412
+ i += 2;
413
+ } else if (order.startsWith('DD', i)) {
414
+ out.push('DD');
415
+ i += 2;
416
+ } else if (order.startsWith('MM', i)) {
417
+ out.push('MM');
418
+ i += 2;
419
+ } else i += 1;
420
+ }
421
+ return out;
422
+ }
423
+
424
+ /**
425
+ * A real date written the chosen way — what the card shows under the two
426
+ * questions, and what the paper will carry.
427
+ *
428
+ * ⚠ Plain ASCII digits, and no `Intl` anywhere near it. The reader's
429
+ * language decides NOTHING here: the arrangement and the separator are the
430
+ * ones just chosen, and the digits are the ones the stamper will print. An
431
+ * example localised into Arabic-Indic numerals would be a promise the PDF
432
+ * then breaks.
433
+ */
434
+ export function dateExample(order: string, separator: string, on: Date): string {
435
+ const parts = orderTokens(order).map((t) => {
436
+ if (t === 'YYYY') return String(on.getFullYear()).padStart(4, '0');
437
+ if (t === 'YY') return String(on.getFullYear() % 100).padStart(2, '0');
438
+ if (t === 'MM') return String(on.getMonth() + 1).padStart(2, '0');
439
+ return String(on.getDate()).padStart(2, '0');
440
+ });
441
+ return parts.join(separator);
442
+ }
443
+
444
+ export function normalizeSigners(raw: unknown): PdfSigner[] {
445
+ if (!Array.isArray(raw)) return [];
446
+ return (raw as Array<Record<string, unknown>>)
447
+ .filter((s) => s && typeof s === 'object' && typeof s.id === 'string' && s.id)
448
+ .map((s) => ({
449
+ id: s.id as string,
450
+ label: (s.label as PluginText | string) ?? (s.id as string),
451
+ color: typeof s.color === 'string' ? s.color : undefined,
452
+ }));
453
+ }
454
+
455
+ /** The colour a field is drawn in: its signer's, else by the signer's position, else the first. */
456
+ export function signerColor(signers: PdfSigner[], assignee: string | undefined): string {
457
+ const idx = signers.findIndex((s) => s.id === assignee);
458
+ if (idx < 0) return SIGNER_PALETTE[0];
459
+ return signers[idx].color || SIGNER_PALETTE[idx % SIGNER_PALETTE.length];
460
+ }
461
+
462
+ /**
463
+ * Fill mode: may `signer` act on this field? Their own, and any field that
464
+ * was never assigned (a one-signer document rarely bothers with assignees).
465
+ */
466
+ export function isFieldOf(field: Pick<PdfField, 'assignee'>, signer: string | undefined): boolean {
467
+ if (!field.assignee) return true;
468
+ return !!signer && field.assignee === signer;
469
+ }
470
+
471
+ /** What a filled value looks like on the wire, per type. */
472
+ export function hasValue(field: Pick<PdfField, 'type' | 'value'>): boolean {
473
+ const v = field.value;
474
+ if (field.type === 'checkbox') return v === true;
475
+ return typeof v === 'string' ? v !== '' : v !== undefined && v !== null;
476
+ }
477
+
478
+ /** One entry of fill mode's answer. `label`/`font`/`rule` ride along when the field has them. */
479
+ export interface PdfFillEntry {
480
+ id: string;
481
+ value: unknown;
482
+ /** v3 3.1 - the field's name, so the audit trail says what was filled in. */
483
+ label?: string;
484
+ font?: SignFontKey;
485
+ rule?: PdfFieldRule;
486
+ }
487
+
488
+ /**
489
+ * Fill mode's `data.values[id]`: the signer's own fields that hold a value.
490
+ *
491
+ * ⚠ `font` and `rule` travel WITH the value. The plugin stamps the PDF from
492
+ * this array alone — it never sees the screen — so a signature typed in
493
+ * Caveat that arrives as a bare string is stamped in whatever the plugin
494
+ * happens to default to, which is how a signed document comes back in a
495
+ * face the signer never chose.
496
+ */
497
+ export function fillValues(fields: PdfField[], signer: string | undefined): { fields: PdfFillEntry[] } {
498
+ return {
499
+ fields: fields
500
+ .filter((f) => isFieldOf(f, signer) && hasValue(f))
501
+ .map((f) => {
502
+ const e: PdfFillEntry = { id: f.id, value: f.value };
503
+ if (f.label) e.label = f.label;
504
+ if (f.font) e.font = f.font;
505
+ if (f.rule) e.rule = f.rule;
506
+ return e;
507
+ }),
508
+ };
509
+ }
510
+
511
+ /** The fields with the values a previous fill answer (`{fields: [{id, value}]}`) carried. */
512
+ export function applyFillValues(fields: PdfField[], raw: unknown): PdfField[] {
513
+ const given = (raw as { fields?: unknown } | null)?.fields;
514
+ if (!Array.isArray(given)) return fields;
515
+ const byId = new Map<string, { value: unknown; font?: SignFontKey }>();
516
+ for (const e of given as Array<{ id?: unknown; value?: unknown; font?: unknown }>) {
517
+ if (e && typeof e.id === 'string') {
518
+ byId.set(e.id, { value: e.value, font: isSignFontKey(e.font) ? e.font : undefined });
519
+ }
520
+ }
521
+ return fields.map((f) => {
522
+ const seeded = byId.get(f.id);
523
+ if (!seeded) return f;
524
+ return { ...f, value: seeded.value, ...(seeded.font ? { font: seeded.font } : {}) };
525
+ });
526
+ }
527
+
528
+ /** One field's face changed (the signer picked another one from the field's own picker). */
529
+ export function withFont(fields: PdfField[], id: string, font: SignFontKey): PdfField[] {
530
+ return fields.map((f) => (f.id === id ? { ...f, font } : f));
531
+ }
532
+
533
+ /** One field changed value. */
534
+ export function withValue(fields: PdfField[], id: string, value: unknown): PdfField[] {
535
+ return fields.map((f) => {
536
+ if (f.id !== id) return f;
537
+ const next = { ...f };
538
+ if (value === undefined || value === null || value === '' || value === false) delete next.value;
539
+ else next.value = value;
540
+ return next;
541
+ });
542
+ }
543
+
544
+ /** Are all the signer's required fields filled? */
545
+ export function missingRequired(fields: PdfField[], signer: string | undefined): PdfField[] {
546
+ return fields.filter((f) => f.required && isFieldOf(f, signer) && !hasValue(f));
547
+ }
548
+
549
+ /** `sig-1`, `sig-2`, … — never one already on the page. */
550
+ export function newFieldId(type: PdfFieldType, existing: Array<Pick<PdfField, 'id'>>): string {
551
+ const prefix = { signature: 'sig', initials: 'ini', date: 'date', text: 'text', checkbox: 'chk' }[type];
552
+ const ids = new Set(existing.map((f) => f.id));
553
+ for (let n = 1; ; n++) {
554
+ const id = `${prefix}-${n}`;
555
+ if (!ids.has(id)) return id;
556
+ }
557
+ }
558
+
559
+ /** A field placed at `at` (fractions, top-left) with the type's default size, kept inside the page. */
560
+ export function placeField(
561
+ fields: PdfField[],
562
+ type: PdfFieldType,
563
+ page: number,
564
+ at: { x: number; y: number },
565
+ size: { w: number; h: number } = DEFAULT_FIELD_SIZE[type],
566
+ assignee?: string,
567
+ ): PdfField {
568
+ const box = clampFrac({ x: at.x, y: at.y, w: size.w, h: size.h });
569
+ const f: PdfField = { id: newFieldId(type, fields), type, page, ...box };
570
+ if (assignee) f.assignee = assignee;
571
+ return f;
572
+ }
573
+
574
+ /** The same box on another page, as a new field. */
575
+ export function copyToPage(fields: PdfField[], id: string, page: number): PdfField[] {
576
+ const src = fields.find((f) => f.id === id);
577
+ if (!src) return fields;
578
+ const copy: PdfField = { ...src, id: newFieldId(src.type, fields), page: Math.max(1, Math.trunc(page)) };
579
+ delete copy.value;
580
+ return [...fields, copy];
581
+ }
582
+
583
+ /** Today as `YYYY-MM-DD` in the local calendar — the `date` field's auto value. */
584
+ export function todayIso(now: Date = new Date()): string {
585
+ const y = now.getFullYear();
586
+ const m = String(now.getMonth() + 1).padStart(2, '0');
587
+ const d = String(now.getDate()).padStart(2, '0');
588
+ return `${y}-${m}-${d}`;
589
+ }