@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,339 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PluginPageView — an app plugin's `page` view, drawn as a whole page.
4
+ *
5
+ * ⚠⚠ The SAME conversation a `modal` view has. `usePluginSurface` is the
6
+ * shared half (the surface, the values, the debounced `change`, the footer
7
+ * press, the `{op}` answer) and this file is only the frame: a title bar of
8
+ * its own, one column at reading width, the footer buttons at the bottom,
9
+ * and no dialog scroll inside a dialog. Writing a second conversation here
10
+ * is how a `page` wizard and a `modal` wizard start disagreeing about what
11
+ * `submit` sends.
12
+ *
13
+ * Its own two states, which a modal has no use for:
14
+ * • QUEUED — a `page` action never goes through `…/run`, so the job is
15
+ * born from this screen's submit. A tab that just closed itself would
16
+ * leave the person with no idea whether anything happened; instead the
17
+ * page says the job is queued and offers the way back to the operations
18
+ * tray that is now tracking it.
19
+ * • DONE — the plugin ended the conversation (`done`), with nothing queued.
20
+ *
21
+ * ⚠ It is in the package, not in the admin SPA: fm.example.com, the desktop shell
22
+ * and the embeds all reach `page` views through the same explorer, so a copy
23
+ * per host would be a copy per host to keep in step.
24
+ */
25
+ import { computed, onMounted, ref, watch } from 'vue';
26
+ import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
27
+ import type { FileApi } from '../../composables/useFileApi';
28
+ import { useLocale } from '../../composables/useLocale';
29
+ import { usePluginSurface } from '../../composables/usePluginSurface';
30
+ import { labelOf } from '../../lib/pluginLabel';
31
+ import SurfaceConversation from './SurfaceConversation.vue';
32
+ import SurfaceFooterButtons from './SurfaceFooterButtons.vue';
33
+ import SurfaceSections from './SurfaceSections.vue';
34
+ import { walkNodes } from '../../lib/surfaceValues';
35
+ import { openTargetFor } from '../../lib/surfaceOpen';
36
+
37
+ const props = defineProps<{
38
+ locale: LocaleCode;
39
+ theme?: ThemeMode;
40
+ api: FileApi;
41
+ plugin: string;
42
+ view: string;
43
+ /** Adapter-qualified path the page was opened on; echoed on every event. */
44
+ path?: string;
45
+ /** Storage names a file-chooser may span; where its picker opens. */
46
+ storages?: string[];
47
+ startAt?: string;
48
+ /** Where "back to the files" goes. Absent: the button is not drawn. */
49
+ backHref?: string;
50
+ /** Where the operations tray lives, for the queued state. */
51
+ opsHref?: string;
52
+ /**
53
+ * The prefix the SPA is served from (`/admin/`, `/drive/`) — what a
54
+ * `Surface.open` address is built against (v3 §3.0). Absent: the request
55
+ * is dropped, because a wrong address is worse than none.
56
+ */
57
+ mountBase?: string;
58
+ /**
59
+ * The CHROME around the conversation; the conversation itself is the same
60
+ * either way.
61
+ *
62
+ * `page` (the default) is a browser tab of its own: a title bar, one column
63
+ * at reading width, the viewport's height, a footer stuck to its bottom.
64
+ * `embedded` is the same screen drawn INSIDE a host's page — the admin
65
+ * panel's Apps section, an embed that already has a header — and brings
66
+ * none of those three, because the host has already said where the screen
67
+ * begins, how wide it is and what it is called.
68
+ *
69
+ * ⚠ A step carrying a document keeps the INLINE layout when embedded. The
70
+ * `page` rule fits a PDF to the VIEWPORT (v3 §3.2); inside a host's page
71
+ * the viewport is not this frame's box, so the same rule would fit the
72
+ * document to a height the frame does not have.
73
+ */
74
+ frame?: 'page' | 'embedded';
75
+ /**
76
+ * The section of a home page to open (`surface.sections`), as the host's
77
+ * address says. ⚠ The HOST owns it: choosing a section emits `section`
78
+ * and the host puts it in its URL, which comes back here — so Back walks
79
+ * the sections, and a link names one.
80
+ */
81
+ section?: string;
82
+ }>();
83
+
84
+ const emit = defineEmits<{
85
+ /** The server enqueued a job for this page; the raw ops row. */
86
+ (e: 'op', op: Record<string, unknown>): void;
87
+ /**
88
+ * The person chose another section of a home page — or, with
89
+ * `{replace: true}`, a page opened with NO section learned which one the
90
+ * plugin shows first: the address should name it without adding a step
91
+ * to history.
92
+ */
93
+ (e: 'section', id: string, how?: { replace?: boolean }): void;
94
+ }>();
95
+
96
+ const { t } = useLocale(() => props.locale);
97
+
98
+ /** Drawn inside a host's page rather than in a tab of its own. */
99
+ const embedded = computed(() => props.frame === 'embedded');
100
+
101
+ type PageState = 'loading' | 'surface' | 'queued' | 'done' | 'error';
102
+ const state = ref<PageState>('loading');
103
+ const loadError = ref('');
104
+ const toast = ref('');
105
+
106
+ const conv = usePluginSurface(
107
+ {
108
+ api: props.api,
109
+ plugin: props.plugin,
110
+ view: props.view,
111
+ path: () => props.path,
112
+ locale: () => props.locale,
113
+ errorText: () => t('plugin.view.error'),
114
+ },
115
+ {
116
+ // ⚠ `onOp` fires BEFORE `onDone` (usePluginSurface posts them in that
117
+ // order), so the queued state has to be set after both — otherwise
118
+ // `onDone` would overwrite it with "finished" and the person would never
119
+ // see that a job is running.
120
+ /**
121
+ * v3 §3.0 — "go to this file, and start that screen on it".
122
+ *
123
+ * ⚠ A real navigation, not a tab: this IS a whole page, and the person
124
+ * asked to be taken to a document. The address is the explorer deep link
125
+ * a notification already uses, so the placement question is answered
126
+ * where it is answerable — the explorer knows which of the plugin's
127
+ * views is a `page` and opens that one in its own tab.
128
+ */
129
+ onOpen: (req) => {
130
+ const target = openTargetFor(req, { plugin: props.plugin, base: props.mountBase });
131
+ if (!target.href) return;
132
+ try {
133
+ if (target.newTab) {
134
+ const win = window.open(target.href, '_blank');
135
+ if (win) win.opener = null;
136
+ else window.location.assign(target.href);
137
+ } else {
138
+ window.location.assign(target.href);
139
+ }
140
+ } catch {
141
+ /* a blocked navigation leaves the screen as it is */
142
+ }
143
+ },
144
+ onOp: (op) => {
145
+ emit('op', op);
146
+ state.value = 'queued';
147
+ },
148
+ onDone: () => {
149
+ if (state.value !== 'queued') state.value = 'done';
150
+ },
151
+ onToast: (m) => (toast.value = m),
152
+ },
153
+ );
154
+
155
+ const current = conv.current;
156
+ const title = computed(() => labelOf(current.value?.title, props.locale) || props.view);
157
+
158
+ /**
159
+ * The section being shown. It starts from the host's address and follows
160
+ * it; a choice made here moves it at once (so the menu answers the click
161
+ * even in a host that does not keep an address), then tells the host.
162
+ */
163
+ const wanted = ref(props.section ?? '');
164
+ const sections = computed(() => current.value?.sections ?? []);
165
+ /** Which entry is lit: what the plugin says it drew, else what was asked. */
166
+ const activeSection = computed(() => current.value?.section || wanted.value);
167
+
168
+ function chooseSection(id: string): void {
169
+ if (!id || id === activeSection.value) return;
170
+ wanted.value = id;
171
+ emit('section', id);
172
+ void load();
173
+ }
174
+
175
+ watch(
176
+ () => props.section,
177
+ (s) => {
178
+ const next = s ?? '';
179
+ // The host echoing the choice made above is not a new request.
180
+ if (next === wanted.value) return;
181
+ wanted.value = next;
182
+ void load();
183
+ },
184
+ );
185
+
186
+ async function load(): Promise<void> {
187
+ // ⚠ A section change keeps the page standing (menu and all) while the
188
+ // next table is fetched: a switch that blanked the screen to "Loading…"
189
+ // made the menu itself flash away under the pointer that chose it.
190
+ if (!current.value) state.value = 'loading';
191
+ loadError.value = '';
192
+ try {
193
+ // The section only when there is one: a page that has no menu asks the
194
+ // same question it always asked.
195
+ const res = wanted.value
196
+ ? await props.api.pluginView(props.plugin, props.view, props.path, wanted.value)
197
+ : await props.api.pluginView(props.plugin, props.view, props.path);
198
+ if (!res?.surface) {
199
+ loadError.value = t('plugin.view.error');
200
+ state.value = 'error';
201
+ return;
202
+ }
203
+ conv.setSurface(res.surface);
204
+ state.value = 'surface';
205
+ // ⚠ Opened without a section, the page shows the plugin's first choice
206
+ // — and the address has to say which, or choosing that same entry from
207
+ // the menu changes nothing (measured in the e2e round: a click on the
208
+ // lit "requested" left the URL bare), and a copied link opens wherever
209
+ // the plugin's default happens to be that day.
210
+ const shown = res.surface.section ?? '';
211
+ if (!wanted.value && shown) {
212
+ wanted.value = shown;
213
+ emit('section', shown, { replace: true });
214
+ }
215
+ } catch (e) {
216
+ // ⚠ An app's own error text never met the catalogue — isolate it.
217
+ loadError.value = t.foreign?.(String((e as Error)?.message ?? '')) || t('plugin.view.error');
218
+ state.value = 'error';
219
+ }
220
+ }
221
+
222
+ onMounted(() => void load());
223
+ defineExpose({ reload: load });
224
+ /**
225
+ * Is this step showing a DOCUMENT?
226
+ *
227
+ * ⚠⚠ v3 §3.2 — "in `page` placement the node takes the whole viewport
228
+ * minus the step header and footer, and fits the page to it". The complaint
229
+ * that started the round was a signature page you had to scroll in two
230
+ * directions: the document was fitted to a 960px text column inside a page
231
+ * that scrolled, so a portrait page ran off the bottom and the boxes were
232
+ * found by hunting.
233
+ *
234
+ * So the FRAME changes shape when the surface carries one: the header and
235
+ * the footer keep their size, the body becomes the remainder, and the node
236
+ * fits the page into it (`SurfacePdfFields.fitWidth`). Only then — an
237
+ * ordinary form step is still a readable column, and locking every step to
238
+ * the viewport would turn a four-field form into a tall empty screen.
239
+ */
240
+ /**
241
+ * ⚠⚠ ...and only the step that REALLY shows one. A `pdf-fields` node in
242
+ * `define` mode draws no document at all — it is a column of cards, one per
243
+ * box — so shaping the frame to the window for it locked a growing list
244
+ * inside `height: 100vh; overflow: hidden` with no scroller of its own: the
245
+ * fourth box and everything after it sat below the fold, unreachable (the
246
+ * owner, 2026-09-23: "kutucukları oluşturduğumuz sayfada overflow hidden
247
+ * olduğu için aşağıda kalan ekstra kutucuklara kaydırarak gidemiyoruz").
248
+ * `place` and `edit` do carry the document and keep the window shape.
249
+ */
250
+ const showsDocument = computed(() => {
251
+ let found = false;
252
+ walkNodes(conv.current.value?.nodes, (n) => {
253
+ if (n.type !== 'pdf-fields') return;
254
+ if ((n.props as { mode?: unknown } | undefined)?.mode === 'define') return;
255
+ found = true;
256
+ });
257
+ return found;
258
+ });
259
+
260
+ </script>
261
+
262
+ <template>
263
+ <div
264
+ class="fe fe-apppage"
265
+ :class="[
266
+ theme === 'dark' ? 'fe--theme-dark' : theme === 'light' ? 'fe--theme-light' : '',
267
+ { 'is-doc': showsDocument && !embedded, 'fe-apppage--embedded': embedded },
268
+ ]"
269
+ data-testid="plugin-page"
270
+ >
271
+ <header v-if="!embedded" class="fe-apppage__bar">
272
+ <h1 class="fe-apppage__title" data-testid="plugin-page-title">{{ title }}</h1>
273
+ <p v-if="path" class="fe-apppage__path" :title="path">{{ path }}</p>
274
+ <a v-if="backHref" class="fe-btn fe-apppage__back" :href="backHref" data-testid="plugin-page-back">
275
+ {{ t('plugin.page_view.back') }}
276
+ </a>
277
+ </header>
278
+
279
+ <main class="fe-apppage__body">
280
+ <SurfaceSections
281
+ v-if="state === 'surface' && sections.length"
282
+ :sections="sections"
283
+ :active="activeSection"
284
+ :locale="locale"
285
+ :label="title"
286
+ :disabled="conv.busy.value"
287
+ @select="chooseSection"
288
+ />
289
+ <p v-if="state === 'loading'" class="fe-surface__text fe-surface__text--muted" data-testid="plugin-page-loading">
290
+ {{ t('plugin.view.loading') }}
291
+ </p>
292
+
293
+ <SurfaceConversation
294
+ v-else-if="state === 'surface'"
295
+ :conv="conv"
296
+ :locale="locale"
297
+ :theme="theme"
298
+ :api="api"
299
+ :plugin="plugin"
300
+ :storages="storages"
301
+ :start-at="startAt"
302
+ :toast="toast"
303
+ :layout="embedded ? 'inline' : 'page'"
304
+ />
305
+
306
+ <div v-else-if="state === 'queued'" class="fe-apppage__state" data-testid="plugin-page-queued">
307
+ <h2 class="fe-apppage__state-title">{{ t('plugin.page_view.queued_title') }}</h2>
308
+ <p class="fe-surface__text">{{ t('plugin.page_view.queued_text') }}</p>
309
+ <div class="fe-apppage__state-actions">
310
+ <a v-if="opsHref" class="fe-btn fe-btn--primary" :href="opsHref" data-testid="plugin-page-ops">
311
+ {{ t('plugin.page_view.open_ops') }}
312
+ </a>
313
+ <a v-if="backHref" class="fe-btn" :href="backHref">{{ t('plugin.page_view.back') }}</a>
314
+ </div>
315
+ </div>
316
+
317
+ <div v-else-if="state === 'done'" class="fe-apppage__state" data-testid="plugin-page-done">
318
+ <h2 class="fe-apppage__state-title">{{ t('plugin.page_view.done_title') }}</h2>
319
+ <p class="fe-surface__text">{{ t('plugin.page_view.done_text') }}</p>
320
+ <div class="fe-apppage__state-actions">
321
+ <a v-if="backHref" class="fe-btn fe-btn--primary" :href="backHref">{{ t('plugin.page_view.back') }}</a>
322
+ </div>
323
+ </div>
324
+
325
+ <div v-else class="fe-apppage__state" data-testid="plugin-page-error">
326
+ <h2 class="fe-apppage__state-title">{{ t('plugin.page_view.error_title') }}</h2>
327
+ <p v-if="loadError" class="fe-surface__error">{{ loadError }}</p>
328
+ <div class="fe-apppage__state-actions">
329
+ <button type="button" class="fe-btn" @click="load()">{{ t('plugin.page_view.retry') }}</button>
330
+ <a v-if="backHref" class="fe-btn" :href="backHref">{{ t('plugin.page_view.back') }}</a>
331
+ </div>
332
+ </div>
333
+ </main>
334
+
335
+ <footer v-if="state === 'surface' && conv.footer.value.length" class="fe-apppage__foot">
336
+ <SurfaceFooterButtons :conv="conv" :locale="locale" testid-prefix="plugin-page" />
337
+ </footer>
338
+ </div>
339
+ </template>
@@ -0,0 +1,159 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PluginViewModal — the dialog an app plugin's `modal` view opens in (and
4
+ * the frame a `home` view is drawn in, full-size).
5
+ *
6
+ * The explorer hands it the first surface (the answer to `run`, or the
7
+ * `open` event of a view); from then on the conversation is
8
+ * `usePluginSurface`'s — the same one the inspector section holds — and
9
+ * this file is only the frame: title, size, the footer buttons, Close.
10
+ * When the server answers `{op}` the row goes up (`op`) and the dialog
11
+ * closes.
12
+ *
13
+ * ⚠ No iframe, no `v-html`, no script: SurfaceRenderer draws nodes with our
14
+ * own components and shows an unknown type as unknown.
15
+ *
16
+ * ⚠ The body and the buttons are `SurfaceConversation` / `SurfaceFooterButtons`
17
+ * — shared with the `page` frame and the public card, so a fix to any of the
18
+ * five things a surface body draws reaches all three.
19
+ */
20
+ import { computed, watch } from 'vue';
21
+ import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
22
+ import type { FileApi } from '../../composables/useFileApi';
23
+ import type { PluginSurface, SurfaceOpenRequest } from '../../types/Plugins';
24
+ import { useLocale } from '../../composables/useLocale';
25
+ import { usePluginSurface } from '../../composables/usePluginSurface';
26
+ import { labelOf } from '../../lib/pluginLabel';
27
+ import Modal from '../../modals/Modal.vue';
28
+ import SurfaceConversation from './SurfaceConversation.vue';
29
+ import SurfaceSections from './SurfaceSections.vue';
30
+ import SurfaceFooterButtons from './SurfaceFooterButtons.vue';
31
+
32
+ const props = defineProps<{
33
+ open: boolean;
34
+ locale: LocaleCode;
35
+ theme?: ThemeMode;
36
+ api: FileApi;
37
+ plugin: string;
38
+ view: string;
39
+ /** The first screen. */
40
+ surface: PluginSurface | null;
41
+ /** Adapter-qualified path of the row the view was opened on (echoed on every event). */
42
+ path?: string;
43
+ /** Storage names a file-chooser may span; where its picker opens. */
44
+ storages?: string[];
45
+ startAt?: string;
46
+ /** Forces the dialog size (a `home` view is drawn `xl`); else the surface's own. */
47
+ size?: 'sm' | 'md' | 'lg' | 'xl';
48
+ }>();
49
+
50
+ const emit = defineEmits<{
51
+ (e: 'close'): void;
52
+ /** The server enqueued a job for this view; the raw ops row. */
53
+ (e: 'op', op: Record<string, unknown>): void;
54
+ (e: 'toast', message: string): void;
55
+ /**
56
+ * v3 §3.0 — the answer says "go to this file". Reported UP rather than
57
+ * acted on here: the explorer is what can show a file and start a screen
58
+ * on it (`openAppTarget`), and a dialog navigating the window under itself
59
+ * would throw away the folder the person was standing in.
60
+ */
61
+ (e: 'open', req: SurfaceOpenRequest): void;
62
+ }>();
63
+
64
+ const { t } = useLocale(() => props.locale);
65
+
66
+ const conv = usePluginSurface(
67
+ {
68
+ api: props.api,
69
+ plugin: props.plugin,
70
+ view: props.view,
71
+ path: () => props.path,
72
+ locale: () => props.locale,
73
+ errorText: () => t('plugin.view.error'),
74
+ },
75
+ {
76
+ onOp: (op) => emit('op', op),
77
+ onDone: () => emit('close'),
78
+ onToast: (m) => emit('toast', m),
79
+ onOpen: (req) => emit('open', req),
80
+ },
81
+ );
82
+
83
+ watch(
84
+ () => props.surface,
85
+ (s) => conv.setSurface(s),
86
+ { immediate: true },
87
+ );
88
+
89
+ const current = conv.current;
90
+ const title = computed(() => labelOf(current.value?.title, props.locale) || props.view);
91
+ /**
92
+ * A home page's menu, when a host with no page of its own draws one in this
93
+ * dialog (the degraded frame — an embed without `pluginPageBase`). There is
94
+ * no address to keep here, so a choice simply asks for that section.
95
+ */
96
+ const sections = computed(() => current.value?.sections ?? []);
97
+ async function chooseSection(id: string): Promise<void> {
98
+ if (!id || id === current.value?.section) return;
99
+ try {
100
+ const res = await props.api.pluginView(props.plugin, props.view, props.path, id);
101
+ if (res?.surface) conv.setSurface(res.surface);
102
+ } catch (e) {
103
+ // ⚠ An app's own error text never met the catalogue — isolate it.
104
+ emit('toast', t.foreign?.(String((e as Error)?.message ?? '')) || t('plugin.view.error'));
105
+ }
106
+ }
107
+
108
+ const size = computed<'sm' | 'md' | 'lg' | 'xl'>(() => {
109
+ if (props.size) return props.size;
110
+ const s = current.value?.size;
111
+ return s === 'sm' || s === 'lg' || s === 'xl' ? s : 'md';
112
+ });
113
+ </script>
114
+
115
+ <template>
116
+ <Modal :open="open" :title="title" :size="size" :theme="theme" @close="emit('close')">
117
+ <div class="fe-plugin-view" data-testid="plugin-view">
118
+ <SurfaceSections
119
+ v-if="sections.length"
120
+ :sections="sections"
121
+ :active="current?.section"
122
+ :locale="locale"
123
+ :label="title"
124
+ :disabled="conv.busy.value"
125
+ @select="chooseSection"
126
+ />
127
+ <SurfaceConversation
128
+ :conv="conv"
129
+ :locale="locale"
130
+ :theme="theme"
131
+ :api="api"
132
+ :plugin="plugin"
133
+ :storages="storages"
134
+ :start-at="startAt"
135
+ />
136
+ </div>
137
+ <!-- ⚠ The frame's own Close only when the screen brought no buttons of
138
+ its own. A plugin's footer already carries its way out (Cancel,
139
+ Back), the header has its ×, and Escape and an outside click close
140
+ the dialog — a "Close" beside the plugin's "Cancel" was two buttons
141
+ doing one thing (the converter's first step read "Kapat · Vazgeç ·
142
+ İleri", release-candidate sweep 2026-09-21). A screen with no
143
+ buttons at all — a result, a notice — still gets one, so no screen
144
+ is a dead end for a pointer. -->
145
+ <template #actions>
146
+ <button
147
+ v-if="!conv.footer.value.length"
148
+ type="button"
149
+ class="fe-btn"
150
+ :disabled="conv.busy.value"
151
+ data-testid="plugin-view-close"
152
+ @click="emit('close')"
153
+ >
154
+ {{ t('plugin.view.close') }}
155
+ </button>
156
+ <SurfaceFooterButtons :conv="conv" :locale="locale" testid-prefix="plugin-view" />
157
+ </template>
158
+ </Modal>
159
+ </template>
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SurfaceConversation — the body of ANY screen a plugin is talking through.
4
+ *
5
+ * Three frames draw a plugin surface: the modal a `modal` view opens, the
6
+ * whole page a `page` view opens, and the card an outside participant gets
7
+ * at `/p/<token>`. The FRAME is what differs (a dialog, a document, a card);
8
+ * the body is the same five things every time — the nodes, the toast the
9
+ * answer carried, "working…", the failure, and the footer buttons.
10
+ *
11
+ * ⚠⚠ Written once for that reason. Three copies of this block existed by the
12
+ * time the `page` placement arrived, and the drift had already started: the
13
+ * modal showed no toast at all, so a plugin that answered `{toast}` from a
14
+ * dialog was simply not heard. Anything added here — a node type's wiring, a
15
+ * busy affordance, an aria role — now reaches all three frames or none.
16
+ */
17
+ import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
18
+ import type { PluginSurfaceStore } from '../../composables/usePluginSurface';
19
+ import type { FileRefResolver } from '../../types/Plugins';
20
+ import { useLocale } from '../../composables/useLocale';
21
+ import SurfaceRenderer, { type SurfaceApi } from './SurfaceRenderer.vue';
22
+
23
+ const props = defineProps<{
24
+ /** The conversation this body belongs to (`usePluginSurface`). */
25
+ conv: PluginSurfaceStore;
26
+ locale: LocaleCode;
27
+ theme?: ThemeMode;
28
+ /** Absent in a bare render (the public page has no explorer API). */
29
+ api?: SurfaceApi;
30
+ /** The plugin the surface belongs to — the people-picker's lookup asks for it. */
31
+ plugin?: string;
32
+ storages?: string[];
33
+ startAt?: string;
34
+ /** A public page's exposed copies (`pub:N`) → a URL the browser may load. */
35
+ fileUrl?: FileRefResolver;
36
+ /** The last toast the frame collected, if it shows one inline. */
37
+ toast?: string;
38
+ /**
39
+ * v3 §3.2 — `page` lets a `pdf-fields` node take the viewport instead of
40
+ * sitting in the document flow. The FRAME knows which it is; the nodes do
41
+ * not, and must not guess from the window size (an embed in a narrow
42
+ * column is not a page).
43
+ */
44
+ layout?: 'inline' | 'page';
45
+ /** Where the document may grow to in `page` layout — the frame's own box. */
46
+ pageHeight?: string;
47
+ }>();
48
+
49
+ const { t } = useLocale(() => props.locale);
50
+ </script>
51
+
52
+ <template>
53
+ <SurfaceRenderer
54
+ v-if="conv.current.value"
55
+ :nodes="conv.current.value.nodes ?? []"
56
+ :locale="locale"
57
+ :theme="theme"
58
+ :errors="conv.errors.value"
59
+ :values="conv.values.value"
60
+ :api="api"
61
+ :plugin="plugin"
62
+ :storages="storages"
63
+ :start-at="startAt"
64
+ :file-url="fileUrl"
65
+ :layout="layout"
66
+ :page-height="pageHeight"
67
+ :invalid-keys="conv.invalidKeys.value"
68
+ :disabled="conv.busy.value"
69
+ @update:values="conv.updateValues"
70
+ @change="conv.scheduleChange"
71
+ @action="conv.rowAction"
72
+ />
73
+ <p v-if="toast" class="fe-surface__text fe-surface__text--info" role="status">{{ toast }}</p>
74
+ <p v-if="conv.busy.value" class="fe-surface__text fe-surface__text--muted">{{ t('plugin.view.busy') }}</p>
75
+ <p v-if="conv.failure.value" class="fe-surface__error" role="alert">{{ conv.failure.value }}</p>
76
+ </template>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SurfaceFooterButtons — a surface's footer, wherever the frame puts it.
4
+ *
5
+ * The rule a plugin's buttons follow is not the frame's business: the
6
+ * primary one posts `submit`, every other one posts `action`, a `danger` one
7
+ * is red, and none of them is clickable while the conversation is busy
8
+ * (`usePluginSurface.press`). Three frames (dialog, page, public shell)
9
+ * place the row differently and must not each re-derive that.
10
+ *
11
+ * ⚠⚠ v3 §2 rule 3 — on a STEP (a surface carrying a `steps` node) the row is
12
+ * arranged rather than merely listed: Back on the left, one primary on the
13
+ * right, everything else between them, and a second `primary: true` is drawn
14
+ * as an ordinary button. `lib/surfaceSteps` holds the arrangement so the
15
+ * frames, the tests and the plugin test kit all read one answer.
16
+ *
17
+ * ⚠ `testidPrefix` is the one thing that legitimately differs per frame: the
18
+ * suites address the dialog's buttons and the page's separately, so a shared
19
+ * id would make a passing test ambiguous about which screen it measured.
20
+ */
21
+ import { computed } from 'vue';
22
+ import type { LocaleCode } from '../../types/ExplorerConfig';
23
+ import type { PluginSurfaceStore } from '../../composables/usePluginSurface';
24
+ import type { SurfaceAction } from '../../types/Plugins';
25
+ import { labelOf } from '../../lib/pluginLabel';
26
+ import { hasSteps, stepFooter } from '../../lib/surfaceSteps';
27
+
28
+ const props = defineProps<{
29
+ conv: PluginSurfaceStore;
30
+ locale: LocaleCode;
31
+ /** `plugin-view` → `data-testid="plugin-view-action-<id>"`. */
32
+ testidPrefix: string;
33
+ /** The inspector's row is compact; every other frame uses the full size. */
34
+ small?: boolean;
35
+ }>();
36
+
37
+ const stepped = computed(() => hasSteps(props.conv.current.value?.nodes));
38
+
39
+ const arranged = computed(() => stepFooter(props.conv.footer.value, stepped.value));
40
+
41
+ /** Back, the ordinary buttons, then the one primary — in that DOM order. */
42
+ const ordered = computed<SurfaceAction[]>(() => {
43
+ const f = arranged.value;
44
+ return [...(f.back ? [f.back] : []), ...f.others, ...(f.primary ? [f.primary] : [])];
45
+ });
46
+
47
+ /** Only the arranged primary is primary; a plugin's second one is not. */
48
+ function isPrimary(a: SurfaceAction): boolean {
49
+ if (!stepped.value) return a.primary === true;
50
+ return a === arranged.value.primary;
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <button
56
+ v-for="a in ordered"
57
+ :key="a.id"
58
+ type="button"
59
+ class="fe-btn"
60
+ :class="{
61
+ 'fe-btn--sm': small,
62
+ 'fe-btn--primary': isPrimary(a) && !a.danger,
63
+ 'fe-btn--danger': a.danger,
64
+ 'fe-surface__back': stepped && a === arranged.back,
65
+ }"
66
+ :disabled="props.conv.busy.value || a.disabled"
67
+ :data-testid="`${testidPrefix}-action-${a.id}`"
68
+ @click="props.conv.press(a)"
69
+ >
70
+ {{ labelOf(a.label, locale) }}
71
+ </button>
72
+ </template>
73
+