@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
@@ -33,8 +33,26 @@ import { useLocale } from '../composables/useLocale';
33
33
  import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
34
34
  import ContextMenu, { type ContextAction } from './ContextMenu.vue';
35
35
 
36
- /** The virtual listings the panel can open. '' = an ordinary folder. */
37
- export type NavView = '' | 'recent' | 'starred' | 'shared' | 'trash' | 'tag';
36
+ /** The virtual listings the panel can open. '' = an ordinary folder.
37
+ * ⚠ `home` is not a listing: it is the overview (storages · recent · starred)
38
+ * the reference shell lands on, and the explorer renders it instead of a file
39
+ * list. It is in this union because the panel's selected-row logic, the
40
+ * address bar and the tab strip all key off ONE type — a second mechanism for
41
+ * "which destination am I on" is how two of them end up disagreeing. */
42
+ export type NavView = '' | 'home' | 'recent' | 'starred' | 'shared' | 'trash' | 'tag';
43
+
44
+ /**
45
+ * A row in the panel's first group — what `open-view` carries.
46
+ *
47
+ * ⚠ It is NOT `NavView`. "My files" is a destination without being a view: it
48
+ * opens the root LISTING, so the explorer answers it with an ordinary
49
+ * navigation and `activeView` goes back to `''`. Giving it a NavView value
50
+ * would mean a mode that every `if (navView)` in the explorer — "is this a
51
+ * cross-folder view?", "is there a folder to upload into?", "what does Up do?"
52
+ * — would answer wrongly, for a row whose whole job is to be an ordinary
53
+ * folder again.
54
+ */
55
+ export type NavDest = Exclude<NavView, '' | 'tag'> | 'myfiles';
38
56
 
39
57
  export interface NavStorage {
40
58
  name: string;
@@ -99,24 +117,28 @@ const props = defineProps<{
99
117
  /** RBAC/root state — false hides the write affordances. */
100
118
  canWrite?: boolean;
101
119
  locale: LocaleCode;
102
- /* === surucu:d1 — the Drive shell ==================================== */
120
+ /* === surucu:d1 — the shell ========================================== */
103
121
  /**
104
- * Fold the primary actions into ONE "+ New" menu (`uiProfile: 'drive'`).
105
- *
106
- * Absent/false keeps the two-button block every other profile has had since
107
- * v0.30.1 Upload as the primary, New folder one step quieter so this is
108
- * additive and nothing that mounts the package today moves.
122
+ * There is no `newMenu` prop any more. The two-button block (Upload as the
123
+ * primary, New folder one step quieter) is gone and the "+ New" menu is what
124
+ * this panel draws, everywhere — it is the shell, not a profile. Nothing was
125
+ * removed: the menu holds what the explorer could already doupload files,
126
+ * make a folder (the modal offers the encrypted variant from inside itself),
127
+ * and ask somebody else for files.
109
128
  *
110
- * The menu holds what the explorer can ALREADY do: upload files, make a
111
- * folder (the modal offers the encrypted variant from inside itself), and
112
- * ask somebody else for files. There is deliberately no "Upload folder": the
113
- * upload path takes a flat `File[]` and would have to create the intermediate
114
- * directories itself, and an entry that quietly flattens someone's folder into
115
- * one heap is worse than an entry that is not there.
129
+ * There is deliberately no "Upload folder": the upload path takes a flat
130
+ * `File[]` and would have to create the intermediate directories itself, and
131
+ * an entry that quietly flattens someone's folder into one heap is worse
132
+ * than an entry that is not there.
116
133
  */
117
- newMenu?: boolean;
118
134
  /** Offer "Request files" in that menu — a folder we may write to and share. */
119
135
  canRequestFiles?: boolean;
136
+ /**
137
+ * belge:n1 — offer "New document" in that menu. False on a server that
138
+ * publishes no `newdoc_types`, and on one where every type it publishes
139
+ * needs an editor service this deployment has not got.
140
+ */
141
+ canNewDocument?: boolean;
120
142
  /**
121
143
  * The signed-in person's storage line, from `GET /api/files/quota/me`. Null
122
144
  * (the default) renders nothing at all.
@@ -134,12 +156,13 @@ const props = defineProps<{
134
156
 
135
157
  const emit = defineEmits<{
136
158
  (e: 'toggle'): void;
137
- (e: 'open-view', view: Exclude<NavView, '' | 'tag'>): void;
159
+ (e: 'open-view', view: NavDest): void;
138
160
  (e: 'open-tag', tag: string): void;
139
161
  (e: 'open-storage', name: string): void;
140
162
  (e: 'open-root'): void;
141
163
  (e: 'upload'): void;
142
164
  (e: 'new-folder'): void;
165
+ (e: 'new-document'): void;
143
166
  (e: 'open-connections'): void;
144
167
  (e: 'open-tokens'): void;
145
168
  /* surucu:d1 — "Request files": the access modal on THIS folder, drop tab. */
@@ -148,7 +171,7 @@ const emit = defineEmits<{
148
171
  (e: 'close'): void;
149
172
  }>();
150
173
 
151
- const { t } = useLocale(() => props.locale);
174
+ const { t, formatSize } = useLocale(() => props.locale);
152
175
 
153
176
  // In drawer mode "expanded" is the only meaningful state: a rail inside an
154
177
  // overlay would be an overlay that shows nothing but icons while covering the
@@ -160,11 +183,54 @@ const sharedSet = computed(() => new Set(props.sharedStorages ?? []));
160
183
  /** The views that answer "what did *I* do" — dropped for an app token. */
161
184
  const IDENTITY_VIEWS = new Set<string>(['recent', 'starred', 'shared']);
162
185
 
186
+ /**
187
+ * gorunum:v3-shell — the destinations, and their ORDER.
188
+ *
189
+ * Home · My files · Shared with me · Recent · Starred · Trash. The order is
190
+ * not a taste: it runs from the widest answer to the narrowest — everything
191
+ * you have, then your own tree, then somebody else's, then two slices of your
192
+ * own, then what you threw away. The panel used to open with Recent, which
193
+ * put a slice of the tree above the tree.
194
+ *
195
+ * ⚠⚠ "My files" is drawn ONLY where it can honestly mean "my files" — a
196
+ * deployment whose caller can see at most ONE storage. There it opens that
197
+ * storage's root and the label is the truth (measured: `soleStorageName` in
198
+ * FileExplorer sends `load('')` straight to the storage, so the row lands on
199
+ * the file list, exactly as the reference build's own "My files" does).
200
+ *
201
+ * With SEVERAL storages it cannot mean that, and what it actually did was open
202
+ * the multi-storage root — a listing OF THE DRIVES. Owner, 2026-09-13: "my
203
+ * files kısmında yüklediğimiz dosyalar gelmesi lazım ama onun yerine storages
204
+ * gösteren ana bölge geliyor… direk silebiliriz." Three reasons it goes rather
205
+ * than gets repointed:
206
+ *
207
+ * 1. it was the THIRD copy of that list. Home's "Storages" section draws the
208
+ * same drives as cards with a size caption, and this panel's own STORAGES
209
+ * group draws them one click away — and all three are on screen together.
210
+ * "Aynı işlevi yapan iki buton olmaması lazım."
211
+ * 2. it was the worst of the three. The drives are synthesized rows, so the
212
+ * listing shows them under Type / Modified / Size columns that are all
213
+ * "—", owner "System", and with no filter row at all.
214
+ * 3. nothing is stranded. Measured in the browser: the breadcrumb's house
215
+ * crumb (`crumbs[0]`, `adapterPath: ''`) and Alt+↑ from a storage root
216
+ * both land on exactly that listing, and both leave the panel in the same
217
+ * state this row used to.
218
+ *
219
+ * ⚠ `<= 1`, not `=== 1`: a caller who can see NO storage keeps the row, so an
220
+ * account with nothing mounted still has a door back to the root listing and
221
+ * its "no storages" message, rather than a panel of views with no files in it.
222
+ * ⚠ It is still not an identity view — where it IS drawn it stays drawn for an
223
+ * app token, which has no Recent and no Starred to escape through.
224
+ */
163
225
  const views = computed(() => {
164
- const list: Array<{ key: Exclude<NavView, '' | 'tag'>; label: string }> = [
226
+ const list: Array<{ key: NavDest; label: string }> = [
227
+ { key: 'home', label: t('sidenav.home') },
228
+ ...(props.storages.length <= 1
229
+ ? [{ key: 'myfiles' as NavDest, label: t('sidenav.myfiles') }]
230
+ : []),
231
+ { key: 'shared', label: t('sidenav.shared') },
165
232
  { key: 'recent', label: t('sidenav.recent') },
166
233
  { key: 'starred', label: t('sidenav.starred') },
167
- { key: 'shared', label: t('sidenav.shared') },
168
234
  ];
169
235
  if (props.trashVisible !== false) list.push({ key: 'trash', label: t('sidenav.trash') });
170
236
  // Filtered at the end rather than built conditionally: Trash is shared by
@@ -176,6 +242,21 @@ const views = computed(() => {
176
242
 
177
243
  const writable = computed(() => props.canWrite !== false);
178
244
 
245
+ /**
246
+ * Which row reads as the one you are standing on.
247
+ *
248
+ * Every view answers for itself. "My files" is the exception, because it has
249
+ * no view value to compare against: it is lit when the explorer is showing the
250
+ * ROOT of the tree and nothing else — no virtual view, and no storage opened,
251
+ * which is the state where the storage rows below all read as inactive too.
252
+ * Inside a storage the storage's own row is the active one, so lighting both
253
+ * would claim the selection is in two places.
254
+ */
255
+ function isActiveDest(key: NavDest): boolean {
256
+ if (key === 'myfiles') return !props.activeView && !props.activeStorage;
257
+ return props.activeView === key;
258
+ }
259
+
179
260
  /* === etiket:t1 — an unbounded list in a fixed panel ====================
180
261
  * A user with sixty tags must not push Storages and Connections off the
181
262
  * bottom of the panel, and must not be handed sixty identical glyphs on a
@@ -212,9 +293,29 @@ const newMenuRef = ref<InstanceType<typeof ContextMenu> | null>(null);
212
293
 
213
294
  const newActions = computed<ContextAction[]>(() => {
214
295
  const list: ContextAction[] = [
215
- { key: 'upload', label: t('drive.new.upload'), icon: '⬆', disabled: !writable.value },
216
- { key: 'new-folder', label: t('drive.new.folder'), icon: '📁', disabled: !writable.value },
296
+ /* NO `icon:` on any of these three, and that is the fix rather than an
297
+ omission. `ContextMenu.iconFor` resolves `actionIconSvg(a.icon || a.key)`,
298
+ so an `icon` string is an OVERRIDE — and `'⬆'` / `'📁'` / `'🔗'` are not
299
+ keys, so the lookup missed, the component fell through to its literal
300
+ branch and printed the emoji. Meanwhile `new-document`, which carries no
301
+ override, resolved by key and drew a proper glyph: one menu, two icon
302
+ systems, three rows apart. All three keys have had stroked glyphs in
303
+ `lib/actionIcons.ts` the whole time (`upload`, `new-folder`,
304
+ `request-files`); dropping the override is what reaches them.
305
+ ⚠ Emoji in markup is forbidden here for a reason that is visible in this
306
+ very menu: they are rendered by whatever font the OS ships, so the row
307
+ came out flat-grey on one machine and full-colour on another, at a weight
308
+ that matched nothing beside it. */
309
+ { key: 'upload', label: t('drive.new.upload'), disabled: !writable.value },
310
+ { key: 'new-folder', label: t('drive.new.folder'), disabled: !writable.value },
217
311
  ];
312
+ // belge:n1 — DROPPED rather than disabled when unavailable, unlike
313
+ // "Request files": a disabled row invites the question "why?" and cannot
314
+ // answer it. The dialog behind it explains what is missing; a greyed row
315
+ // in a menu has nowhere to say so.
316
+ if (props.canNewDocument) {
317
+ list.push({ key: 'new-document', label: t('drive.new.document'), disabled: !writable.value });
318
+ }
218
319
  // Only when there is a real folder to hang a drop link on. Rendered as a
219
320
  // disabled row rather than dropped, so the menu does not change height
220
321
  // between folders — a menu whose items move is a menu people misclick.
@@ -222,7 +323,6 @@ const newActions = computed<ContextAction[]>(() => {
222
323
  list.push({
223
324
  key: 'request-files',
224
325
  label: t('drive.new.request'),
225
- icon: '🔗',
226
326
  disabled: !props.canRequestFiles,
227
327
  });
228
328
  return list;
@@ -238,23 +338,17 @@ function openNewMenu() {
238
338
 
239
339
  function onNewSelect(a: ContextAction) {
240
340
  if (a.key === 'upload') emit('upload');
341
+ else if (a.key === 'new-document') emit('new-document');
241
342
  else if (a.key === 'new-folder') emit('new-folder');
242
343
  else if (a.key === 'request-files') emit('request-files');
243
344
  }
244
345
 
245
- /* === surucu:d1 — the storage line =================================== */
246
- function formatBytes(n: number): string {
247
- if (!Number.isFinite(n) || n < 0) return '—';
248
- const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
249
- let v = n;
250
- let i = 0;
251
- while (v >= 1024 && i < units.length - 1) {
252
- v /= 1024;
253
- i += 1;
254
- }
255
- const digits = v < 10 && i > 0 ? 1 : 0;
256
- return `${v.toFixed(digits)} ${units[i]}`;
257
- }
346
+ /* === surucu:d1 — the storage line ===================================
347
+ * The size comes from `useLocale.formatSize`, the same call `HomeView` makes
348
+ * for the same `drive.storage.*` string. There was a private byte formatter
349
+ * here with hardcoded English units and its own rounding, so one quota
350
+ * rendered two ways — "9.3 GB" in this rail, "9.31 GB" on the home screen,
351
+ * and "10 GB" in the admin panel that set it. */
258
352
 
259
353
  const quotaPercent = computed(() => {
260
354
  const q = props.quota;
@@ -266,17 +360,12 @@ const quotaText = computed(() => {
266
360
  const q = props.quota;
267
361
  if (!q) return '';
268
362
  return q.unlimited || q.total <= 0
269
- ? t('drive.storage.used_unlimited', { used: formatBytes(q.used) })
270
- : t('drive.storage.used', { used: formatBytes(q.used), total: formatBytes(q.total) });
363
+ ? t('drive.storage.used_unlimited', { used: formatSize(q.used) })
364
+ : t('drive.storage.used', { used: formatSize(q.used), total: formatSize(q.total) });
271
365
  });
272
366
 
273
- const toggleLabel = computed(() =>
274
- props.narrow
275
- ? t('sidenav.close')
276
- : props.expanded
277
- ? t('sidenav.collapse')
278
- : t('sidenav.expand'),
279
- );
367
+ /** Only the drawer draws this control now (see the template). */
368
+ const toggleLabel = computed(() => t('sidenav.close'));
280
369
  </script>
281
370
 
282
371
  <template>
@@ -290,7 +379,16 @@ const toggleLabel = computed(() =>
290
379
  :aria-label="t('sidenav.title')"
291
380
  data-testid="sidenav"
292
381
  >
293
- <div class="fe-sidenav__head">
382
+ <!-- gorunum:v3-shell — the panel's own edge control is now the DRAWER's
383
+ close button and nothing else.
384
+ ⚠ At 560px and up it is gone, deliberately: the collapse button lives
385
+ at the far left of the top bar (`.fe-toolbar__brand`), above the panel
386
+ rather than inside it, which is where the reference puts it and where
387
+ it still exists when the panel is a 56px rail. Two buttons for one
388
+ verb, one of them inside the thing it collapses, was the duplicate
389
+ this removed. At 390px the panel is an overlay ON TOP of the files, so
390
+ it keeps a dismiss of its own — the same reason a dialog has one. -->
391
+ <div v-if="narrow" class="fe-sidenav__head">
294
392
  <button
295
393
  type="button"
296
394
  class="fe-sidenav__toggle"
@@ -298,13 +396,8 @@ const toggleLabel = computed(() =>
298
396
  :title="toggleLabel"
299
397
  :aria-label="toggleLabel"
300
398
  data-testid="sidenav-toggle"
301
- @click="narrow ? emit('close') : emit('toggle')"
399
+ @click="emit('close')"
302
400
  >
303
- <!-- The glyph says what the control does NEXT, and each state gets
304
- its own: an expanded panel closes (arrow into the sidebar), a rail
305
- opens (arrow out of it), a drawer dismisses (cross). One hamburger
306
- for all three reads as decoration — and this toolbar already
307
- carries two other three-line glyphs. -->
308
401
  <svg
309
402
  class="fe-ficon"
310
403
  viewBox="0 0 24 24"
@@ -316,34 +409,23 @@ const toggleLabel = computed(() =>
316
409
  aria-hidden="true"
317
410
  focusable="false"
318
411
  >
319
- <template v-if="narrow">
320
- <path d="M6 6l12 12M18 6L6 18" />
321
- </template>
322
- <template v-else-if="expanded">
323
- <rect x="3.5" y="4.5" width="17" height="15" rx="2" />
324
- <path d="M9.5 4.5v15" />
325
- <path d="M17 9.5L14.5 12l2.5 2.5" />
326
- </template>
327
- <template v-else>
328
- <rect x="3.5" y="4.5" width="17" height="15" rx="2" />
329
- <path d="M9.5 4.5v15" />
330
- <path d="M14.5 9.5L17 12l-2.5 2.5" />
331
- </template>
412
+ <path d="M6 6l12 12M18 6L6 18" />
332
413
  </svg>
333
414
  </button>
334
415
  </div>
335
416
 
336
- <!-- Primary action. Upload is the thing people come here to do, so in the
337
- panel it reads as the main button rather than one toolbar icon among
338
- fourteen. New folder stays, one step quieter. -->
417
+ <!-- Primary action. Making something is what people come to this panel to
418
+ do, so it reads as the main button rather than one toolbar icon among
419
+ fourteen. -->
339
420
  <!-- Rendered even with nowhere to write, and disabled instead. A block
340
421
  that appears and disappears makes every row below it jump by 90px each
341
422
  time the user opens a view, which reads as the panel reloading. -->
342
423
  <div class="fe-sidenav__primary">
343
- <!-- surucu:d1 — one primary action, the shape #14's mockups draw. The
344
- two-button block below is what every other profile still renders. -->
424
+ <!-- surucu:d1 — ONE primary action, the shape #14's mockups draw, in
425
+ every profile and every embed. The two-button block that used to
426
+ stand here behind `v-if="!newMenu"` (Upload + New folder) is gone:
427
+ both verbs are the first two rows of this menu. -->
345
428
  <button
346
- v-if="newMenu"
347
429
  ref="newBtnEl"
348
430
  type="button"
349
431
  class="fe-sidenav__new"
@@ -353,72 +435,49 @@ const toggleLabel = computed(() =>
353
435
  data-testid="sidenav-new"
354
436
  @click="openNewMenu"
355
437
  >
356
- <svg
357
- class="fe-ficon"
358
- viewBox="0 0 24 24"
359
- fill="none"
360
- stroke="currentColor"
361
- stroke-width="2"
362
- stroke-linecap="round"
363
- aria-hidden="true"
364
- focusable="false"
365
- >
366
- <path d="M12 5v14M5 12h14" />
367
- </svg>
368
- <span v-if="showLabels" class="fe-sidenav__text">{{ t('drive.new') }}</span>
369
- </button>
370
- <button
371
- v-if="!newMenu"
372
- type="button"
373
- class="fe-sidenav__upload"
374
- :disabled="!writable"
375
- :title="t('toolbar.upload')"
376
- :aria-label="t('toolbar.upload')"
377
- data-testid="sidenav-upload"
378
- @click="emit('upload')"
379
- >
380
- <svg
381
- class="fe-ficon"
382
- viewBox="0 0 24 24"
383
- fill="none"
384
- stroke="currentColor"
385
- stroke-width="1.9"
386
- stroke-linecap="round"
387
- stroke-linejoin="round"
388
- aria-hidden="true"
389
- focusable="false"
390
- >
391
- <path d="M12 16V4" />
392
- <path d="M7 9l5-5 5 5" />
393
- <path d="M4 17v2a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-2" />
394
- </svg>
395
- <span v-if="showLabels" class="fe-sidenav__text">{{ t('toolbar.upload') }}</span>
396
- </button>
397
- <button
398
- v-if="!newMenu"
399
- type="button"
400
- class="fe-sidenav__secondary"
401
- :disabled="!writable"
402
- :title="t('toolbar.new_folder')"
403
- :aria-label="t('toolbar.new_folder')"
404
- data-testid="sidenav-new-folder"
405
- @click="emit('new-folder')"
406
- >
407
- <svg
408
- class="fe-ficon"
409
- viewBox="0 0 24 24"
410
- fill="none"
411
- stroke="currentColor"
412
- stroke-width="1.8"
413
- stroke-linecap="round"
414
- stroke-linejoin="round"
415
- aria-hidden="true"
416
- focusable="false"
417
- >
418
- <path d="M3 7.5A1.5 1.5 0 0 1 4.5 6h4l2 2.5h7A1.5 1.5 0 0 1 19 10v7.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 3 17.5z" />
419
- <path d="M12 11.5v5M9.5 14h5" />
420
- </svg>
421
- <span v-if="showLabels" class="fe-sidenav__text">{{ t('toolbar.new_folder') }}</span>
438
+ <!-- ⚠⚠ TWO SPANS AND A DIVIDER, NOT A SPLIT BUTTON. The reference draws
439
+ a label half, a hairline and a chevron half; the owner asked twice
440
+ for the chevron ("new tuşunun yanına caret koymamışsın, onu koymanı
441
+ istiyorum") because a button that opens a menu has to look like
442
+ one. But it stays ONE `<button aria-haspopup="menu">`: a real split
443
+ button needs two focusable regions with two different actions, and
444
+ here both halves do the same thing. Copying the shape without
445
+ copying the mechanism is deliberate — two tab stops that lead to
446
+ one menu is a keyboard user counting controls that do not exist.
447
+ ⚠ The divider is `aria-hidden` decoration; it is 16px inset inside
448
+ a 34px pill, not full height, or it reads as a seam between two
449
+ buttons — which is exactly the thing this is not. -->
450
+ <span class="fe-sidenav__new-main">
451
+ <svg
452
+ class="fe-ficon"
453
+ viewBox="0 0 24 24"
454
+ fill="none"
455
+ stroke="currentColor"
456
+ stroke-width="2"
457
+ stroke-linecap="round"
458
+ aria-hidden="true"
459
+ focusable="false"
460
+ >
461
+ <path d="M12 5v14M5 12h14" />
462
+ </svg>
463
+ <span v-if="showLabels" class="fe-sidenav__text">{{ t('drive.new') }}</span>
464
+ </span>
465
+ <span v-if="showLabels" class="fe-sidenav__new-rule" aria-hidden="true"></span>
466
+ <span v-if="showLabels" class="fe-sidenav__new-caret" aria-hidden="true">
467
+ <svg
468
+ class="fe-ficon"
469
+ viewBox="0 0 24 24"
470
+ fill="none"
471
+ stroke="currentColor"
472
+ stroke-width="2"
473
+ stroke-linecap="round"
474
+ stroke-linejoin="round"
475
+ aria-hidden="true"
476
+ focusable="false"
477
+ >
478
+ <path d="M7 10l5 5 5-5" />
479
+ </svg>
480
+ </span>
422
481
  </button>
423
482
  </div>
424
483
 
@@ -428,8 +487,8 @@ const toggleLabel = computed(() =>
428
487
  <button
429
488
  type="button"
430
489
  class="fe-sidenav__item"
431
- :class="{ 'is-active': activeView === v.key }"
432
- :aria-current="activeView === v.key ? 'page' : undefined"
490
+ :class="{ 'is-active': isActiveDest(v.key) }"
491
+ :aria-current="isActiveDest(v.key) ? 'page' : undefined"
433
492
  :title="v.label"
434
493
  :aria-label="v.label"
435
494
  :data-testid="`sidenav-view-${v.key}`"
@@ -446,7 +505,15 @@ const toggleLabel = computed(() =>
446
505
  aria-hidden="true"
447
506
  focusable="false"
448
507
  >
449
- <template v-if="v.key === 'recent'">
508
+ <template v-if="v.key === 'home'">
509
+ <path d="M4 10.5L12 4l8 6.5" />
510
+ <path d="M6 9.8V19a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V9.8" />
511
+ <path d="M10 20v-5.5h4V20" />
512
+ </template>
513
+ <template v-else-if="v.key === 'myfiles'">
514
+ <path d="M3.5 7.5A1.5 1.5 0 0 1 5 6h4l2 2.5h8A1.5 1.5 0 0 1 20.5 10v7.5A1.5 1.5 0 0 1 19 19H5a1.5 1.5 0 0 1-1.5-1.5z" />
515
+ </template>
516
+ <template v-else-if="v.key === 'recent'">
450
517
  <circle cx="12" cy="12" r="8.5" />
451
518
  <path d="M12 7.5V12l3 2" />
452
519
  </template>
@@ -721,7 +788,6 @@ const toggleLabel = computed(() =>
721
788
  </div>
722
789
 
723
790
  <ContextMenu
724
- v-if="newMenu"
725
791
  ref="newMenuRef"
726
792
  :locale="locale"
727
793
  :theme="theme"
@@ -19,6 +19,7 @@ import { computed, ref } from 'vue';
19
19
  import type { LocaleCode } from '../types/ExplorerConfig';
20
20
  import type { StorageField, StorageFieldOption } from '../types/Connections';
21
21
  import { useLocale } from '../composables/useLocale';
22
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
22
23
 
23
24
  const props = defineProps<{
24
25
  fields: StorageField[];
@@ -175,7 +176,16 @@ function toggleReveal(key: string) {
175
176
  :aria-label="revealed[f.key] ? t('conn.form.hide') : t('conn.form.reveal')"
176
177
  @click="toggleReveal(f.key)"
177
178
  >
178
- {{ revealed[f.key] ? '🙈' : '👁' }}
179
+ <!-- ikon:emoji the two states of a secret field. 👁/🙈 were a
180
+ hairline dingbat beside a full-colour monkey, i.e. two
181
+ different fonts for one toggle; these are the same eye the
182
+ context menu's Preview row uses, with and without its slash.
183
+ The state is named on the button (title + aria-label). -->
184
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
185
+ <span
186
+ aria-hidden="true"
187
+ v-html="actionIconSvg(revealed[f.key] ? 'toggle-hidden' : 'preview')"
188
+ ></span>
179
189
  </button>
180
190
  </div>
181
191
 
@@ -313,6 +323,11 @@ function toggleReveal(key: string) {
313
323
  }
314
324
  .fe-cfield__eye {
315
325
  flex: 0 0 auto;
326
+ /* ikon:emoji — it holds an SVG now, not a 👁, so it centres the box
327
+ instead of a text baseline. */
328
+ display: inline-flex;
329
+ align-items: center;
330
+ justify-content: center;
316
331
  border: 1px solid var(--fe-border-strong);
317
332
  background: var(--fe-bg-elev);
318
333
  color: var(--fe-text);
@@ -321,6 +336,9 @@ function toggleReveal(key: string) {
321
336
  cursor: pointer;
322
337
  font-size: 14px;
323
338
  }
339
+ .fe-cfield__eye .fe-aicon {
340
+ color: currentColor;
341
+ }
324
342
  .fe-cfield__check {
325
343
  display: flex;
326
344
  align-items: center;