@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
@@ -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 { fileIconSvg } from '../lib/fileIcons';
31
+ import { fileIconTile, typeLabelFor } from '../lib/fileIcons';
32
+ import { actionIconSvg } from '../lib/actionIcons';
33
+ import TagPicker from './TagPicker.vue';
32
34
 
33
- const props = defineProps<{
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(() => props.locale);
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
- if (!ms) return '—';
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
- <h2 class="fe-inspector__title">{{ t('inspector.title') }}</h2>
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-inspector__close"
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
- >×</button>
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
- <div v-if="isMulti" class="fe-inspector__hero">
524
- <span class="fe-inspector__bigicon" v-html="fileIconSvg({ type: 'dir' })"></span>
525
- <p class="fe-inspector__name">
526
- {{ t('inspector.items_summary', { n: nodes.length, size: formatSize(multiTotal) }) }}
527
- </p>
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
- >⧉</button>
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
- >⧉</button>
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 → current folder summary -->
593
- <div v-else class="fe-inspector__hero">
594
- <span class="fe-inspector__bigicon" v-html="fileIconSvg({ type: 'dir' })"></span>
595
- <p class="fe-inspector__name" :title="dirLabel">{{ dirLabel }}</p>
596
- <p class="fe-inspector__sub">{{ t('inspector.folder_items', { n: dirCount }) }}</p>
597
- </div>
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
- <span class="fe-inspector__linkicon" aria-hidden="true">🔗</span>
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
- >⧉</button>
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>