@brftech/filex-core 0.39.1 → 0.41.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.
- package/README.md +32 -25
- package/dist/ArchiveViewer-BNqukFg8.js +124 -0
- package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
- package/dist/CsvViewer-ii_-MgmG.js +141 -0
- package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
- package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
- package/dist/EpubViewer-p4B8iiWb.js +163 -0
- package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
- package/dist/IpynbViewer-D_qYRJhu.js +184 -0
- package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
- package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
- package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
- package/dist/PsdViewer-CuV-OPSd.js +122 -0
- package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
- package/dist/TiffViewer-BLeoF4Be.js +142 -0
- package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
- package/dist/Viewer3D-DHhhU76E.js +75 -0
- package/dist/Viewer3D-DHhhU76E.js.map +1 -0
- package/dist/filex-core.js +217 -20077
- 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-BraG7Cz4.js +26664 -0
- package/dist/index-BraG7Cz4.js.map +1 -0
- package/dist/index.d.ts +4276 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2170 -492
- 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 +57 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +939 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +162 -30
- package/src/components/GridView.vue +322 -58
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1158 -167
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +34 -5
- 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 +43 -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 +127 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +16 -4
- package/src/components/Toolbar.vue +965 -469
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +85 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useKeyboardShortcuts.ts +122 -0
- package/src/composables/useLocale.ts +365 -25
- package/src/index.ts +182 -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 +11 -1
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +152 -23
- package/src/lib/timezone.ts +456 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +432 -25
- package/src/locales/tr.ts +425 -25
- 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 +543 -473
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5435 -8
- package/src/styles/variables.css +140 -60
- 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
|
@@ -28,9 +28,12 @@ import type { FileApi, Grant, NodeVersion } from '../composables/useFileApi';
|
|
|
28
28
|
import type { FileNode, ShareInfo } from '../types/FileNode';
|
|
29
29
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
30
30
|
import { useLocale } from '../composables/useLocale';
|
|
31
|
-
import {
|
|
31
|
+
import { fileIconTile, typeLabelFor } from '../lib/fileIcons';
|
|
32
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
33
|
+
import TagPicker from './TagPicker.vue';
|
|
32
34
|
|
|
33
|
-
const props =
|
|
35
|
+
const props = withDefaults(
|
|
36
|
+
defineProps<{
|
|
34
37
|
api: FileApi;
|
|
35
38
|
/** Current selection (empty array → current-folder summary). */
|
|
36
39
|
nodes: FileNode[];
|
|
@@ -40,6 +43,19 @@ const props = defineProps<{
|
|
|
40
43
|
dirCount: number;
|
|
41
44
|
/** RBAC effective level of the current dir ('' = ACL not enforced). */
|
|
42
45
|
dirPerm?: string;
|
|
46
|
+
/**
|
|
47
|
+
* pane:p1 — non-empty ⇒ `nodes` is the LAST selected thing, not what is
|
|
48
|
+
* ticked in the pane the person is looking at; the value NAMES where it
|
|
49
|
+
* lives.
|
|
50
|
+
*
|
|
51
|
+
* ⚠ The panel holds its subject on purpose (owner, 2026-09-13: "son seçilen
|
|
52
|
+
* şeyi tutsun"), which means it can be describing a file in the other half of
|
|
53
|
+
* a split window — so it has to SAY so. Without this line the panel would be
|
|
54
|
+
* a fact with no address: three sections about `report.pdf` over a listing
|
|
55
|
+
* with nothing ticked in it, and no way to tell whether that is the file you
|
|
56
|
+
* just clicked or the one you clicked five minutes ago.
|
|
57
|
+
*/
|
|
58
|
+
heldIn?: string;
|
|
43
59
|
locale: LocaleCode;
|
|
44
60
|
/** Narrow/embed mode → full-size overlay presentation. */
|
|
45
61
|
narrow?: boolean;
|
|
@@ -61,7 +77,42 @@ const props = defineProps<{
|
|
|
61
77
|
* would be worse than the two real feeds.
|
|
62
78
|
*/
|
|
63
79
|
tabs?: boolean;
|
|
64
|
-
|
|
80
|
+
/* === etiket:t1 — the Tags section ==================================
|
|
81
|
+
*
|
|
82
|
+
* The three props a self-fetching child in this package takes, passed
|
|
83
|
+
* straight through to `TagPicker`: it talks to
|
|
84
|
+
* `/api/files/manager/tags` itself and needs all three to do it.
|
|
85
|
+
*
|
|
86
|
+
* ⚠ `authCredentials` is not decoration. A credentialed cross-origin
|
|
87
|
+
* request cannot be answered with `Access-Control-Allow-Origin: *`, so
|
|
88
|
+
* an embed served from a different origin to the API loses its tags
|
|
89
|
+
* without it — the same reason TagPicker's own prop documents.
|
|
90
|
+
*
|
|
91
|
+
* All three optional: a host that passes none gets no Tags section
|
|
92
|
+
* rather than a control that reads nothing. That is the honest failure
|
|
93
|
+
* for this panel, which hides every section it cannot fill.
|
|
94
|
+
*/
|
|
95
|
+
/** API origin for the tag routes. `''` is valid — it means "same origin". */
|
|
96
|
+
apiBase?: string;
|
|
97
|
+
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
98
|
+
authCredentials?: RequestCredentials;
|
|
99
|
+
}>(),
|
|
100
|
+
{
|
|
101
|
+
/* ⚠ ON by default, and that is the owner's call of 2026-09-12, not a
|
|
102
|
+
* taste: the Details/Activity split, "People with access" and the share
|
|
103
|
+
* link row used to arrive only with `uiProfile: 'drive'`, which meant the
|
|
104
|
+
* screen the owner actually uses — the admin one — was the only screen
|
|
105
|
+
* that never got them ("their app and our app will be one to one";
|
|
106
|
+
* web/src/views/Explore.vue carries the full quote). The profile string
|
|
107
|
+
* is gone; this is the last gate that answered to it, so it answers yes.
|
|
108
|
+
* An embedder that wants the old flat scroll can still pass `:tabs="false"`.
|
|
109
|
+
*/
|
|
110
|
+
tabs: true,
|
|
111
|
+
narrow: false,
|
|
112
|
+
dirPerm: '',
|
|
113
|
+
thumbSrc: undefined,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
65
116
|
|
|
66
117
|
const emit = defineEmits<{
|
|
67
118
|
(e: 'close'): void;
|
|
@@ -71,9 +122,31 @@ const emit = defineEmits<{
|
|
|
71
122
|
(e: 'toast', message: string): void;
|
|
72
123
|
/** Fired after a successful restore/snapshot so the host can reload. */
|
|
73
124
|
(e: 'changed'): void;
|
|
125
|
+
/**
|
|
126
|
+
* etiket:t1 — tags on this node were edited here.
|
|
127
|
+
*
|
|
128
|
+
* ⚠ The host drops its cached tag list on this, refreshes the panel's
|
|
129
|
+
* Tags section and reloads an open tag view — exactly what the
|
|
130
|
+
* context-menu tag modal already causes. Without the emit the edit
|
|
131
|
+
* lands on the server and the sidebar keeps showing the list from
|
|
132
|
+
* before it, which is the kind of staleness nobody notices until they
|
|
133
|
+
* go looking for the tag they just made.
|
|
134
|
+
*/
|
|
135
|
+
(e: 'tags-changed', tags: string[]): void;
|
|
136
|
+
/**
|
|
137
|
+
* etiket:t1 — a tag chip was clicked: open that tag's view.
|
|
138
|
+
*
|
|
139
|
+
* ⚠ Re-emitted rather than handled, like every other navigation this panel
|
|
140
|
+
* offers. The panel knows a tag's NAME; only the host knows that the tag view
|
|
141
|
+
* is a `.tag~<name>` sentinel, how to leave whatever view is on screen to get
|
|
142
|
+
* there, and what that does to the tab strip.
|
|
143
|
+
*/
|
|
144
|
+
(e: 'open-tag', tag: string): void;
|
|
74
145
|
}>();
|
|
75
146
|
|
|
76
|
-
const { t, formatSize, nodeDisplayName } = useLocale(
|
|
147
|
+
const { t, formatSize, formatDate: formatDateOf, nodeDisplayName } = useLocale(
|
|
148
|
+
() => props.locale,
|
|
149
|
+
);
|
|
77
150
|
|
|
78
151
|
// ── selection shape ──────────────────────────────────────────────────
|
|
79
152
|
const single = computed<FileNode | null>(() =>
|
|
@@ -95,20 +168,100 @@ const thumb = computed<string | null>(() =>
|
|
|
95
168
|
single.value && isFile.value && props.thumbSrc ? props.thumbSrc(single.value) : null,
|
|
96
169
|
);
|
|
97
170
|
|
|
171
|
+
/* === gorunum:v4-dialogs — what the head says =============================
|
|
172
|
+
*
|
|
173
|
+
* The panel used to open with the word "Details" and put the item's own name
|
|
174
|
+
* a section lower, under a second heading. The reference shell puts the item
|
|
175
|
+
* in the head — its glyph, its name, and one caption naming what it is — and
|
|
176
|
+
* that is the right way round: the panel is ABOUT the item, and a header that
|
|
177
|
+
* names the panel instead of its subject is a label on the frame.
|
|
178
|
+
*
|
|
179
|
+
* All three fall back cleanly: a multi-selection is counted, no selection at
|
|
180
|
+
* all describes the folder being viewed. Nothing here invents a fact — the
|
|
181
|
+
* caption is the same kind name the listing's Type column prints
|
|
182
|
+
* (`typeLabelFor`) and the same size formatter the rows use.
|
|
183
|
+
* ---------------------------------------------------------------------- */
|
|
184
|
+
|
|
185
|
+
/** The name in the head: the item, the count, or the folder you are in. */
|
|
186
|
+
const headName = computed<string>(() => {
|
|
187
|
+
if (isMulti.value) return t('inspector.items', { n: props.nodes.length });
|
|
188
|
+
if (single.value) return nodeDisplayName(single.value);
|
|
189
|
+
return props.dirLabel;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/** The line under it — "Folder", "TypeScript · 4.8 KB", "12.4 MB in total". */
|
|
193
|
+
const headCaption = computed<string>(() => {
|
|
194
|
+
if (isMulti.value) return formatSize(multiTotal.value);
|
|
195
|
+
const n = single.value;
|
|
196
|
+
if (n) {
|
|
197
|
+
const kind = typeLabelFor(n, t);
|
|
198
|
+
if (n.type === 'dir') return kind;
|
|
199
|
+
const size = typeof n.size === 'number' ? formatSize(n.size) : '';
|
|
200
|
+
return size ? `${kind} · ${size}` : kind;
|
|
201
|
+
}
|
|
202
|
+
return t('inspector.folder_items', { n: props.dirCount });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Whether the thumbnail is worth the space it takes.
|
|
207
|
+
*
|
|
208
|
+
* ⚠ `thumbSrc` answers for text files too — the host renders the source and
|
|
209
|
+
* hands back a picture of it — and at panel width that came out as a blank
|
|
210
|
+
* olive rectangle with "TS" in the middle of it: a large, prominent block
|
|
211
|
+
* that tells the reader less than the one-word caption already did. A
|
|
212
|
+
* thumbnail earns its place when the file IS a picture; for everything else
|
|
213
|
+
* the type tile and the kind name are the whole of what an image could say.
|
|
214
|
+
*/
|
|
215
|
+
const previewSrc = computed<string | null>(() =>
|
|
216
|
+
thumb.value && /^(image|video)[/]/.test(String(single.value?.mime_type ?? ''))
|
|
217
|
+
? thumb.value
|
|
218
|
+
: null,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The glyph — the LISTING'S tile, not a second drawing of the same idea and
|
|
223
|
+
* not the thumbnail.
|
|
224
|
+
*
|
|
225
|
+
* ⚠ It was the thumbnail first, and that was wrong twice over: the host
|
|
226
|
+
* resolves a thumbnail for text files too, so `app.ts` came out as a 30px
|
|
227
|
+
* smudge of rendered source with no glyph in it at all, and the panel's mark
|
|
228
|
+
* then disagreed with the mark on the row it describes. `fileIconTile` is the
|
|
229
|
+
* same object the row draws, so the head and the row match by construction.
|
|
230
|
+
* The thumbnail is still shown — below, at a size where a picture is a
|
|
231
|
+
* picture.
|
|
232
|
+
*/
|
|
233
|
+
const headIcon = computed<string>(() =>
|
|
234
|
+
fileIconTile(single.value && !isMulti.value ? single.value : { type: 'dir' }),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Whether the Tags section has everything it needs.
|
|
239
|
+
*
|
|
240
|
+
* A node id, because the tag routes are keyed by it (a client-synthesized row
|
|
241
|
+
* — a multi-storage virtual folder — has none), and a host that wired the
|
|
242
|
+
* three props. Missing either, the section is not drawn at all rather than
|
|
243
|
+
* drawn empty: an "Add tag" button that cannot save is worse than no button.
|
|
244
|
+
*/
|
|
245
|
+
const canTag = computed(
|
|
246
|
+
() => nodeId.value != null && props.apiBase !== undefined && !isMulti.value,
|
|
247
|
+
);
|
|
248
|
+
|
|
98
249
|
function shortHash(h: string): string {
|
|
99
250
|
return h.length > 12 ? `${h.slice(0, 12)}…` : h;
|
|
100
251
|
}
|
|
101
252
|
|
|
253
|
+
/**
|
|
254
|
+
* zaman:z1 — the panel prints the SAME string the row behind it prints.
|
|
255
|
+
*
|
|
256
|
+
* This used to be a private `formatDate()` that called `toLocaleString` with
|
|
257
|
+
* no `timeZone` at all, so the details panel answered on the BROWSER's clock
|
|
258
|
+
* while the listing cell four pixels away answered on the zone the viewer
|
|
259
|
+
* chose. It also mapped the locale by hand ('en-GB'), so even in one zone the
|
|
260
|
+
* two disagreed about the shape of a date. Both halves live in
|
|
261
|
+
* `useLocale.formatDate` now; `{ time: true }` is the listing's own variant.
|
|
262
|
+
*/
|
|
102
263
|
function formatDate(ms: number | undefined): string {
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
return new Date(ms).toLocaleString(props.locale === 'en' ? 'en-GB' : 'tr-TR', {
|
|
106
|
-
dateStyle: 'medium',
|
|
107
|
-
timeStyle: 'short',
|
|
108
|
-
});
|
|
109
|
-
} catch {
|
|
110
|
-
return new Date(ms).toISOString();
|
|
111
|
-
}
|
|
264
|
+
return formatDateOf(ms, { time: true }) || '—';
|
|
112
265
|
}
|
|
113
266
|
|
|
114
267
|
function formatDateStr(s: string | undefined | null): string {
|
|
@@ -359,7 +512,7 @@ const activityUsable = computed(
|
|
|
359
512
|
/* === /surucu:d1 === */
|
|
360
513
|
|
|
361
514
|
watch(
|
|
362
|
-
() => props.nodes.map((n) => n.path).join('
|
|
515
|
+
() => props.nodes.map((n) => n.path).join('\u0000'),
|
|
363
516
|
() => void refresh(),
|
|
364
517
|
{ immediate: true },
|
|
365
518
|
);
|
|
@@ -480,15 +633,46 @@ watch(
|
|
|
480
633
|
role="complementary"
|
|
481
634
|
:aria-label="t('inspector.title')"
|
|
482
635
|
>
|
|
636
|
+
<!-- The head is about the ITEM, not about the panel: its glyph, its name
|
|
637
|
+
and one caption naming what it is. "Copy name" is next to the name it
|
|
638
|
+
copies; the close button is last, on the edge. -->
|
|
483
639
|
<header class="fe-inspector__head">
|
|
484
|
-
|
|
640
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
641
|
+
<span class="fe-inspector__head-icon" aria-hidden="true" v-html="headIcon"></span>
|
|
642
|
+
<div class="fe-inspector__head-text">
|
|
643
|
+
<h2 class="fe-inspector__title" :title="headName">{{ headName }}</h2>
|
|
644
|
+
<p class="fe-inspector__caption">{{ headCaption }}</p>
|
|
645
|
+
<!-- pane:p1 — the held-subject line. Drawn ONLY while this panel is
|
|
646
|
+
describing the last selected thing rather than a live selection,
|
|
647
|
+
and it names the folder that thing is in, because with a split
|
|
648
|
+
window that folder may not be the one on screen. `role="note"`:
|
|
649
|
+
it is a fact about the panel, not another fact about the file. -->
|
|
650
|
+
<p v-if="heldIn" class="fe-inspector__held" role="note" data-testid="inspector-held">
|
|
651
|
+
{{ t('inspector.held', { where: heldIn }) }}
|
|
652
|
+
</p>
|
|
653
|
+
</div>
|
|
485
654
|
<button
|
|
655
|
+
v-if="single"
|
|
486
656
|
type="button"
|
|
487
|
-
class="fe-
|
|
657
|
+
class="fe-inspector__iconbtn"
|
|
658
|
+
:title="t('inspector.copy_name')"
|
|
659
|
+
:aria-label="t('inspector.copy_name')"
|
|
660
|
+
data-testid="inspector-copy-name"
|
|
661
|
+
@click="copyText(headName)"
|
|
662
|
+
>
|
|
663
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
664
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
665
|
+
</button>
|
|
666
|
+
<button
|
|
667
|
+
type="button"
|
|
668
|
+
class="fe-inspector__iconbtn fe-inspector__close"
|
|
488
669
|
:title="t('inspector.close')"
|
|
489
670
|
:aria-label="t('inspector.close')"
|
|
490
671
|
@click="emit('close')"
|
|
491
|
-
|
|
672
|
+
>
|
|
673
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
674
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
675
|
+
</button>
|
|
492
676
|
</header>
|
|
493
677
|
|
|
494
678
|
<!-- surucu:d1 — Details / Activity. Rendered only in the drive shell; the
|
|
@@ -519,35 +703,40 @@ watch(
|
|
|
519
703
|
<section v-if="!tabs || tab === 'details'" class="fe-inspector__section">
|
|
520
704
|
<h3 class="fe-inspector__heading">{{ t('inspector.section.general') }}</h3>
|
|
521
705
|
|
|
522
|
-
<!-- Multi selection → summary
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
<
|
|
526
|
-
{{ t('inspector.
|
|
527
|
-
|
|
528
|
-
</div>
|
|
529
|
-
|
|
530
|
-
<!-- Single selection → full meta -->
|
|
531
|
-
<template v-else-if="single">
|
|
532
|
-
<div class="fe-inspector__hero">
|
|
533
|
-
<img
|
|
534
|
-
v-if="thumb"
|
|
535
|
-
class="fe-inspector__thumb"
|
|
536
|
-
:src="thumb"
|
|
537
|
-
alt=""
|
|
538
|
-
aria-hidden="true"
|
|
539
|
-
/>
|
|
540
|
-
<span
|
|
541
|
-
v-else
|
|
542
|
-
class="fe-inspector__bigicon"
|
|
543
|
-
v-html="fileIconSvg(single)"
|
|
544
|
-
></span>
|
|
545
|
-
<p class="fe-inspector__name" :title="single.basename">
|
|
546
|
-
{{ nodeDisplayName(single) }}
|
|
547
|
-
</p>
|
|
706
|
+
<!-- Multi selection → summary. The head already counts them, so this
|
|
707
|
+
is the one fact the head does not carry. -->
|
|
708
|
+
<dl v-if="isMulti" class="fe-inspector__meta">
|
|
709
|
+
<div class="fe-inspector__row">
|
|
710
|
+
<dt>{{ t('inspector.size') }}</dt>
|
|
711
|
+
<dd>{{ formatSize(multiTotal) }}</dd>
|
|
548
712
|
</div>
|
|
713
|
+
</dl>
|
|
549
714
|
|
|
715
|
+
<!-- Single selection → full meta.
|
|
716
|
+
⚠ No Owner row. There is no per-node owner on the wire
|
|
717
|
+
(`handlers/shared.go`: "There is no per-node owner"), so the
|
|
718
|
+
reference shell's "Owner: You" would be a constant wearing the
|
|
719
|
+
shape of a fact. -->
|
|
720
|
+
<template v-else-if="single">
|
|
721
|
+
<!-- The thumbnail, when the host resolved one. Wide and short: this
|
|
722
|
+
is the one place in the panel where a picture beats a word, and
|
|
723
|
+
the 30px square in the head is not that place.
|
|
724
|
+
⚠ INSIDE this branch, not before it: a `v-if` sibling between
|
|
725
|
+
`v-if="isMulti"` and this `v-else-if` silently captures the
|
|
726
|
+
else — which is exactly what happened, and the whole General
|
|
727
|
+
table stopped rendering while the heading above it stayed. -->
|
|
728
|
+
<img
|
|
729
|
+
v-if="previewSrc"
|
|
730
|
+
class="fe-inspector__preview"
|
|
731
|
+
:src="previewSrc"
|
|
732
|
+
alt=""
|
|
733
|
+
aria-hidden="true"
|
|
734
|
+
/>
|
|
550
735
|
<dl class="fe-inspector__meta">
|
|
736
|
+
<div class="fe-inspector__row">
|
|
737
|
+
<dt>{{ t('inspector.type') }}</dt>
|
|
738
|
+
<dd>{{ typeLabelFor(single, t) }}</dd>
|
|
739
|
+
</div>
|
|
551
740
|
<div class="fe-inspector__row">
|
|
552
741
|
<dt>{{ t('inspector.path') }}</dt>
|
|
553
742
|
<dd class="fe-inspector__pathcell">
|
|
@@ -558,7 +747,10 @@ watch(
|
|
|
558
747
|
:title="t('inspector.copy')"
|
|
559
748
|
:aria-label="t('inspector.copy')"
|
|
560
749
|
@click="copyText(single.path)"
|
|
561
|
-
|
|
750
|
+
>
|
|
751
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
752
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
753
|
+
</button>
|
|
562
754
|
</dd>
|
|
563
755
|
</div>
|
|
564
756
|
<div v-if="isFile" class="fe-inspector__row">
|
|
@@ -573,6 +765,30 @@ watch(
|
|
|
573
765
|
<dt>{{ t('inspector.mime') }}</dt>
|
|
574
766
|
<dd class="fe-inspector__mime">{{ single.mime_type }}</dd>
|
|
575
767
|
</div>
|
|
768
|
+
<!-- ⚠ The node id lives HERE and nowhere else (owner's call,
|
|
769
|
+
2026-09-13). It used to be a row in the right-click menu and the
|
|
770
|
+
selection bar — a developer's handle on a support ticket sitting
|
|
771
|
+
in front of everyone, twice. This panel is where the other
|
|
772
|
+
technical facts about a file already are (Path, MIME, ETag), and
|
|
773
|
+
it is still one click to copy. Drawn only when the row actually
|
|
774
|
+
has one: a client-synthesized row (a multi-storage folder) has
|
|
775
|
+
no backend id, and an empty "ID —" teaches nothing. -->
|
|
776
|
+
<div v-if="typeof single.id === 'number'" class="fe-inspector__row">
|
|
777
|
+
<dt>{{ t('inspector.nodeId') }}</dt>
|
|
778
|
+
<dd class="fe-inspector__pathcell">
|
|
779
|
+
<span class="fe-inspector__path">{{ single.id }}</span>
|
|
780
|
+
<button
|
|
781
|
+
type="button"
|
|
782
|
+
class="fe-inspector__copy"
|
|
783
|
+
:title="t('inspector.copy')"
|
|
784
|
+
:aria-label="t('inspector.copy')"
|
|
785
|
+
@click="copyText(String(single.id))"
|
|
786
|
+
>
|
|
787
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
788
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
789
|
+
</button>
|
|
790
|
+
</dd>
|
|
791
|
+
</div>
|
|
576
792
|
<div v-if="etag" class="fe-inspector__row">
|
|
577
793
|
<dt>{{ t('inspector.etag') }}</dt>
|
|
578
794
|
<dd class="fe-inspector__pathcell">
|
|
@@ -583,18 +799,41 @@ watch(
|
|
|
583
799
|
:title="t('inspector.copy')"
|
|
584
800
|
:aria-label="t('inspector.copy')"
|
|
585
801
|
@click="copyText(etag)"
|
|
586
|
-
|
|
802
|
+
>
|
|
803
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
804
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
805
|
+
</button>
|
|
587
806
|
</dd>
|
|
588
807
|
</div>
|
|
589
808
|
</dl>
|
|
590
809
|
</template>
|
|
591
810
|
|
|
592
|
-
<!-- No selection →
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
811
|
+
<!-- No selection → nothing more to say. The head already names the
|
|
812
|
+
folder you are in and counts what is in it. -->
|
|
813
|
+
<p v-else class="fe-inspector__empty">{{ t('inspector.select_hint') }}</p>
|
|
814
|
+
</section>
|
|
815
|
+
|
|
816
|
+
<!-- ══ etiket:t1 — Etiketler ══
|
|
817
|
+
The SAME `TagPicker` the context menu opens in a modal, mounted
|
|
818
|
+
inline. Not a second tag editor: one component, so the chips, the
|
|
819
|
+
colours and the add flow are identical wherever tags are edited,
|
|
820
|
+
and a fix to any of them reaches both places at once. -->
|
|
821
|
+
<section
|
|
822
|
+
v-if="canTag && (!tabs || tab === 'details')"
|
|
823
|
+
class="fe-inspector__section"
|
|
824
|
+
data-testid="inspector-tags"
|
|
825
|
+
>
|
|
826
|
+
<h3 class="fe-inspector__heading">{{ t('inspector.section.tags') }}</h3>
|
|
827
|
+
<TagPicker
|
|
828
|
+
:node-id="nodeId as number"
|
|
829
|
+
:locale="locale"
|
|
830
|
+
:api-base="apiBase"
|
|
831
|
+
:auth-headers="authHeaders"
|
|
832
|
+
:auth-credentials="authCredentials"
|
|
833
|
+
@change="(tags: string[]) => emit('tags-changed', tags)"
|
|
834
|
+
@open="(tag: string) => emit('open-tag', tag)"
|
|
835
|
+
@error="() => emit('toast', t('inspector.error'))"
|
|
836
|
+
/>
|
|
598
837
|
</section>
|
|
599
838
|
|
|
600
839
|
<!-- ══ Sürümler ══ -->
|
|
@@ -736,7 +975,8 @@ watch(
|
|
|
736
975
|
button that changes it. Only when there is no link yet; the list
|
|
737
976
|
below is what an item that HAS links has always shown. -->
|
|
738
977
|
<div v-if="tabs && shares.length === 0" class="fe-inspector__linkrow">
|
|
739
|
-
|
|
978
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
979
|
+
<span class="fe-inspector__linkicon" aria-hidden="true" v-html="actionIconSvg('link')"></span>
|
|
740
980
|
<span class="fe-inspector__linknone">{{ t('inspector.link.none') }}</span>
|
|
741
981
|
<button
|
|
742
982
|
type="button"
|
|
@@ -762,7 +1002,10 @@ watch(
|
|
|
762
1002
|
:title="t('inspector.shares.copy')"
|
|
763
1003
|
:aria-label="t('inspector.shares.copy')"
|
|
764
1004
|
@click="copyText(s.url)"
|
|
765
|
-
|
|
1005
|
+
>
|
|
1006
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
1007
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
1008
|
+
</button>
|
|
766
1009
|
</li>
|
|
767
1010
|
</ul>
|
|
768
1011
|
</section>
|