@brftech/filex-core 0.42.2 → 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 (234) 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-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-CAoTSXTO.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-CdZeK4j4.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 +5227 -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 +1055 -253
  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 +31 -15
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -62
  76. package/src/components/SSHKeysPanel.vue +79 -30
  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 -31
  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 +18 -3
  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 +515 -117
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +536 -152
  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 +2890 -260
  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 +50 -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-BUQWLhwX.js +0 -124
  216. package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
  217. package/dist/CsvViewer-Bg2_aqzc.js +0 -141
  218. package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
  219. package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
  220. package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
  221. package/dist/IpynbViewer-D0GwGpnE.js +0 -184
  222. package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
  223. package/dist/MermaidViewer-DGwAvRZg.js +0 -137
  224. package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
  225. package/dist/PsdViewer-DhGweA0m.js +0 -122
  226. package/dist/PsdViewer-DhGweA0m.js.map +0 -1
  227. package/dist/TiffViewer-D0AurvAh.js +0 -142
  228. package/dist/TiffViewer-D0AurvAh.js.map +0 -1
  229. package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
  230. package/dist/index-BArI-B1M.js +0 -26911
  231. package/dist/index-BArI-B1M.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
  234. package/src/composables/useScrolledX.ts +0 -21
@@ -0,0 +1,509 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PdfFieldEditor — what a box IS, as opposed to where it sits.
4
+ *
5
+ * ⚠⚠ One editor, two screens. The request wizard asks for a box in two
6
+ * steps now — "define the boxes" (this, one card per box, no document in
7
+ * sight) and "place them" (the document, with these boxes handed out one at
8
+ * a time) — and the one-screen `edit` mode still shows it for whichever box
9
+ * is selected. If the controls were written twice they would drift, and the
10
+ * person would meet two different ideas of what naming a box means.
11
+ *
12
+ * Nothing here knows about pages, rectangles or pointers: placing is the
13
+ * parent's job. That split is the whole point of the component.
14
+ */
15
+ import { computed } from 'vue';
16
+ import type { LocaleCode } from '../../../types/ExplorerConfig';
17
+ import { useLocale } from '../../../composables/useLocale';
18
+ import { appTextOr, labelOf } from '../../../lib/pluginLabel';
19
+ import {
20
+ dateChoices,
21
+ dateExample,
22
+ formatFor,
23
+ isDrawnType,
24
+ linesOf,
25
+ normalizeDateLabels,
26
+ normalizeFormats,
27
+ normalizeStampLines,
28
+ splitFormat,
29
+ stampExample,
30
+ type PdfField,
31
+ type PdfSigner,
32
+ type PdfSignatureStyle,
33
+ } from '../../../lib/pdfFields';
34
+ import { PDF_RULE_KINDS, type PdfFieldRule, type PdfRuleKind } from '../../../lib/pdfFieldRules';
35
+ import { SIGN_FONTS, type SignFontKey } from '../../../lib/signFonts';
36
+ import ChoiceButtons, { type ChoiceOption } from '../../ChoiceButtons.vue';
37
+
38
+ const props = defineProps<{
39
+ field: PdfField;
40
+ signers: PdfSigner[];
41
+ /**
42
+ * The date layouts this surface offers (`[{id, label, example}]`, the
43
+ * node's own `formats` prop). Empty or absent: no layout control is
44
+ * drawn at all, because there is nothing to choose between.
45
+ */
46
+ formats?: unknown;
47
+ /**
48
+ * The date control's three captions as the PLUGIN words them
49
+ * (`date_labels`): the two questions and the word over the example.
50
+ * Absent: this package's own catalogue stands in.
51
+ */
52
+ dateLabels?: unknown;
53
+ locale: LocaleCode;
54
+ disabled?: boolean;
55
+ /** `surface-pdf` on the one-screen editor; the define cards pass their own. */
56
+ testidPrefix?: string;
57
+ /**
58
+ * The lines a plugin can print under a signature (the node's
59
+ * `stamp_lines`, `lib/pdfFields.normalizeStampLines`). Absent or empty:
60
+ * the plugin offers no choice, and no control is drawn.
61
+ */
62
+ stampLines?: unknown;
63
+ }>();
64
+
65
+ const emit = defineEmits<{
66
+ /** A changed copy of the field. */
67
+ (e: 'patch', f: PdfField): void;
68
+ (e: 'remove'): void;
69
+ }>();
70
+
71
+ const { t } = useLocale(() => props.locale);
72
+ const prefix = computed(() => props.testidPrefix || 'surface-pdf');
73
+
74
+ function typeLabel(type: PdfField['type']): string {
75
+ return t(`plugin.pdf.type_${type}`);
76
+ }
77
+
78
+ /** Change the field through a copy — the parent owns the list. */
79
+ function patch(fn: (f: PdfField) => void): void {
80
+ const next = { ...props.field };
81
+ fn(next);
82
+ emit('patch', next);
83
+ }
84
+
85
+ /**
86
+ * The box's NAME, exactly as it was typed — spaces and all.
87
+ *
88
+ * ⚠⚠ NEVER trim here. `:value` is bound to the field, so a trim on every
89
+ * keystroke writes the trimmed name straight back into the input: the space
90
+ * typed after a word is taken off again before the next letter arrives, and
91
+ * a two-word name becomes impossible to type (the owner, 2026-09-23:
92
+ * "isimde boşluk bırakamıyorum hiç izin vermiyor"). A name is free text in
93
+ * any script; the ASCII IDENTITY the PDF names the field by is derived from
94
+ * it by the plugin (`fields.Slug`) and shown to nobody. The record is
95
+ * trimmed once, where the record is written.
96
+ */
97
+ function setLabel(v: string): void {
98
+ patch((f) => {
99
+ if (v) f.label = v;
100
+ else delete f.label;
101
+ });
102
+ }
103
+
104
+ function assign(assignee: string): void {
105
+ patch((f) => {
106
+ if (assignee) f.assignee = assignee;
107
+ else delete f.assignee;
108
+ });
109
+ }
110
+
111
+ function setRequired(on: boolean): void {
112
+ patch((f) => {
113
+ if (on) f.required = true;
114
+ else delete f.required;
115
+ });
116
+ }
117
+
118
+ function setRuleKind(kind: string): void {
119
+ patch((f) => {
120
+ const k = (PDF_RULE_KINDS as readonly string[]).includes(kind) ? (kind as PdfRuleKind) : 'any';
121
+ const rule: PdfFieldRule = { ...(f.rule ?? { kind: 'any' }), kind: k };
122
+ if (k === 'any' && !rule.min && !rule.max) delete f.rule;
123
+ else f.rule = rule;
124
+ });
125
+ }
126
+
127
+ function setRuleBound(which: 'min' | 'max', raw: unknown): void {
128
+ const n = Math.trunc(Number(String(raw ?? '').trim()));
129
+ patch((f) => {
130
+ const rule: PdfFieldRule = { ...(f.rule ?? { kind: 'any' }) };
131
+ if (Number.isFinite(n) && n > 0) rule[which] = n;
132
+ else delete rule[which];
133
+ if (rule.kind === 'any' && !rule.min && !rule.max) delete f.rule;
134
+ else f.rule = rule;
135
+ });
136
+ }
137
+
138
+ function setFormat(id: string): void {
139
+ patch((f) => {
140
+ if (id) f.format = id;
141
+ else delete f.format;
142
+ });
143
+ }
144
+
145
+ function setFont(key: SignFontKey): void {
146
+ patch((f) => {
147
+ f.font = key;
148
+ });
149
+ }
150
+
151
+ /** A signature or initials box: drawn by hand, or a name typed in a face. */
152
+ const drawn = computed(() => isDrawnType(props.field.type));
153
+
154
+ /** How this signature box is given. Absent means drawn — what a signature is. */
155
+ const style = computed<PdfSignatureStyle>(() => props.field.style ?? 'drawn');
156
+
157
+ function setStyle(v: string): void {
158
+ patch((f) => {
159
+ if (v === 'typed') f.style = 'typed';
160
+ else delete f.style;
161
+ });
162
+ }
163
+
164
+ const styleOptions = computed<ChoiceOption[]>(() => [
165
+ { value: 'drawn', label: t('plugin.pdf.style_drawn') },
166
+ { value: 'typed', label: t('plugin.pdf.style_typed') },
167
+ ]);
168
+
169
+ /**
170
+ * Is the face a question here at all?
171
+ *
172
+ * ⚠ The owner, 2026-09-21: "text seçersem font çıkar, text seçmezsem font
173
+ * seçimi çıkmaz". A drawn signature is somebody's hand; a face offered for it
174
+ * was a control that changed nothing the signer would ever see. Text and
175
+ * date boxes are words, so they always have one; a checkbox never does.
176
+ */
177
+ const asksFont = computed(() => {
178
+ if (props.field.type === 'checkbox') return false;
179
+ if (drawn.value) return style.value === 'typed';
180
+ return true;
181
+ });
182
+
183
+ /* ── what is printed under a signature ────────────────────────────── */
184
+
185
+ const lineCatalogue = computed(() => normalizeStampLines(props.stampLines));
186
+ /** The ids this box prints, in the catalogue's order (its default set when it has none). */
187
+ const pickedLines = computed(() => linesOf(props.field, lineCatalogue.value));
188
+ const lineOptions = computed<ChoiceOption[]>(() =>
189
+ lineCatalogue.value.map((l) => ({ value: l.id, label: labelOf(l.label, props.locale) || l.id })),
190
+ );
191
+
192
+ function setLines(v: string | string[]): void {
193
+ const ids = (Array.isArray(v) ? v : [v]).filter(Boolean);
194
+ patch((f) => {
195
+ // ⚠ An EMPTY list is kept, and means "nothing under the signature". It is
196
+ // not the same as absent (the plugin's default set), and folding the two
197
+ // together would put back the very lines the person just took off.
198
+ f.lines = ids;
199
+ });
200
+ }
201
+
202
+ /**
203
+ * The words the plugin will print, one per line, for this box's signer —
204
+ * the plugin's own example for each chosen line, so the card previews its
205
+ * wording rather than this package guessing at it.
206
+ */
207
+ const linePreview = computed(() =>
208
+ pickedLines.value
209
+ .map((id) => lineCatalogue.value.find((l) => l.id === id))
210
+ .filter((l): l is NonNullable<typeof l> => !!l)
211
+ .map((l) => labelOf(stampExample(l, props.field.assignee), props.locale) || labelOf(l.label, props.locale)),
212
+ );
213
+
214
+ /**
215
+ * Who fills this box, or who signs it.
216
+ *
217
+ * ⚠ The owner, 2026-09-21: "tarih, metin, onay kutusu gibi şeylerde
218
+ * 'imzacı' yazısı yerine 'dolduran' yazısı koyalım". A date or a tick is
219
+ * FILLED; calling its owner "the signer" asked the requester a question
220
+ * about the wrong act.
221
+ */
222
+ const assigneeLabel = computed(() => t(drawn.value ? 'plugin.pdf.assignee' : 'plugin.pdf.filled_by'));
223
+
224
+ /**
225
+ * "Anyone"'s value on the buttons. ⚠ Not `''`: the choice control reads an
226
+ * empty value as "nothing picked", so a box that belonged to anyone showed
227
+ * NO button pressed — measured on the define step, 2026-09-21, three
228
+ * unpressed buttons under every new box of a two-signer request. The
229
+ * sentinel lives only between this control and `assign`; the field still
230
+ * carries no `assignee` at all.
231
+ */
232
+ const ANYONE = '*';
233
+
234
+ /** Who the box belongs to, as buttons — "Anyone" first. */
235
+ const assigneeOptions = computed<ChoiceOption[]>(() => [
236
+ { value: ANYONE, label: t('plugin.pdf.unassigned') },
237
+ ...props.signers.map((sg) => ({ value: sg.id, label: labelOf(sg.label, props.locale) || sg.id })),
238
+ ]);
239
+
240
+ /** What a text box accepts, as buttons. */
241
+ const ruleOptions = computed<ChoiceOption[]>(() =>
242
+ PDF_RULE_KINDS.map((k) => ({ value: k, label: t(`plugin.pdf.rule_kind_${k}`) })),
243
+ );
244
+
245
+ /**
246
+ * How a date box is written, as buttons — each one with an example under
247
+ * it, because `MM/DD/YYYY` is a puzzle and `12/31/2000` is an answer.
248
+ */
249
+ const dateFormats = computed(() => normalizeFormats(props.formats));
250
+ const formatOptions = computed<ChoiceOption[]>(() =>
251
+ dateFormats.value.map((f) => ({ value: f.id, label: labelOf(f.label, props.locale) || f.id, help: f.example })),
252
+ );
253
+
254
+ /**
255
+ * Which layout is shown as chosen. A box that has not been given one yet is
256
+ * drawn on the FIRST the surface offers: a catalogue's first entry is what
257
+ * the plugin falls back to, so showing nothing selected would be telling
258
+ * the person their date has no layout when it has one.
259
+ */
260
+ const pickedFormat = computed(() => props.field.format ?? dateFormats.value[0]?.id ?? '');
261
+
262
+ /* ── a date's layout: TWO questions ───────────────────────────────────
263
+ *
264
+ * ⚠⚠ The owner, 2026-09-23: "tarih biçimi ve ayraçlarını ayrı ayrı
265
+ * seçebilir olalım … Tarih biçimi iki farklı seçimle tamamlandığında örnek
266
+ * halini alta gösterelim". Twenty layouts in one row is a wall to hunt
267
+ * through; the same twenty are five arrangements and four separators, and
268
+ * nobody has to read a pattern to answer either one. The BOX still stores
269
+ * the whole pattern, so a request made before this keeps its layout
270
+ * exactly and no signed document is re-written under anybody.
271
+ */
272
+ const dateParts = computed(() => dateChoices(dateFormats.value));
273
+ const dateCaptions = computed(() => normalizeDateLabels(props.dateLabels));
274
+ /* An app may caption these three itself; filex has its own words for all of
275
+ them. ⚠ `appTextOr`, not `labelOf(…) || t(…)`: the app's caption for THIS
276
+ reader wins, then filex's own, and the app's other languages last — or a
277
+ reader in Arabic gets the app's English in the middle of an Arabic screen
278
+ while the language pack holds the very same caption (v0.43.0). */
279
+ function caption(k: 'order' | 'separator' | 'example', fallback: string): string {
280
+ return appTextOr(dateCaptions.value[k], props.locale, () => t(fallback));
281
+ }
282
+
283
+ const pickedPair = computed(() => splitFormat(dateFormats.value, pickedFormat.value));
284
+ const pickedOrder = computed(() => pickedPair.value?.order ?? dateParts.value?.orders[0]?.id ?? '');
285
+ const pickedSeparator = computed(() => pickedPair.value?.separator ?? dateParts.value?.separators[0]?.id ?? '');
286
+
287
+ const orderOptions = computed<ChoiceOption[]>(() =>
288
+ (dateParts.value?.orders ?? []).map((o) => ({ value: o.id, label: labelOf(o.label, props.locale) || o.id })),
289
+ );
290
+ const separatorOptions = computed<ChoiceOption[]>(() =>
291
+ (dateParts.value?.separators ?? []).map((s) => ({ value: s.id, label: labelOf(s.label, props.locale) || s.id })),
292
+ );
293
+
294
+ /** Today, written the way the two answers say — the whole point of asking. */
295
+ const dateShown = computed(() => dateExample(pickedOrder.value, pickedSeparator.value, new Date()));
296
+
297
+ function setOrder(v: string): void {
298
+ setFormat(formatFor(dateFormats.value, v, pickedSeparator.value));
299
+ }
300
+ function setSeparator(v: string): void {
301
+ setFormat(formatFor(dateFormats.value, pickedOrder.value, v));
302
+ }
303
+ </script>
304
+
305
+ <template>
306
+ <div class="fe-spdf__props" :data-testid="`${prefix}-editor`">
307
+ <span class="fe-spdf__propname">{{ typeLabel(field.type) }}</span>
308
+
309
+ <!-- v3 §3.1 — the box's own name. Offered with the type's name already
310
+ in it, so a page of boxes is never a page of "Signature". -->
311
+ <label class="fe-spdf__prop">
312
+ <span>{{ t('plugin.pdf.label') }}</span>
313
+ <input
314
+ class="fe-cfield__input fe-spdf__name"
315
+ type="text"
316
+ :value="field.label ?? ''"
317
+ :placeholder="typeLabel(field.type)"
318
+ :disabled="disabled"
319
+ :data-testid="`${prefix}-label`"
320
+ @input="setLabel(($event.target as HTMLInputElement).value)"
321
+ />
322
+ </label>
323
+
324
+ <div v-if="signers.length" class="fe-spdf__prop">
325
+ <span :id="`${prefix}-${field.id}-assignee-lbl`" :data-testid="`${prefix}-assignee-label`">{{ assigneeLabel }}</span>
326
+ <ChoiceButtons
327
+ :options="assigneeOptions"
328
+ :model-value="field.assignee || ANYONE"
329
+ :disabled="disabled"
330
+ :aria-labelledby="`${prefix}-${field.id}-assignee-lbl`"
331
+ :testid-prefix="`${prefix}-assignee`"
332
+ @update:model-value="(v: string | string[]) => assign(String(v) === ANYONE ? '' : String(v))"
333
+ />
334
+ </div>
335
+
336
+ <!-- A signature is drawn, or it is a name typed in a face — the
337
+ requester decides which, and only the typed one has a face to pick. -->
338
+ <div v-if="drawn" class="fe-spdf__prop" :data-testid="`${prefix}-style`">
339
+ <span :id="`${prefix}-${field.id}-style-lbl`">{{ t('plugin.pdf.style') }}</span>
340
+ <ChoiceButtons
341
+ :options="styleOptions"
342
+ :model-value="style"
343
+ :disabled="disabled"
344
+ :aria-labelledby="`${prefix}-${field.id}-style-lbl`"
345
+ :testid-prefix="`${prefix}-style`"
346
+ @update:model-value="(v: string | string[]) => setStyle(String(v))"
347
+ />
348
+ </div>
349
+
350
+ <label class="fe-spdf__prop fe-spdf__prop--check">
351
+ <input
352
+ type="checkbox"
353
+ :checked="field.required === true"
354
+ :disabled="disabled"
355
+ :data-testid="`${prefix}-required`"
356
+ @change="setRequired(($event.target as HTMLInputElement).checked)"
357
+ />
358
+ <span>{{ t('plugin.pdf.required') }}</span>
359
+ </label>
360
+
361
+ <!-- A text box says what it will take. ⚠ Only `text`: a date field
362
+ already IS a date, a checkbox is a boolean, and a signature is a
363
+ picture — a rule on any of them would be a control that does
364
+ nothing. -->
365
+ <template v-if="field.type === 'text'">
366
+ <div class="fe-spdf__prop">
367
+ <span :id="`${prefix}-${field.id}-rule-lbl`">{{ t('plugin.pdf.rule') }}</span>
368
+ <ChoiceButtons
369
+ :options="ruleOptions"
370
+ :model-value="field.rule?.kind ?? 'any'"
371
+ :disabled="disabled"
372
+ :aria-labelledby="`${prefix}-${field.id}-rule-lbl`"
373
+ :testid-prefix="`${prefix}-rule`"
374
+ @update:model-value="(v: string | string[]) => setRuleKind(String(v))"
375
+ />
376
+ </div>
377
+ <label class="fe-spdf__prop">
378
+ <span>{{ t('plugin.pdf.rule_min_label') }}</span>
379
+ <input
380
+ class="fe-cfield__input fe-spdf__num"
381
+ type="number"
382
+ min="0"
383
+ :value="field.rule?.min ?? ''"
384
+ :disabled="disabled"
385
+ :data-testid="`${prefix}-rule-min`"
386
+ @input="setRuleBound('min', ($event.target as HTMLInputElement).value)"
387
+ />
388
+ </label>
389
+ <label class="fe-spdf__prop">
390
+ <span>{{ t('plugin.pdf.rule_max_label') }}</span>
391
+ <input
392
+ class="fe-cfield__input fe-spdf__num"
393
+ type="number"
394
+ min="0"
395
+ :value="field.rule?.max ?? ''"
396
+ :disabled="disabled"
397
+ :data-testid="`${prefix}-rule-max`"
398
+ @input="setRuleBound('max', ($event.target as HTMLInputElement).value)"
399
+ />
400
+ </label>
401
+ </template>
402
+
403
+ <!-- ⚠ A date box's layout is its `rule`: it is the one thing about the
404
+ box that is still an open question once it has a name, and leaving
405
+ it unasked meant the plugin's own default was the only answer
406
+ anybody could give. Buttons, never a dropdown (v3 §2). -->
407
+ <template v-if="field.type === 'date' && dateParts">
408
+ <div class="fe-spdf__prop">
409
+ <span :id="`${prefix}-${field.id}-order-lbl`">{{ caption('order', 'plugin.pdf.date_order') }}</span>
410
+ <ChoiceButtons
411
+ :options="orderOptions"
412
+ :model-value="pickedOrder"
413
+ :disabled="disabled"
414
+ :aria-labelledby="`${prefix}-${field.id}-order-lbl`"
415
+ :testid-prefix="`${prefix}-order`"
416
+ @update:model-value="(v: string | string[]) => setOrder(String(v))"
417
+ />
418
+ </div>
419
+ <div class="fe-spdf__prop">
420
+ <span :id="`${prefix}-${field.id}-sep-lbl`">{{ caption('separator', 'plugin.pdf.date_separator') }}</span>
421
+ <ChoiceButtons
422
+ :options="separatorOptions"
423
+ :model-value="pickedSeparator"
424
+ :disabled="disabled"
425
+ :aria-labelledby="`${prefix}-${field.id}-sep-lbl`"
426
+ :testid-prefix="`${prefix}-sep`"
427
+ @update:model-value="(v: string | string[]) => setSeparator(String(v))"
428
+ />
429
+ </div>
430
+ <p class="fe-spdf__dateex" :data-testid="`${prefix}-date-example`">
431
+ <span class="fe-spdf__dateex-title">{{ caption('example', 'plugin.pdf.date_example') }}</span>
432
+ <bdi class="fe-spdf__dateex-value">{{ dateShown }}</bdi>
433
+ </p>
434
+ </template>
435
+ <div v-else-if="field.type === 'date' && formatOptions.length" class="fe-spdf__prop">
436
+ <span :id="`${prefix}-${field.id}-format-lbl`">{{ t('plugin.pdf.format') }}</span>
437
+ <ChoiceButtons
438
+ :options="formatOptions"
439
+ :model-value="pickedFormat"
440
+ :disabled="disabled"
441
+ :aria-labelledby="`${prefix}-${field.id}-format-lbl`"
442
+ :testid-prefix="`${prefix}-format`"
443
+ @update:model-value="(v: string | string[]) => setFormat(String(v))"
444
+ />
445
+ </div>
446
+
447
+ <!-- The face the stamper draws this field in. Each button is set in the
448
+ face it offers: a font's NAME tells nobody what it looks like. -->
449
+ <div
450
+ v-if="asksFont"
451
+ class="fe-signfonts fe-spdf__prop"
452
+ role="group"
453
+ :aria-label="t('plugin.pdf.font')"
454
+ :data-testid="`${prefix}-fonts`"
455
+ >
456
+ <button
457
+ v-for="f in SIGN_FONTS"
458
+ :key="f.key"
459
+ type="button"
460
+ class="fe-signfonts__btn"
461
+ :class="[f.className, { 'is-on': (field.font ?? 'caveat') === f.key }]"
462
+ :title="f.name"
463
+ :disabled="disabled"
464
+ :aria-pressed="(field.font ?? 'caveat') === f.key ? 'true' : 'false'"
465
+ :data-testid="`${prefix}-font-${f.key}`"
466
+ @click="setFont(f.key)"
467
+ >
468
+ Aa
469
+ </button>
470
+ </div>
471
+
472
+ <!-- What is printed under the signature: chosen per box, from the
473
+ lines the plugin can really fill in (its signing record), with the
474
+ plugin's own wording previewed underneath. ⚠ Nothing typed here
475
+ reaches the paper — the requester picks WHICH facts, never their
476
+ values. -->
477
+ <div v-if="drawn && lineOptions.length" class="fe-spdf__prop fe-spdf__lines" :data-testid="`${prefix}-lines`">
478
+ <span :id="`${prefix}-${field.id}-lines-lbl`">{{ t('plugin.pdf.lines') }}</span>
479
+ <ChoiceButtons
480
+ :options="lineOptions"
481
+ :model-value="pickedLines"
482
+ multi
483
+ :disabled="disabled"
484
+ :aria-labelledby="`${prefix}-${field.id}-lines-lbl`"
485
+ :testid-prefix="`${prefix}-line`"
486
+ @update:model-value="setLines"
487
+ />
488
+ <div class="fe-spdf__linepreview" :data-testid="`${prefix}-lines-preview`">
489
+ <span class="fe-spdf__linepreview-title">{{ t('plugin.pdf.lines_preview') }}</span>
490
+ <template v-if="linePreview.length">
491
+ <span v-for="(ln, i) in linePreview" :key="i" class="fe-spdf__linepreview-line">{{ ln }}</span>
492
+ </template>
493
+ <span v-else class="fe-spdf__linepreview-line is-none">{{ t('plugin.pdf.lines_none') }}</span>
494
+ </div>
495
+ </div>
496
+
497
+ <slot name="extra"></slot>
498
+
499
+ <button
500
+ type="button"
501
+ class="fe-btn fe-btn--sm fe-btn--danger"
502
+ :disabled="disabled"
503
+ :data-testid="`${prefix}-delete`"
504
+ @click="emit('remove')"
505
+ >
506
+ {{ t('plugin.pdf.delete') }}
507
+ </button>
508
+ </div>
509
+ </template>
@@ -0,0 +1,94 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SurfaceFileChooser — a plugin surface's `file-chooser` node. "Choose…"
4
+ * opens the explorer's OWN destination picker (`modals/DestinationPickerModal`,
5
+ * the same dialog Move to… / Copy to… use) in `choose` mode; `kind: "file"`
6
+ * asks it to list files and answer with the one ticked. The answer is an
7
+ * adapter-qualified path, which is what goes into `data.values[id]`.
8
+ */
9
+ import { computed, ref } from 'vue';
10
+ import type { LocaleCode, ThemeMode } from '../../../types/ExplorerConfig';
11
+ import type { FileApi } from '../../../composables/useFileApi';
12
+ import { useLocale } from '../../../composables/useLocale';
13
+ import { labelOfWire, parentOfWire } from '../../../lib/destinationTree';
14
+ import DestinationPickerModal from '../../../modals/DestinationPickerModal.vue';
15
+
16
+ const props = defineProps<{
17
+ id: string;
18
+ kind?: string;
19
+ modelValue: string;
20
+ locale: LocaleCode;
21
+ theme?: ThemeMode;
22
+ /** Absent: the button is drawn disabled — there is nothing to browse with. */
23
+ api?: Pick<FileApi, 'index'>;
24
+ /** Storage names the picker may span. */
25
+ storages?: string[];
26
+ /** Where the picker opens — the folder the view was opened in. */
27
+ startAt?: string;
28
+ disabled?: boolean;
29
+ invalid?: boolean;
30
+ }>();
31
+
32
+ const emit = defineEmits<{
33
+ (e: 'update:modelValue', v: string): void;
34
+ }>();
35
+
36
+ const { t } = useLocale(() => props.locale);
37
+
38
+ const open = ref(false);
39
+ const pick = computed<'file' | 'dir'>(() => (props.kind === 'dir' ? 'dir' : 'file'));
40
+ const shown = computed(() => (props.modelValue ? labelOfWire(props.modelValue, '') || props.modelValue : ''));
41
+ /** Where the picker opens: the chosen entry's folder, else where the view was opened. */
42
+ const openAt = computed(() => {
43
+ if (!props.modelValue) return props.startAt;
44
+ if (pick.value === 'dir') return props.modelValue;
45
+ return parentOfWire(props.modelValue, false) ?? props.modelValue;
46
+ });
47
+
48
+ function onPick(path: string) {
49
+ open.value = false;
50
+ emit('update:modelValue', path);
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <div class="fe-sfile" :class="{ 'is-invalid': invalid }" data-testid="surface-file-chooser">
56
+ <span
57
+ class="fe-sfile__value"
58
+ :class="{ 'fe-sfile__value--empty': !modelValue }"
59
+ :title="modelValue || undefined"
60
+ data-testid="surface-file-chooser-value"
61
+ >{{ shown || t('plugin.file.none') }}</span>
62
+ <button
63
+ type="button"
64
+ class="fe-btn fe-btn--sm"
65
+ :disabled="disabled || !api"
66
+ data-testid="surface-file-chooser-open"
67
+ @click="open = true"
68
+ >
69
+ {{ t('plugin.file.choose') }}
70
+ </button>
71
+ <button
72
+ v-if="modelValue"
73
+ type="button"
74
+ class="fe-btn fe-btn--sm"
75
+ :disabled="disabled"
76
+ data-testid="surface-file-chooser-clear"
77
+ @click="emit('update:modelValue', '')"
78
+ >
79
+ {{ t('plugin.file.clear') }}
80
+ </button>
81
+ <DestinationPickerModal
82
+ v-if="api"
83
+ :open="open"
84
+ :api="api"
85
+ :locale="locale"
86
+ mode="choose"
87
+ :pick="pick"
88
+ :storages="storages"
89
+ :start-at="openAt"
90
+ @close="open = false"
91
+ @pick="onPick"
92
+ />
93
+ </div>
94
+ </template>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SurfaceForm — a plugin surface's `form` node.
4
+ *
5
+ * The fields are drawn by `StorageFields`, the ONE field renderer this
6
+ * package has (a storage driver's config form, an app's settings form and
7
+ * now a plugin screen all declare `Field[]` in the same shape). This file
8
+ * only translates: a plugin's `label`/`help` may be Text, and the values are
9
+ * flat in the surface's value map, keyed by field key.
10
+ *
11
+ * ⚠⚠ v3 — it also decides WHICH fields exist right now. A field may carry
12
+ * `show_when` / `required_when` (`lib/surfaceConditions`), so "the name of
13
+ * the new file" is simply not on the step while the output is a new version
14
+ * of the old one. A form may not present a contradiction, and the renderer
15
+ * is what makes that true rather than each plugin.
16
+ *
17
+ * ⚠ Hiding is only half of it: the hidden field's VALUE must not travel
18
+ * either, or the plugin receives an answer to a question it stopped asking.
19
+ * That half is `usePluginSurface`'s, because it owns what goes on the wire —
20
+ * this component could only blank the value, and `""` is an answer too.
21
+ */
22
+ import { computed } from 'vue';
23
+ import type { LocaleCode } from '../../../types/ExplorerConfig';
24
+ import type { PluginField, PluginText } from '../../../types/Plugins';
25
+ import { labelOf } from '../../../lib/pluginLabel';
26
+ import { storageFieldOf } from '../../../lib/surfaceValues';
27
+ import { fieldRequired, visibleFields } from '../../../lib/surfaceConditions';
28
+ import StorageFields from '../../StorageFields.vue';
29
+
30
+ const props = defineProps<{
31
+ fields: PluginField[];
32
+ /** The surface's whole value map — field values are read and written flat. */
33
+ values: Record<string, unknown>;
34
+ locale: LocaleCode;
35
+ /** `surface.errors`: an entry under a field key marks it invalid, with those words. */
36
+ errors?: Record<string, PluginText>;
37
+ disabled?: boolean;
38
+ /** Field keys the host is pointing at as unanswered (a blocked submit). */
39
+ invalid?: string[];
40
+ }>();
41
+
42
+ const emit = defineEmits<{
43
+ (e: 'update:values', v: Record<string, unknown>): void;
44
+ }>();
45
+
46
+ /** Only the fields whose `show_when` holds, conditions cascaded. */
47
+ const shown = computed(() => visibleFields(props.fields, props.values));
48
+
49
+ const mapped = computed(() =>
50
+ shown.value.map((f) => {
51
+ const sf = storageFieldOf(f, props.locale);
52
+ // `required_when` is the same promise as `required`, made conditionally:
53
+ // once the condition holds, the star is on the label and the submit is
54
+ // blocked — so nobody is refused by the server for a rule the screen
55
+ // never showed them.
56
+ return { ...sf, required: fieldRequired(f, props.values) };
57
+ }),
58
+ );
59
+
60
+ const fieldErrors = computed<Record<string, string>>(() => {
61
+ const out: Record<string, string> = {};
62
+ for (const f of mapped.value) {
63
+ const msg = labelOf(props.errors?.[f.key], props.locale);
64
+ if (msg) out[f.key] = msg;
65
+ }
66
+ return out;
67
+ });
68
+ </script>
69
+
70
+ <template>
71
+ <div class="fe-surface__form" data-testid="surface-form">
72
+ <StorageFields
73
+ :fields="mapped"
74
+ :model-value="values"
75
+ :locale="locale"
76
+ :errors="fieldErrors"
77
+ :invalid="invalid"
78
+ :disabled="disabled"
79
+ @update:model-value="(v: Record<string, unknown>) => emit('update:values', v)"
80
+ />
81
+ </div>
82
+ </template>