@brftech/filex-core 0.40.0 → 0.41.1

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 (125) hide show
  1. package/README.md +32 -25
  2. package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
  3. package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
  4. package/dist/CsvViewer-CyuwENI_.js +141 -0
  5. package/dist/CsvViewer-CyuwENI_.js.map +1 -0
  6. package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-BIYsTX97.js} +48 -42
  7. package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
  8. package/dist/EpubViewer-K9PjCMmS.js +163 -0
  9. package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
  10. package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
  11. package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
  12. package/dist/MermaidViewer-Bp73aId8.js +137 -0
  13. package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
  14. package/dist/PsdViewer-C2eLivw1.js +122 -0
  15. package/dist/PsdViewer-C2eLivw1.js.map +1 -0
  16. package/dist/TiffViewer-DsHg5gcz.js +142 -0
  17. package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
  18. package/dist/Viewer3D-C_dc2HN9.js +75 -0
  19. package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
  20. package/dist/filex-core.js +218 -20361
  21. package/dist/filex-core.js.map +1 -1
  22. package/dist/filex-core.umd.cjs +61 -62
  23. package/dist/filex-core.umd.cjs.map +1 -1
  24. package/dist/index-vxMZpYb-.js +26700 -0
  25. package/dist/index-vxMZpYb-.js.map +1 -0
  26. package/dist/index.d.ts +4212 -862
  27. package/dist/style.css +1 -1
  28. package/package.json +1 -1
  29. package/src/FileExplorer.vue +2154 -506
  30. package/src/components/AdvancedSearch.vue +870 -0
  31. package/src/components/Breadcrumb.vue +191 -19
  32. package/src/components/CommandPalette.vue +66 -22
  33. package/src/components/ConnectionsPanel.vue +6 -2
  34. package/src/components/ContextMenu.vue +39 -2
  35. package/src/components/EncryptedFolderModal.vue +31 -25
  36. package/src/components/FilePane.vue +951 -0
  37. package/src/components/FilterBar.vue +568 -72
  38. package/src/components/GalleryView.vue +172 -56
  39. package/src/components/GridView.vue +331 -82
  40. package/src/components/HomeView.vue +331 -0
  41. package/src/components/InspectorPanel.vue +296 -53
  42. package/src/components/ListView.vue +1173 -194
  43. package/src/components/NFSExportsPanel.vue +7 -4
  44. package/src/components/OnboardingTour.vue +156 -33
  45. package/src/components/QuickLook.vue +26 -1
  46. package/src/components/RecentlyOpened.vue +17 -2
  47. package/src/components/S3KeysPanel.vue +7 -4
  48. package/src/components/SSHKeysPanel.vue +7 -4
  49. package/src/components/ShortcutSettings.vue +6 -2
  50. package/src/components/ShortcutsHelp.vue +4 -1
  51. package/src/components/SideNav.vue +206 -140
  52. package/src/components/StorageFields.vue +19 -1
  53. package/src/components/TabBar.vue +87 -1
  54. package/src/components/TagPicker.vue +77 -6
  55. package/src/components/ThemeGallery.vue +13 -198
  56. package/src/components/ThemePalette.vue +244 -0
  57. package/src/components/TimeZoneDialog.vue +135 -0
  58. package/src/components/TimeZonePicker.vue +696 -0
  59. package/src/components/TokensPanel.vue +35 -14
  60. package/src/components/Toolbar.vue +953 -465
  61. package/src/components/ViewSwitcher.vue +9 -4
  62. package/src/composables/useConnections.ts +35 -4
  63. package/src/composables/useExplorerTimeZone.ts +89 -0
  64. package/src/composables/useFileApi.ts +53 -4
  65. package/src/composables/useLocale.ts +382 -26
  66. package/src/composables/useRowTouch.ts +95 -0
  67. package/src/index.ts +177 -3
  68. package/src/lib/actionIcons.ts +330 -0
  69. package/src/lib/advSearch.ts +162 -0
  70. package/src/lib/dateGroups.ts +197 -0
  71. package/src/lib/destinationTree.ts +235 -0
  72. package/src/lib/downloadSelection.ts +154 -0
  73. package/src/lib/fileFilters.ts +319 -12
  74. package/src/lib/fileIcons.ts +299 -5
  75. package/src/lib/filePreview.ts +605 -0
  76. package/src/lib/listing.ts +72 -0
  77. package/src/lib/shareTtl.ts +31 -2
  78. package/src/lib/sortOrder.ts +459 -0
  79. package/src/lib/themes.ts +151 -116
  80. package/src/lib/timezone.ts +501 -0
  81. package/src/lib/uiProfile.ts +82 -0
  82. package/src/lib/viewPrefs.ts +1036 -0
  83. package/src/locales/en.ts +416 -26
  84. package/src/locales/tr.ts +409 -26
  85. package/src/modals/ConvertModal.vue +23 -3
  86. package/src/modals/DestinationPickerModal.vue +341 -0
  87. package/src/modals/Modal.vue +18 -4
  88. package/src/modals/NewDocumentModal.vue +525 -0
  89. package/src/modals/NewFolderModal.vue +15 -9
  90. package/src/modals/PermissionsModal.vue +557 -476
  91. package/src/modals/PreviewModal.vue +516 -156
  92. package/src/styles/base.css +5409 -8
  93. package/src/styles/variables.css +139 -65
  94. package/src/types/Connections.ts +9 -0
  95. package/src/types/ExplorerConfig.ts +144 -39
  96. package/src/types/FileNode.ts +28 -0
  97. package/src/viewers/ArchiveViewer.vue +46 -8
  98. package/src/viewers/CsvViewer.vue +19 -2
  99. package/src/viewers/DrawioViewer.vue +12 -1
  100. package/src/viewers/EpubViewer.vue +50 -6
  101. package/src/viewers/IpynbViewer.vue +19 -2
  102. package/src/viewers/MermaidViewer.vue +20 -3
  103. package/src/viewers/PdfViewer.vue +24 -4
  104. package/src/viewers/PsdViewer.vue +28 -4
  105. package/src/viewers/TiffViewer.vue +19 -2
  106. package/src/viewers/Viewer3D.vue +36 -2
  107. package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
  108. package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
  109. package/dist/CsvViewer-CqWeV8VO.js +0 -131
  110. package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
  111. package/dist/DrawioViewer-BNALOB04.js.map +0 -1
  112. package/dist/EpubViewer-BPmlImG7.js +0 -145
  113. package/dist/EpubViewer-BPmlImG7.js.map +0 -1
  114. package/dist/IpynbViewer-CDlQmuCV.js +0 -175
  115. package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
  116. package/dist/MermaidViewer-sfZWdR-d.js +0 -127
  117. package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
  118. package/dist/PsdViewer-DwuySZsb.js +0 -112
  119. package/dist/PsdViewer-DwuySZsb.js.map +0 -1
  120. package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
  121. package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
  122. package/dist/Viewer3D-CJJJgkuv.js +0 -60
  123. package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
  124. package/src/components/SecondaryPane.vue +0 -447
  125. package/src/modals/ShareModal.vue +0 -139
@@ -17,6 +17,7 @@
17
17
  import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
18
18
  import type { LocaleCode } from '../types/ExplorerConfig';
19
19
  import { useLocale } from '../composables/useLocale';
20
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
20
21
 
21
22
  const props = defineProps<{
22
23
  /** Converter base, e.g. https://fm.example.com/convert */
@@ -152,7 +153,16 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
152
153
  <div class="filex-cv">
153
154
  <header class="filex-cv__head">
154
155
  <h3>{{ t('convert.title') }} — {{ fileName }}</h3>
155
- <button class="filex-cv__x" @click="emit('close')">✕</button>
156
+ <button
157
+ class="filex-cv__x"
158
+ type="button"
159
+ :title="t('convert.close')"
160
+ :aria-label="t('convert.close')"
161
+ @click="emit('close')"
162
+ >
163
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
164
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
165
+ </button>
156
166
  </header>
157
167
 
158
168
  <div v-if="status === 'loading'" class="filex-cv__msg">
@@ -160,7 +170,11 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
160
170
  </div>
161
171
 
162
172
  <div v-else-if="status === 'done'" class="filex-cv__msg filex-cv__ok">
163
- <p>✓ {{ t('convert.done') }}</p>
173
+ <p>
174
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
175
+ <span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('check')"></span>
176
+ {{ t('convert.done') }}
177
+ </p>
164
178
  <button class="filex-cv__convert" @click="emit('close')">{{ t('convert.close') }}</button>
165
179
  </div>
166
180
 
@@ -231,7 +245,13 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
231
245
  padding: 12px 16px; border-bottom: 1px solid var(--fe-border, #e2e6ed);
232
246
  }
233
247
  .filex-cv__head h3 { margin: 0; font-size: 14px; font-weight: 600; }
234
- .filex-cv__x { border: 0; background: none; cursor: pointer; font-size: 16px; color: var(--fe-text-muted, #5a6475); }
248
+ .filex-cv__x {
249
+ display: inline-flex; align-items: center; justify-content: center;
250
+ border: 0; background: none; cursor: pointer; font-size: 16px;
251
+ color: var(--fe-text-muted, #5a6475);
252
+ }
253
+ .filex-cv__x .fe-aicon { color: currentColor; }
254
+ .filex-cv__ok .fe-icon { display: inline-flex; vertical-align: -0.2em; }
235
255
  .filex-cv__msg { padding: 28px; text-align: center; color: var(--fe-text-muted, #5a6475); }
236
256
  .filex-cv__ok { color: #059669; }
237
257
  .filex-cv__src { padding: 12px 16px 0; font-size: 13px; color: var(--fe-text-muted, #5a6475); }
@@ -0,0 +1,341 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * DestinationPickerModal — "choose a folder", as one component.
4
+ *
5
+ * # Why it exists
6
+ *
7
+ * `Move to…` and `Copy to…` were missing from the selection bar, and the
8
+ * missing part was never the verb: `transferItems` already moves and copies,
9
+ * across storages, and cut+paste has always done it. What did not exist
10
+ * anywhere in the client was a way to SAY WHERE. `dirsOnly` — the backend's
11
+ * folders-only listing — had zero consumers; `api.subfolders()` had been
12
+ * written and never called.
13
+ *
14
+ * # Why a component and not two buttons' private dialog
15
+ *
16
+ * `NewDocumentModal` already grew a private one-level browser for its own
17
+ * "where should this go?" question. A second private copy for Move/Copy is how
18
+ * the two start disagreeing — about what a writable folder is, about whether an
19
+ * unwritable one is hidden or greyed, about what sits above a storage root. So
20
+ * this takes its inputs as props and answers with one event, and the rules live
21
+ * in `lib/destinationTree.ts` where they can be tested without a DOM. Anything
22
+ * that needs a folder — Move, Copy, and the new-document flow next — mounts
23
+ * this.
24
+ *
25
+ * # Contract
26
+ *
27
+ * <DestinationPickerModal
28
+ * :open="show" :api="api" :locale="locale"
29
+ * :storages="['main','s3']" // drive names; >1 adds a drives level
30
+ * :start-at="'main://belgeler'" // where to open (the current folder)
31
+ * :moving="['main://belgeler/eski']" // folders that may not be the target
32
+ * mode="move" // titles the dialog and its button
33
+ * @pick="(wirePath) => …"
34
+ * @close="show = false"
35
+ * />
36
+ *
37
+ * `pick` emits an adapter-qualified wire path — exactly what `api.copy` and
38
+ * `api.moveAsync` take.
39
+ */
40
+ import { computed, ref, watch } from 'vue';
41
+ import Modal from './Modal.vue';
42
+ import { useLocale } from '../composables/useLocale';
43
+ import type { FileApi, ManagerResponse } from '../composables/useFileApi';
44
+ import type { LocaleCode } from '../types/ExplorerConfig';
45
+ import { iconTile } from '../lib/fileIcons';
46
+ import {
47
+ DRIVES,
48
+ type DestinationRow,
49
+ blockedReason,
50
+ crumbsOfWire,
51
+ destinationRows,
52
+ driveRows,
53
+ initialLocation,
54
+ labelOfWire,
55
+ parentOfWire,
56
+ permAllowsWrite,
57
+ } from '../lib/destinationTree';
58
+
59
+ const props = defineProps<{
60
+ open: boolean;
61
+ api: Pick<FileApi, 'index'>;
62
+ locale: LocaleCode;
63
+ /** Storage names. More than one adds a drives level above the roots. */
64
+ storages?: string[];
65
+ /** Wire path to open at — normally the folder the user is looking at. */
66
+ startAt?: string;
67
+ /**
68
+ * Folders that must not be offered: what is being moved. A destination equal
69
+ * to one of these, or inside one, is shown greyed with a reason rather than
70
+ * hidden, because a folder that silently vanishes reads as data loss.
71
+ */
72
+ moving?: string[];
73
+ /** Titles the dialog and names its confirm button. */
74
+ mode?: 'move' | 'copy' | 'choose';
75
+ /** The parent is running the operation: the dialog stays up but inert. */
76
+ busy?: boolean;
77
+ }>();
78
+
79
+ const emit = defineEmits<{
80
+ (e: 'close'): void;
81
+ (e: 'pick', path: string): void;
82
+ }>();
83
+
84
+ const { t } = useLocale(() => props.locale);
85
+
86
+ const at = ref<string>(DRIVES);
87
+ const rows = ref<DestinationRow[]>([]);
88
+ const loading = ref(false);
89
+ /** The listing of `at` itself — what says whether the CURRENT folder is writable. */
90
+ const here = ref<ManagerResponse | undefined>(undefined);
91
+ const failed = ref(false);
92
+
93
+ const multiDrive = computed(() => (props.storages?.length ?? 0) > 1);
94
+
95
+ /**
96
+ * One listing cache for the life of the dialog.
97
+ *
98
+ * Walking up and back down is the normal way people use a picker, and without
99
+ * this every Up button press costs another round trip to a storage that may be
100
+ * an S3 bucket on another continent.
101
+ */
102
+ let cache = new Map<string, ManagerResponse>();
103
+
104
+ watch(
105
+ () => props.open,
106
+ (isOpen) => {
107
+ if (!isOpen) return;
108
+ cache = new Map();
109
+ void goTo(initialLocation(props.startAt, props.storages));
110
+ },
111
+ { immediate: true },
112
+ );
113
+
114
+ async function load(path: string): Promise<ManagerResponse | undefined> {
115
+ const hit = cache.get(path);
116
+ if (hit) return hit;
117
+ try {
118
+ const resp = await props.api.index(path);
119
+ cache.set(path, resp);
120
+ return resp;
121
+ } catch {
122
+ // A folder that cannot be listed is a folder that cannot be offered. The
123
+ // dialog says so in its own body; a listing failure inside a picker is not
124
+ // an error the host should be made to toast.
125
+ return undefined;
126
+ }
127
+ }
128
+
129
+ async function goTo(path: string): Promise<void> {
130
+ at.value = path;
131
+ failed.value = false;
132
+ if (path === DRIVES) {
133
+ here.value = undefined;
134
+ rows.value = driveRows(props.storages, props.moving);
135
+ return;
136
+ }
137
+ loading.value = true;
138
+ const resp = await load(path);
139
+ // A second navigation may have started while this listing was in flight;
140
+ // only the newest one gets to write the rows.
141
+ if (at.value !== path) return;
142
+ loading.value = false;
143
+ here.value = resp;
144
+ failed.value = !resp;
145
+ rows.value = destinationRows(resp?.files, props.moving);
146
+ }
147
+
148
+ const parent = computed(() => parentOfWire(at.value, multiDrive.value));
149
+ const crumbs = computed(() => crumbsOfWire(at.value));
150
+
151
+ /** Why the CURRENT folder cannot be chosen, or null when it can. */
152
+ const hereBlocked = computed(() => blockedReason(at.value, props.moving));
153
+
154
+ const hereWritable = computed(() => {
155
+ if (at.value === DRIVES) return false;
156
+ if (!here.value) return false;
157
+ if (here.value.read_only) return false;
158
+ return permAllowsWrite(here.value.perm as string | undefined);
159
+ });
160
+
161
+ const canChoose = computed(
162
+ () => !props.busy && at.value !== DRIVES && hereWritable.value && hereBlocked.value === null,
163
+ );
164
+
165
+ /**
166
+ * Is the line under the list a REFUSAL or a prompt?
167
+ *
168
+ * "Open a storage to choose a folder inside it" is not a mistake anybody made —
169
+ * it is the drives level telling you what to do next. Painting it in the danger
170
+ * colour, which is what happened the first time this was drawn, makes the very
171
+ * first screen of the dialog look like something has gone wrong.
172
+ */
173
+ const reasonIsRefusal = computed(() => at.value !== DRIVES);
174
+
175
+ /** The one line under the list that says why Choose is off, if it is. */
176
+ const reason = computed<string>(() => {
177
+ if (at.value === DRIVES) return t('destpicker.pick_a_drive');
178
+ if (hereBlocked.value === 'self') return t('destpicker.blocked.self');
179
+ if (hereBlocked.value === 'descendant') return t('destpicker.blocked.descendant');
180
+ if (failed.value) return t('destpicker.unreadable');
181
+ if (here.value && !hereWritable.value) return t('destpicker.readonly_here');
182
+ return '';
183
+ });
184
+
185
+ const title = computed(() => {
186
+ if (props.mode === 'move') return t('destpicker.title.move');
187
+ if (props.mode === 'copy') return t('destpicker.title.copy');
188
+ return t('destpicker.title.choose');
189
+ });
190
+
191
+ const confirmLabel = computed(() => {
192
+ if (props.mode === 'move') return t('destpicker.confirm.move');
193
+ if (props.mode === 'copy') return t('destpicker.confirm.copy');
194
+ return t('destpicker.confirm.choose');
195
+ });
196
+
197
+ function rowTitle(row: DestinationRow): string {
198
+ if (row.blocked === 'self') return t('destpicker.blocked.self');
199
+ if (row.blocked === 'descendant') return t('destpicker.blocked.descendant');
200
+ if (!row.writable) return t('destpicker.readonly');
201
+ return row.label;
202
+ }
203
+
204
+ function openRow(row: DestinationRow): void {
205
+ // A blocked row is a dead end — everything under it is blocked too — so
206
+ // walking into it would be navigation that cannot end anywhere.
207
+ if (row.blocked) return;
208
+ void goTo(row.path);
209
+ }
210
+
211
+ function choose(): void {
212
+ if (!canChoose.value) return;
213
+ emit('pick', at.value);
214
+ }
215
+
216
+ const drivesLabel = computed(() => t('destpicker.drives'));
217
+ </script>
218
+
219
+ <template>
220
+ <Modal
221
+ :open="open"
222
+ :title="title"
223
+ size="md"
224
+ @close="emit('close')"
225
+ >
226
+ <div class="fe-destpick" data-testid="destpicker">
227
+ <div class="fe-destpick__bar">
228
+ <button
229
+ type="button"
230
+ class="fe-destpick__up"
231
+ :disabled="!parent"
232
+ :title="t('destpicker.up')"
233
+ :aria-label="t('destpicker.up')"
234
+ data-testid="destpicker-up"
235
+ @click="goTo(parent as string)"
236
+ >
237
+ <svg
238
+ class="fe-ficon"
239
+ viewBox="0 0 24 24"
240
+ fill="none"
241
+ stroke="currentColor"
242
+ stroke-width="2"
243
+ stroke-linecap="round"
244
+ stroke-linejoin="round"
245
+ aria-hidden="true"
246
+ focusable="false"
247
+ >
248
+ <path d="M12 19V5M5 12l7-7 7 7" />
249
+ </svg>
250
+ </button>
251
+ <nav class="fe-destpick__crumbs" :aria-label="t('destpicker.crumbs')">
252
+ <button
253
+ v-if="multiDrive"
254
+ type="button"
255
+ class="fe-destpick__crumb"
256
+ :class="{ 'is-current': at === DRIVES }"
257
+ @click="goTo(DRIVES)"
258
+ >
259
+ {{ drivesLabel }}
260
+ </button>
261
+ <template v-for="c in crumbs" :key="c.path">
262
+ <span class="fe-destpick__sep" aria-hidden="true">/</span>
263
+ <button
264
+ type="button"
265
+ class="fe-destpick__crumb"
266
+ :class="{ 'is-current': c.path === at }"
267
+ @click="goTo(c.path)"
268
+ >
269
+ {{ c.label }}
270
+ </button>
271
+ </template>
272
+ <span v-if="!multiDrive && !crumbs.length" class="fe-destpick__crumb is-current">
273
+ {{ labelOfWire(at, drivesLabel) }}
274
+ </span>
275
+ </nav>
276
+ </div>
277
+
278
+ <ul class="fe-destpick__rows" data-testid="destpicker-rows">
279
+ <li v-if="loading" class="fe-destpick__note">{{ t('destpicker.loading') }}</li>
280
+ <li v-else-if="failed" class="fe-destpick__note">{{ t('destpicker.unreadable') }}</li>
281
+ <li v-else-if="!rows.length" class="fe-destpick__note">{{ t('destpicker.empty') }}</li>
282
+ <li v-for="row in rows" :key="row.path">
283
+ <button
284
+ type="button"
285
+ class="fe-destpick__row"
286
+ :class="{ 'is-locked': !row.writable, 'is-blocked': !!row.blocked }"
287
+ :disabled="!!row.blocked"
288
+ :title="rowTitle(row)"
289
+ :data-testid="'destpicker-row-' + row.label"
290
+ @click="openRow(row)"
291
+ >
292
+ <span class="fe-destpick__rowicon" aria-hidden="true" v-html="iconTile('folder')"></span>
293
+ <span class="fe-destpick__rowname">{{ row.label }}</span>
294
+ <span v-if="row.blocked" class="fe-destpick__rowtag">{{ t('destpicker.tag.blocked') }}</span>
295
+ <span v-else-if="!row.writable" class="fe-destpick__rowtag">{{ t('destpicker.readonly') }}</span>
296
+ <svg
297
+ class="fe-destpick__rowinto"
298
+ viewBox="0 0 24 24"
299
+ fill="none"
300
+ stroke="currentColor"
301
+ stroke-width="2"
302
+ stroke-linecap="round"
303
+ stroke-linejoin="round"
304
+ aria-hidden="true"
305
+ focusable="false"
306
+ >
307
+ <path d="M9 5l7 7-7 7" />
308
+ </svg>
309
+ </button>
310
+ </li>
311
+ </ul>
312
+
313
+ <p
314
+ v-if="reason"
315
+ class="fe-destpick__reason"
316
+ :class="{ 'is-hint': !reasonIsRefusal }"
317
+ data-testid="destpicker-reason"
318
+ >
319
+ {{ reason }}
320
+ </p>
321
+ <p v-else class="fe-destpick__target" data-testid="destpicker-target">
322
+ {{ t('destpicker.target', { name: labelOfWire(at, drivesLabel) }) }}
323
+ </p>
324
+ </div>
325
+
326
+ <template #actions>
327
+ <button type="button" class="fe-btn" @click="emit('close')">
328
+ {{ t('destpicker.cancel') }}
329
+ </button>
330
+ <button
331
+ type="button"
332
+ class="fe-btn fe-btn--primary"
333
+ :disabled="!canChoose"
334
+ data-testid="destpicker-confirm"
335
+ @click="choose"
336
+ >
337
+ {{ confirmLabel }}
338
+ </button>
339
+ </template>
340
+ </Modal>
341
+ </template>
@@ -16,6 +16,18 @@ const props = defineProps<{
16
16
  * from the editor. ESC + emit('close') still wire through so the
17
17
  * parent route can window.close() the tab. */
18
18
  chromeless?: boolean;
19
+ /**
20
+ * gorunum:v1 — full-bleed overlay. Like `chromeless` it drops the dialog
21
+ * head and footer and lets the card own the whole viewport, but it KEEPS
22
+ * the modal identity: the card still paints a ground and the caller draws
23
+ * its own chrome inside the slot (see PreviewModal's viewer bar).
24
+ *
25
+ * ⚠ Deliberately a second flag rather than a reuse of `chromeless`.
26
+ * `chromeless` means "the browser tab IS the container" (the standalone
27
+ * /files/edit route) and e2e/tests/83 asserts on exactly that class; an
28
+ * in-page preview that borrowed it would start claiming to be that route.
29
+ */
30
+ fullbleed?: boolean;
19
31
  /** Explicit theme. When set the modal stamps the appropriate
20
32
  * `.fe--theme-{light,dark}` class on its `.fe` backdrop so the CSS
21
33
  * variable cascade matches the host shell regardless of OS
@@ -100,6 +112,7 @@ function onBackdrop() {
100
112
  class="fe fe-modal__backdrop"
101
113
  :class="[
102
114
  { 'fe-modal__backdrop--chromeless': chromeless },
115
+ { 'fe-modal__backdrop--fullbleed': fullbleed && !chromeless },
103
116
  theme === 'light' ? 'fe--theme-light' : '',
104
117
  theme === 'dark' ? 'fe--theme-dark' : '',
105
118
  ]"
@@ -112,14 +125,15 @@ function onBackdrop() {
112
125
  :class="[
113
126
  `fe-modal__card--${size || 'md'}`,
114
127
  chromeless && 'fe-modal__card--chromeless',
128
+ fullbleed && !chromeless && 'fe-modal__card--fullbleed',
115
129
  ]"
116
130
  role="dialog"
117
131
  aria-modal="true"
118
- :aria-labelledby="title && !chromeless ? titleId : undefined"
119
- :aria-label="!title || chromeless ? title || undefined : undefined"
132
+ :aria-labelledby="title && !chromeless && !fullbleed ? titleId : undefined"
133
+ :aria-label="!title || chromeless || fullbleed ? title || undefined : undefined"
120
134
  @click.stop
121
135
  >
122
- <header v-if="title && !chromeless" class="fe-modal__head">
136
+ <header v-if="title && !chromeless && !fullbleed" class="fe-modal__head">
123
137
  <h2 :id="titleId" class="fe-modal__title">{{ title }}</h2>
124
138
  <button
125
139
  type="button"
@@ -131,7 +145,7 @@ function onBackdrop() {
131
145
  <div class="fe-modal__body">
132
146
  <slot />
133
147
  </div>
134
- <footer v-if="$slots.actions && !chromeless" class="fe-modal__actions">
148
+ <footer v-if="$slots.actions && !chromeless && !fullbleed" class="fe-modal__actions">
135
149
  <slot name="actions" />
136
150
  </footer>
137
151
  </div>