@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.
- package/README.md +32 -25
- package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
- package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
- package/dist/CsvViewer-CyuwENI_.js +141 -0
- package/dist/CsvViewer-CyuwENI_.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-BIYsTX97.js} +48 -42
- package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
- package/dist/EpubViewer-K9PjCMmS.js +163 -0
- package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
- package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
- package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
- package/dist/MermaidViewer-Bp73aId8.js +137 -0
- package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
- package/dist/PsdViewer-C2eLivw1.js +122 -0
- package/dist/PsdViewer-C2eLivw1.js.map +1 -0
- package/dist/TiffViewer-DsHg5gcz.js +142 -0
- package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
- package/dist/Viewer3D-C_dc2HN9.js +75 -0
- package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
- package/dist/filex-core.js +218 -20361
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-vxMZpYb-.js +26700 -0
- package/dist/index-vxMZpYb-.js.map +1 -0
- package/dist/index.d.ts +4212 -862
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2154 -506
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +39 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +951 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +172 -56
- package/src/components/GridView.vue +331 -82
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1173 -194
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +26 -1
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +6 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +135 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +35 -14
- package/src/components/Toolbar.vue +953 -465
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +89 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useLocale.ts +382 -26
- package/src/composables/useRowTouch.ts +95 -0
- package/src/index.ts +177 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +31 -2
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +151 -116
- package/src/lib/timezone.ts +501 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +416 -26
- package/src/locales/tr.ts +409 -26
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +557 -476
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5409 -8
- package/src/styles/variables.css +139 -65
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- 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
|
|
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
|
|
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 {
|
|
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>
|
package/src/modals/Modal.vue
CHANGED
|
@@ -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>
|