@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
@@ -1,15 +1,49 @@
1
1
  <script setup lang="ts">
2
- // Access modal — one popup combining "Permissions" (per-user RBAC grants,
3
- // owner-only) and "Share by link" (public share link, editor+). Opened
4
- // from the explorer's unified "Share / Permissions" action. The layout is a fixed
5
- // header/tabs with a single scrollable body so the popup never grows into one
6
- // long scroll. Styling uses the SFC's --fe-* theme variables (light/dark).
7
- import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
2
+ /**
3
+ * Access modal — "Share / Permissions", the popup behind the explorer's
4
+ * `access` action.
5
+ *
6
+ * gorunum:v2-share the LOOK changed, the capabilities did not.
7
+ *
8
+ * It used to open as three peer tabs (People · Link · Request files), each a
9
+ * grid of controls competing for the first glance. Nine times in ten the
10
+ * person who opened it wants one thing: turn the link on and copy it. So the
11
+ * dialog now leads with exactly that — one switch, one plain sentence saying
12
+ * who can open the item right now, and the link with its Copy button — and
13
+ * everything else sits underneath in a **named** second tier:
14
+ *
15
+ * · "Bağlantı seçenekleri" — PIN, expiry, download cap, the one-line curl,
16
+ * e-mail delivery, the OS share sheet, and every existing download link.
17
+ * · "Erişimi olanlar" — the per-user/per-group grants (owner only).
18
+ * · "Dosya İste" — the inbound drop link (folders only).
19
+ *
20
+ * ⚠ Second tier, not "Advanced": each section says what is in it and carries a
21
+ * one-glance summary of its own state ("PIN · 7 gün · 3 indirme"), because a
22
+ * control nobody can find is a control that does not exist. `initialTab` still
23
+ * decides which section opens with the dialog, so "Request files" still lands
24
+ * on the drop link.
25
+ *
26
+ * ⚠ No `<style>` block: the CSS lives in `styles/base.css`
27
+ * (`gorunum:v2-share`). A scoped style block here is silently dropped in the
28
+ * web-component build — see web/tests/api/scopedStyles.test.ts.
29
+ */
30
+ import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue';
8
31
  import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
9
32
  import type { ShareInfo } from '../types/FileNode';
10
33
  import { shareCliCommand } from '../lib/shareCli';
11
- import { STOCK_EXPIRY_DAYS, clampExpiryOptions, defaultExpiryDays, ttlCeilingHint, validUntilLine } from '../lib/shareTtl';
34
+ import {
35
+ STOCK_EXPIRY_DAYS,
36
+ clampExpiryOptions,
37
+ defaultExpiryDays,
38
+ shareDetailLine,
39
+ ttlCeilingHint,
40
+ validUntilLine,
41
+ } from '../lib/shareTtl';
42
+ import { EXPLORER_CLOCK } from '../lib/timezone';
12
43
  import { resolveLocale } from '../locales/resolve';
44
+ import { formatByteSize, useLocale } from '../composables/useLocale';
45
+ import { actionIconSvg } from '../lib/actionIcons';
46
+ import { fileIconTile } from '../lib/fileIcons';
13
47
 
14
48
  const props = defineProps<{
15
49
  api: FileApi;
@@ -21,23 +55,24 @@ const props = defineProps<{
21
55
  * undefined/0 = no ceiling. The expiry choices are derived from it. */
22
56
  shareMaxTtlDays?: number;
23
57
  /**
24
- * surucu:d1 — which tab this modal opens on. Absent = 'perms', the behaviour
25
- * every existing caller has.
58
+ * surucu:d1 — which part of the dialog the caller wants opened. Absent =
59
+ * 'perms', the behaviour every existing caller has.
26
60
  *
27
- * The drive shell's "Request files" needs 'drop': the file-drop link already
28
- * lives in this modal, and a menu entry that opens it on the permissions tab
29
- * and leaves the user to find the third tab is a menu entry that lied about
30
- * what it does.
61
+ * The drive shell's "Request files" needs 'drop': the file-drop link lives
62
+ * in this dialog, and a menu entry that opened it anywhere else would be a
63
+ * menu entry that lied about what it does.
31
64
  *
32
- * ⚠ A hint, not a lock: `load()` still falls back to 'share' when the caller
33
- * turns out not to be an owner, because the permissions tab is not theirs to
34
- * see.
65
+ * ⚠ A hint, not a lock: `load()` still falls back to the link tier when the
66
+ * caller turns out not to be an owner, because the grants section is not
67
+ * theirs to see.
35
68
  */
36
69
  initialTab?: 'perms' | 'share' | 'drop';
37
70
  }>();
38
71
  const emit = defineEmits<{ (e: 'close'): void }>();
39
72
 
40
- const tr = computed(() => (resolveLocale(props.locale)) !== 'en');
73
+ const localeCode = computed(() => resolveLocale(props.locale));
74
+ const { t } = useLocale(() => localeCode.value);
75
+ const tr = computed(() => localeCode.value !== 'en');
41
76
  function L(t: string, e: string): string {
42
77
  return tr.value ? t : e;
43
78
  }
@@ -50,10 +85,23 @@ const pathParts = computed(() => {
50
85
  const segs = rel.split('/').filter(Boolean);
51
86
  return { adapter, name: segs.length ? segs[segs.length - 1] : adapter, rel };
52
87
  });
88
+ // The type tile beside the title — the same badge the listing draws, so the
89
+ // dialog is visibly about the row the user right-clicked.
90
+ const titleTile = computed(() => {
91
+ const name = pathParts.value.name;
92
+ const dot = name.lastIndexOf('.');
93
+ return fileIconTile({
94
+ type: props.isDir ? 'dir' : 'file',
95
+ extension: !props.isDir && dot > 0 ? name.slice(dot + 1) : '',
96
+ });
97
+ });
53
98
 
54
- type Tab = 'perms' | 'share' | 'drop';
55
- const tab = ref<Tab>(props.initialTab ?? 'perms');
56
- const canManage = ref(false); // owner/admin → can see the permissions tab
99
+ type Section = 'link' | 'people' | 'drop';
100
+ const open = ref<Record<Section, boolean>>({ link: false, people: false, drop: false });
101
+ function toggleSection(s: Section) {
102
+ open.value = { ...open.value, [s]: !open.value[s] };
103
+ }
104
+ const canManage = ref(false); // owner/admin → can see the grants section
57
105
 
58
106
  // ── permissions state ──
59
107
  const loading = ref(true);
@@ -94,6 +142,23 @@ const shareMailTo = ref('');
94
142
  const shareMailBusy = ref(false);
95
143
  const shareMailNotice = ref('');
96
144
 
145
+ /**
146
+ * ⚠ `listShares` returns BOTH kinds of link — a download link (`/s/…`) and a
147
+ * file-drop link (`/d/…`) are the same row with a different `kind`. They used
148
+ * to be listed together under "Existing links", which meant an upload link
149
+ * appeared in the download tab and the "link sharing" state could not be read
150
+ * off the list at all. Split at the source: each section owns its own kind.
151
+ *
152
+ * `kind` is absent from the `ShareInfo` type but present on the wire (see
153
+ * backend/internal/api/handlers/share.go), so it is read through a narrow cast
154
+ * rather than by widening a shared type this dialog does not own.
155
+ */
156
+ function shareKind(s: ShareInfo): string {
157
+ return (s as ShareInfo & { kind?: string }).kind ?? 'download';
158
+ }
159
+ const downloadShares = computed(() => shares.value.filter((s) => shareKind(s) !== 'drop'));
160
+ const dropShares = computed(() => shares.value.filter((s) => shareKind(s) === 'drop'));
161
+
97
162
  // ⚠ Derived from the server's ceiling, never a fixed list: offering "30 days"
98
163
  // on a server that keeps links for 7 would show a choice that is not one.
99
164
  // The same helper drives every surface (see lib/shareTtl.ts).
@@ -195,14 +260,15 @@ async function reload() {
195
260
  storageRbac.value = r.storage_rbac;
196
261
  canManage.value = true;
197
262
  } catch (e) {
198
- // 403 = caller is editor (not owner): no permissions tab, share only.
263
+ // 403 = caller is editor (not owner): no grants section, link only.
199
264
  const st = (e as { status?: number }).status;
200
265
  if (st === 403) {
201
266
  canManage.value = false;
202
- // Editor (not owner): no permissions tab fall back to the link tab.
203
- // Unless the caller asked for a specific one: "Request files" opens
204
- // 'drop', which an editor may perfectly well use.
205
- if (!props.initialTab || props.initialTab === 'perms') tab.value = 'share';
267
+ // Editor (not owner): the grants section is not theirs close it and
268
+ // leave the link tier, which is the whole dialog for them.
269
+ // ⚠ Unless the caller asked for the drop link, which an editor may
270
+ // perfectly well mint.
271
+ open.value = { ...open.value, people: false };
206
272
  } else {
207
273
  err.value = e instanceof Error ? e.message : String(e);
208
274
  }
@@ -218,12 +284,29 @@ async function reloadShares() {
218
284
  shares.value = [];
219
285
  }
220
286
  }
287
+ function onKey(e: KeyboardEvent) {
288
+ if (e.key === 'Escape') emit('close');
289
+ }
221
290
  onMounted(async () => {
291
+ // surucu:d1 — the caller's entry point decides which section opens with the
292
+ // dialog.
293
+ //
294
+ // ⚠ The DEFAULT (no `initialTab`, i.e. the plain "Share / Permissions"
295
+ // action) now opens NOTHING. It used to land on the permissions tab, which
296
+ // meant the common case — turn the link on, copy it — arrived buried under a
297
+ // grant editor. The link tier is the answer to the default question; the
298
+ // grants are one click away with their own state ("2 people") readable from
299
+ // the closed heading. A caller that explicitly wants the grants still says
300
+ // so, and "Request files" still lands on the drop link.
301
+ if (props.initialTab === 'drop') open.value = { ...open.value, drop: true };
302
+ else if (props.initialTab === 'perms') open.value = { ...open.value, people: true };
303
+ document.addEventListener('keydown', onKey);
222
304
  await reload();
223
305
  await reloadShares();
224
306
  });
225
307
  onBeforeUnmount(() => {
226
308
  if (searchTimer) clearTimeout(searchTimer);
309
+ document.removeEventListener('keydown', onKey);
227
310
  });
228
311
 
229
312
  function onEmailInput() {
@@ -315,13 +398,14 @@ async function inviteCreateUser() {
315
398
  busy.value = false;
316
399
  }
317
400
  }
318
- // "Just send a share link" → jump to the share tab with the address prefilled so the
319
- // owner creates a link (with their chosen expiry/PIN) and mails it there.
401
+ // "Just send a share link" → open the link options with the address prefilled
402
+ // so the owner creates a link (with their chosen expiry/PIN) and mails it
403
+ // from there.
320
404
  function gotoShareWithMail() {
321
405
  shareMailTo.value = email.value.trim().toLowerCase();
322
406
  noAccount.value = false;
323
407
  notice.value = '';
324
- tab.value = 'share';
408
+ open.value = { ...open.value, link: true };
325
409
  }
326
410
  async function changeLevel(g: Grant, newLevel: string) {
327
411
  if (newLevel === g.level) return;
@@ -375,6 +459,39 @@ async function createLink() {
375
459
  shareBusy.value = false;
376
460
  }
377
461
  }
462
+
463
+ /**
464
+ * gorunum:v2-share — the switch.
465
+ *
466
+ * ON with nothing minted mints one with whatever the options tier currently
467
+ * says (that is the default: the server's ceiling as the expiry, no PIN, no
468
+ * cap). OFF revokes every DOWNLOAD link on this item, because that is what
469
+ * "link sharing is off" has to mean — a switch that left a live link behind
470
+ * would be the most dangerous control in the dialog.
471
+ *
472
+ * ⚠ Drop links are deliberately untouched: they are an inbound door managed by
473
+ * its own section, and revoking someone's upload invitation because the owner
474
+ * turned off downloads is not what either control says.
475
+ */
476
+ async function toggleLink() {
477
+ if (shareBusy.value) return;
478
+ if (linkOn.value) {
479
+ shareBusy.value = true;
480
+ shareErr.value = '';
481
+ try {
482
+ for (const s of downloadShares.value) await props.api.revokeShare(s.uuid);
483
+ shareResult.value = null;
484
+ await reloadShares();
485
+ } catch (e) {
486
+ shareErr.value = e instanceof Error ? e.message : String(e);
487
+ } finally {
488
+ shareBusy.value = false;
489
+ }
490
+ return;
491
+ }
492
+ await createLink();
493
+ }
494
+
378
495
  async function sendShareMail() {
379
496
  const list = splitEmails(shareMailTo.value);
380
497
  if (!list.length) {
@@ -437,6 +554,7 @@ async function createDropLink() {
437
554
  expiresAt: r.share.expires_at ?? null,
438
555
  clamped: !!r.share.expiry_clamped,
439
556
  };
557
+ await reloadShares();
440
558
  } catch (e) {
441
559
  dropErr.value = e instanceof Error ? e.message : String(e);
442
560
  } finally {
@@ -475,8 +593,15 @@ async function sendDropMail() {
475
593
  }
476
594
  async function revoke(s: ShareInfo) {
477
595
  shareBusy.value = true;
478
- try { await props.api.revokeShare(s.uuid); await reloadShares(); }
479
- catch (e) { shareErr.value = e instanceof Error ? e.message : String(e); }
596
+ try {
597
+ await props.api.revokeShare(s.uuid);
598
+ // ⚠ The lead tier reads `shareResult` first. Revoking the link that is
599
+ // showing up there has to clear it too, or the dialog goes on offering a
600
+ // Copy button — and a switch reading ON — for a link the server has just
601
+ // thrown away.
602
+ if (shareResult.value && shareResult.value.url === s.url) shareResult.value = null;
603
+ await reloadShares();
604
+ } catch (e) { shareErr.value = e instanceof Error ? e.message : String(e); }
480
605
  finally { shareBusy.value = false; }
481
606
  }
482
607
  function copy(text: string, tag = 'url') {
@@ -493,15 +618,24 @@ function copy(text: string, tag = 'url') {
493
618
  // same as an emailed link.
494
619
  const canShare = computed(() => typeof navigator !== 'undefined' && typeof navigator.share === 'function');
495
620
 
496
- // humanSize mirrors the Go humanSize() used in the email body (1.4 MB).
621
+ /**
622
+ * The size line in the share message, spelled the way the SERVER spells it.
623
+ *
624
+ * ⚠ This one really does have to mirror `humanSize()` in
625
+ * backend/internal/api/handlers/mail_templates.go — the same file forwarded
626
+ * by e-mail and by the OS share sheet must not carry two different sizes. So
627
+ * the mirroring is stated as arguments to the one formatter (1024, always one
628
+ * decimal, a dot and English letters, because that is what Go's `%.1f %cB`
629
+ * prints) instead of being a fifth private copy of the arithmetic.
630
+ *
631
+ * ⚠ It is therefore the one place in the UI that is NOT decimal, so a share
632
+ * message reads "1.4 MB" where the listing row reads "1.5 MB". The register
633
+ * entry for this was right about the real fix: the e-mail should render from
634
+ * one source rather than the UI copying its rounding.
635
+ */
497
636
  function humanSize(b?: number): string {
498
637
  if (!b || b <= 0) return '';
499
- const unit = 1024;
500
- if (b < unit) return `${b} B`;
501
- const units = ['K', 'M', 'G', 'T', 'P', 'E'];
502
- let div = unit, exp = 0;
503
- for (let n = b / unit; n >= unit; n /= unit) { div *= unit; exp++; }
504
- return `${(b / div).toFixed(1)} ${units[exp]}B`;
638
+ return formatByteSize(b, { base: 1024, digits: 'fixed1', numberLocale: null });
505
639
  }
506
640
 
507
641
  function expiryLine(days: number): string {
@@ -510,10 +644,67 @@ function expiryLine(days: number): string {
510
644
  }
511
645
  // What the server actually stored — shown under a fresh link so the real
512
646
  // expiry is visible even when the server shortened the request.
647
+ // The explorer this dialog belongs to reads deadlines on its own clock.
648
+ const clock = inject(EXPLORER_CLOCK, undefined);
649
+
513
650
  function validUntil(r: { expiresAt?: string | null } | null): string {
514
- return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en');
651
+ return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en', clock);
515
652
  }
516
653
 
654
+ /* ── the top tier: one switch, one sentence, one link ──────────────────── */
655
+
656
+ /** The link the header row shows: the one just minted, else the oldest live one. */
657
+ const primaryLink = computed(() => shareResult.value?.url ?? downloadShares.value[0]?.url ?? '');
658
+ const linkOn = computed(() => !!primaryLink.value);
659
+
660
+ /**
661
+ * The plain sentence. It says who can open the item RIGHT NOW, which is the
662
+ * question the dialog exists to answer.
663
+ *
664
+ * ⚠ The PIN claim is only made about a link this dialog minted: `listShares`
665
+ * does not return `password_pin` (by design — the server will not re-serve a
666
+ * PIN), so for a link that was already there we say how many exist rather than
667
+ * guessing at its protection. A confident wrong sentence about who can read a
668
+ * file is worse than a vaguer true one.
669
+ */
670
+ const whoLine = computed(() => {
671
+ if (!linkOn.value) return t('access.who.private');
672
+ if (shareResult.value) return shareResult.value.pin ? t('access.who.pin') : t('access.who.link');
673
+ return t('access.who.existing', { n: downloadShares.value.length });
674
+ });
675
+
676
+ /** The muted second line under it: what the server actually stored. */
677
+ const linkDetail = computed(() => {
678
+ const bits: string[] = [];
679
+ if (shareResult.value) {
680
+ bits.push(validUntil(shareResult.value));
681
+ if (shareResult.value.clamped) bits.push(L('(sunucu sınırı uygulandı)', '(server limit applied)'));
682
+ if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
683
+ } else if (downloadShares.value[0]) {
684
+ const s = downloadShares.value[0];
685
+ bits.push(validUntil({ expiresAt: s.expires_at ?? null }));
686
+ if (s.max_downloads) bits.push(maxDlOptions.find((o) => o.v === s.max_downloads)?.l ?? String(s.max_downloads));
687
+ }
688
+ return shareDetailLine(bits);
689
+ });
690
+
691
+ /* Section summaries — a named section still has to say what is inside it, or
692
+ the reader has to open all three to find the control they came for. */
693
+ const linkSummary = computed(() => {
694
+ const bits: string[] = [];
695
+ bits.push(sharePwd.value ? t('access.sum.pin_on') : t('access.sum.pin_off'));
696
+ bits.push(expiryLabel(shareExpiry.value));
697
+ if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
698
+ return bits.filter(Boolean).join(' · ');
699
+ });
700
+ const peopleSummary = computed(() => {
701
+ const n = direct.value.length + inherited.value.length;
702
+ return n ? t('access.sum.people', { n }) : t('access.sum.people_none');
703
+ });
704
+ const dropSummary = computed(() =>
705
+ dropShares.value.length ? t('access.sum.drop', { n: dropShares.value.length }) : t('access.sum.drop_none'),
706
+ );
707
+
517
708
  // Text + title for a download-share link, mirroring shareMailText().
518
709
  function shareBody(): { title: string; text: string } {
519
710
  const name = pathParts.value.name;
@@ -573,478 +764,368 @@ async function nativeShare(body: { title: string; text: string }) {
573
764
  </script>
574
765
 
575
766
  <template>
576
- <div class="fx-perm-overlay" @click.self="emit('close')">
577
- <div class="fx-perm-modal">
578
- <header class="fx-perm-head">
579
- <div class="fx-perm-title">
580
- <span class="fx-perm-ico" aria-hidden="true">{{ isDir ? '📁' : '📄' }}</span>
581
- <div class="fx-perm-titletext">
582
- <h3>{{ L('Paylaş / İzinler', 'Share / Permissions') }}</h3>
583
- <span class="fx-perm-sub" :title="path">{{ pathParts.name }}<span class="fx-perm-subdim"> · {{ pathParts.adapter }}</span></span>
584
- </div>
767
+ <div class="fx-perm-overlay fe-share__scrim" @click.self="emit('close')">
768
+ <!-- ⚠ `fx-perm-modal` is kept alongside the new class: e2e/shots/capture.mjs
769
+ and the embedders' own harnesses address the dialog by it. -->
770
+ <div class="fx-perm-modal fe-share" role="dialog" aria-modal="true" :aria-label="t('access.title', { name: pathParts.name })">
771
+ <header class="fe-share__head">
772
+ <span class="fe-share__tile" aria-hidden="true" v-html="titleTile"></span>
773
+ <div class="fe-share__headtext">
774
+ <h3 class="fe-share__title">{{ t('access.title', { name: pathParts.name }) }}</h3>
775
+ <span class="fe-share__path" :title="path">{{ pathParts.adapter }}</span>
585
776
  </div>
586
- <button class="fx-perm-x" @click="emit('close')" aria-label="close">✕</button>
777
+ <button type="button" class="fe-share__close" :aria-label="t('access.close')" :title="t('access.close')" @click="emit('close')">
778
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
779
+ </button>
587
780
  </header>
588
781
 
589
- <div class="fx-perm-tabs">
590
- <button
591
- v-if="canManage"
592
- class="fx-perm-tab"
593
- :class="{ 'is-active': tab === 'perms' }"
594
- @click="tab = 'perms'"
595
- >{{ L('Kişiler', 'People') }}</button>
596
- <button
597
- class="fx-perm-tab"
598
- :class="{ 'is-active': tab === 'share' }"
599
- @click="tab = 'share'"
600
- >{{ L('Bağlantı', 'Link') }}</button>
601
- <button
602
- v-if="isDir"
603
- class="fx-perm-tab"
604
- :class="{ 'is-active': tab === 'drop' }"
605
- @click="tab = 'drop'"
606
- >{{ L('Dosya İste', 'Request files') }}</button>
607
- </div>
608
-
609
- <div class="fx-perm-body">
610
- <!-- ───────── Permissions tab ───────── -->
611
- <template v-if="tab === 'perms' && canManage">
612
- <div v-if="!storageRbac" class="fx-perm-warn">
613
- {{ L('Bu diskte RBAC kapalı — izinler yalnızca RBAC açık disklerde geçerli.', 'RBAC is off on this storage — grants only apply when RBAC is enabled.') }}
782
+ <div class="fe-share__body">
783
+ <!-- ───────── tier 1: the link, the whole reason people open this ───── -->
784
+ <div class="fe-share__lead">
785
+ <div class="fe-share__switchrow">
786
+ <span class="fe-share__leadicon" aria-hidden="true" v-html="actionIconSvg('access')"></span>
787
+ <span class="fe-share__leadlabel">{{ t('access.link.switch') }}</span>
788
+ <button
789
+ type="button"
790
+ class="fe-share__switch"
791
+ role="switch"
792
+ :aria-checked="linkOn ? 'true' : 'false'"
793
+ :aria-label="t('access.link.switch')"
794
+ data-testid="share-switch"
795
+ :disabled="shareBusy"
796
+ @click="toggleLink"
797
+ ><span class="fe-share__knob"></span></button>
614
798
  </div>
615
- <div v-if="err" class="fx-perm-warn">{{ err }}</div>
616
- <div v-if="loading" class="fx-perm-muted">{{ L('Yükleniyor…', 'Loading…') }}</div>
617
- <template v-else>
618
- <!-- Add person (primary action, on top) -->
619
- <div class="fx-perm-addcard">
620
- <div class="fx-perm-add">
621
- <div class="fx-perm-emailwrap">
622
- <input v-model="email" type="email" class="fx-perm-input" autocomplete="off"
623
- :placeholder="L('İsim veya e-posta', 'Name or email')"
624
- @input="onEmailInput" @keyup.enter="submitEmail" @focus="onEmailInput" />
625
- <ul v-if="showSuggest" class="fx-perm-suggest">
626
- <li v-for="u in suggestions" :key="u.id" @mousedown.prevent="pickUser(u)">
627
- <span class="fx-perm-suggest-av">{{ (u.display_name || u.email).charAt(0).toUpperCase() }}</span>
628
- <span class="fx-perm-suggest-txt">
629
- <span class="fx-perm-suggest-name">{{ u.display_name || u.email }}</span>
630
- <span class="fx-perm-suggest-meta">{{ u.email }} · {{ u.role }}</span>
631
- </span>
632
- </li>
633
- </ul>
799
+ <p class="fe-share__who" data-testid="share-who">{{ whoLine }}</p>
800
+
801
+ <template v-if="linkOn">
802
+ <div class="fe-share__linkrow">
803
+ <a :href="primaryLink" target="_blank" rel="noopener" class="fe-share__url" :title="primaryLink">{{ primaryLink }}</a>
804
+ <button type="button" class="fe-share__copy" data-testid="share-copy" @click="copy(primaryLink, 'new')">
805
+ <span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
806
+ <span>{{ copied === 'new' ? t('access.copied') : t('access.copy') }}</span>
807
+ </button>
808
+ </div>
809
+ <p v-if="linkDetail" class="fe-share__detail" data-testid="share-valid-until">{{ linkDetail }}</p>
810
+ <div v-if="shareResult?.pin" class="fe-share__pinrow">
811
+ <span class="fe-share__pinlabel">PIN</span>
812
+ <code class="fe-share__pin">{{ shareResult?.pin }}</code>
813
+ <button type="button" class="fe-share__mini" @click="copy(shareResult?.pin ?? '', 'sharepin')">
814
+ {{ copied === 'sharepin' ? t('access.copied') : t('access.copy') }}
815
+ </button>
816
+ </div>
817
+ </template>
818
+ <div v-if="shareErr" class="fe-share__warn">{{ shareErr }}</div>
819
+ </div>
820
+
821
+ <!-- ───────── tier 2: named sections, one click each ───────────────── -->
822
+ <div class="fe-share__sections">
823
+ <!-- Link options: PIN · expiry · download cap · curl · e-mail · links -->
824
+ <section class="fe-share__section">
825
+ <button
826
+ type="button"
827
+ class="fe-share__sechead"
828
+ :aria-expanded="open.link ? 'true' : 'false'"
829
+ data-testid="share-options-toggle"
830
+ @click="toggleSection('link')"
831
+ >
832
+ <svg class="fe-share__caret" :class="{ 'is-open': open.link }" viewBox="0 0 24 24" fill="none"
833
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
834
+ aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
835
+ <span class="fe-share__seclabel">{{ t('access.section.link') }}</span>
836
+ <span class="fe-share__secmeta">{{ linkSummary }}</span>
837
+ </button>
838
+ <div v-if="open.link" class="fe-share__panel">
839
+ <div class="fe-share__opts">
840
+ <label class="fe-share__check">
841
+ <input type="checkbox" v-model="sharePwd" />
842
+ <span>{{ L('PIN ile koru', 'Protect with a PIN') }}</span>
843
+ </label>
844
+ <label class="fe-share__field">
845
+ <span class="fe-share__fieldlabel">{{ L('Süre', 'Expiry') }}</span>
846
+ <select v-model.number="shareExpiry" class="fe-share__select" data-testid="share-expiry">
847
+ <option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
848
+ </select>
849
+ </label>
850
+ <label class="fe-share__field">
851
+ <span class="fe-share__fieldlabel">{{ L('İndirme limiti', 'Download limit') }}</span>
852
+ <select v-model.number="shareMaxDl" class="fe-share__select">
853
+ <option v-for="o in maxDlOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
854
+ </select>
855
+ </label>
856
+ </div>
857
+ <p v-if="ttlHint" class="fe-share__hint" data-testid="share-ttl-hint">{{ ttlHint }}</p>
858
+ <button type="button" class="fx-perm-create fe-share__btn fe-share__btn--primary fe-share__btn--wide" :disabled="shareBusy" @click="createLink">
859
+ {{ L('Bağlantı oluştur', 'Create link') }}
860
+ </button>
861
+
862
+ <p v-if="!shareResult && shareMailTo" class="fe-share__hint">
863
+ {{ L('Bir bağlantı oluşturun, ardından', 'Create a link, then it will be sent to') }}
864
+ <strong>{{ shareMailTo }}</strong> {{ L('adresine gönderin.', '.') }}
865
+ </p>
866
+
867
+ <template v-if="shareResult">
868
+ <!-- one-line download command, for pulling the file onto a server -->
869
+ <div class="fx-perm-cli fe-share__cli">
870
+ <span class="fe-share__fieldlabel">{{ L('Komut satırı', 'Command line') }}</span>
871
+ <div class="fe-share__clirow">
872
+ <code class="fe-share__clicmd" :title="shareCli">{{ shareCli }}</code>
873
+ <button type="button" class="fe-share__mini" @click="copy(shareCli, 'sharecli')">
874
+ {{ copied === 'sharecli' ? t('access.copied') : t('access.copy') }}
875
+ </button>
876
+ </div>
634
877
  </div>
635
- <select v-model="level" class="fx-perm-sel" :title="levels.find(o => o.v === level)?.d">
636
- <option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
637
- </select>
638
- <button class="fx-perm-btn fx-perm-btn--primary" :disabled="busy" @click="submitEmail">
639
- {{ L('Ekle', 'Add') }}
878
+
879
+ <!-- send by email (one or more, comma/space separated) -->
880
+ <div class="fe-share__mailrow">
881
+ <input v-model="shareMailTo" type="text" class="fe-share__input" autocomplete="off"
882
+ :placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendShareMail" />
883
+ <button type="button" class="fe-share__btn" :disabled="shareMailBusy" @click="sendShareMail">
884
+ {{ L('Gönder', 'Send') }}
885
+ </button>
886
+ </div>
887
+ <div v-if="shareMailNotice" class="fe-share__notice">{{ shareMailNotice }}</div>
888
+
889
+ <!-- native share (OS share sheet) — same as the fishapp Share button -->
890
+ <button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(shareBody())">
891
+ <span aria-hidden="true" v-html="actionIconSvg('access')"></span>
892
+ <span>{{ L('Paylaş', 'Share') }}</span>
640
893
  </button>
894
+ </template>
895
+
896
+ <div class="fe-share__list">
897
+ <h4 class="fe-share__listhead">{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
898
+ <p v-if="!downloadShares.length" class="fe-share__empty">{{ L('Yok', 'None') }}</p>
899
+ <div v-for="s in downloadShares" :key="s.uuid" class="fe-share__row">
900
+ <span class="fe-share__url" :title="s.url">{{ s.url }}</span>
901
+ <button type="button" class="fe-share__mini" @click="copy(s.url, s.uuid)">
902
+ {{ copied === s.uuid ? t('access.copied') : t('access.copy') }}
903
+ </button>
904
+ <button type="button" class="fe-share__del" :disabled="shareBusy" :title="L('İptal', 'Revoke')"
905
+ :aria-label="L('İptal', 'Revoke')" @click="revoke(s)">
906
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
907
+ </button>
908
+ </div>
641
909
  </div>
910
+ </div>
911
+ </section>
642
912
 
643
- <div v-if="noAccount" class="fx-perm-invite">
644
- <p class="fx-perm-muted">{{ L('Bu e-postada hesap yok. Ne yapmak istersiniz?', 'No account for this email — what next?') }}</p>
645
- <div class="fx-perm-invite-actions">
646
- <div class="fx-perm-invite-row">
647
- <select v-model="createRole" class="fx-perm-sel">
913
+ <!-- People with access: the per-user / inherited grants -->
914
+ <section v-if="canManage" class="fe-share__section">
915
+ <button
916
+ type="button"
917
+ class="fe-share__sechead"
918
+ :aria-expanded="open.people ? 'true' : 'false'"
919
+ data-testid="share-people-toggle"
920
+ @click="toggleSection('people')"
921
+ >
922
+ <svg class="fe-share__caret" :class="{ 'is-open': open.people }" viewBox="0 0 24 24" fill="none"
923
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
924
+ aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
925
+ <span class="fe-share__seclabel">{{ t('access.section.people') }}</span>
926
+ <span class="fe-share__secmeta">{{ peopleSummary }}</span>
927
+ </button>
928
+ <div v-if="open.people" class="fe-share__panel">
929
+ <div v-if="!storageRbac" class="fe-share__warn">
930
+ {{ L('Bu diskte RBAC kapalı — izinler yalnızca RBAC açık disklerde geçerli.', 'RBAC is off on this storage — grants only apply when RBAC is enabled.') }}
931
+ </div>
932
+ <div v-if="err" class="fe-share__warn">{{ err }}</div>
933
+ <p v-if="loading" class="fe-share__hint">{{ L('Yükleniyor…', 'Loading…') }}</p>
934
+ <template v-else>
935
+ <div class="fe-share__add">
936
+ <div class="fe-share__emailwrap">
937
+ <input v-model="email" type="email" class="fe-share__input" autocomplete="off"
938
+ :placeholder="L('İsim veya e-posta', 'Name or email')"
939
+ @input="onEmailInput" @keyup.enter="submitEmail" @focus="onEmailInput" />
940
+ <ul v-if="showSuggest" class="fe-share__suggest">
941
+ <li v-for="u in suggestions" :key="u.id" @mousedown.prevent="pickUser(u)">
942
+ <span class="fe-share__av fe-share__av--sm">{{ (u.display_name || u.email).charAt(0).toUpperCase() }}</span>
943
+ <span class="fe-share__suggesttxt">
944
+ <span class="fe-share__suggestname">{{ u.display_name || u.email }}</span>
945
+ <span class="fe-share__suggestmeta">{{ u.email }} · {{ u.role }}</span>
946
+ </span>
947
+ </li>
948
+ </ul>
949
+ </div>
950
+ <select v-model="level" class="fe-share__select" :title="levels.find(o => o.v === level)?.d">
951
+ <option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
952
+ </select>
953
+ <button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="submitEmail">
954
+ {{ L('Ekle', 'Add') }}
955
+ </button>
956
+ </div>
957
+
958
+ <div v-if="noAccount" class="fe-share__invite">
959
+ <p class="fe-share__hint">{{ L('Bu e-postada hesap yok. Ne yapmak istersiniz?', 'No account for this email — what next?') }}</p>
960
+ <div class="fe-share__inviterow">
961
+ <select v-model="createRole" class="fe-share__select">
648
962
  <option value="user">{{ L('Kullanıcı', 'User') }}</option>
649
963
  <option value="viewer">{{ L('Görüntüleyen', 'Viewer') }}</option>
650
964
  </select>
651
- <button class="fx-perm-btn fx-perm-btn--primary" :disabled="busy" @click="inviteCreateUser">
965
+ <button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="inviteCreateUser">
652
966
  {{ L('Kullanıcı oluştur + yetki ver', 'Create user + grant') }}
653
967
  </button>
654
968
  </div>
655
- <button class="fx-perm-btn fx-perm-btn--ghost" :disabled="busy" @click="gotoShareWithMail">
969
+ <button type="button" class="fe-share__linkbtn" :disabled="busy" @click="gotoShareWithMail">
656
970
  {{ L('Sadece paylaşım linki gönder →', 'Just send a share link →') }}
657
971
  </button>
658
972
  </div>
659
- </div>
660
- <div v-if="notice" class="fx-perm-notice">{{ notice }}</div>
661
- <div v-if="inviteResult?.tempPassword" class="fx-perm-reveal">
662
- {{ L('Geçici parola:', 'Temp password:') }} <code>{{ inviteResult.tempPassword }}</code>
663
- </div>
664
- </div>
973
+ <div v-if="notice" class="fe-share__notice">{{ notice }}</div>
974
+ <div v-if="inviteResult?.tempPassword" class="fe-share__notice">
975
+ {{ L('Geçici parola:', 'Temp password:') }} <code class="fe-share__pin">{{ inviteResult.tempPassword }}</code>
976
+ </div>
665
977
 
666
- <!-- People with access -->
667
- <div class="fx-perm-section">
668
- <h4>{{ L('Erişimi olanlar', 'People with access') }}</h4>
669
- <div v-if="!direct.length && !inherited.length" class="fx-perm-empty">
670
- {{ L('Henüz kimseyle paylaşılmadı.', 'Not shared with anyone yet.') }}
671
- </div>
672
- <div v-for="g in direct" :key="'d' + g.id" class="fx-perm-person">
673
- <span class="fx-perm-av">{{ ginitial(g) }}</span>
674
- <span class="fx-perm-user" :title="g.user_email">{{ glabel(g) }}</span>
675
- <select class="fx-perm-sel fx-perm-sel--sm" :value="g.level"
676
- @change="changeLevel(g, ($event.target as HTMLSelectElement).value)">
677
- <option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
678
- </select>
679
- <button class="fx-perm-del" :disabled="busy" :title="L('Kaldır', 'Remove')" @click="removeGrant(g)">✕</button>
680
- </div>
681
- <div v-for="g in inherited" :key="'i' + g.id" class="fx-perm-person fx-perm-inh">
682
- <span class="fx-perm-av fx-perm-av--dim">{{ ginitial(g) }}</span>
683
- <span class="fx-perm-user" :title="g.user_email">{{ glabel(g) }}</span>
684
- <span class="fx-perm-badge">{{ levelLabel(g.level) }}</span>
685
- <span class="fx-perm-from" :title="L('Üst klasörden gelir', 'Inherited from') + ': ' + (g.path_prefix || '/')">
686
- {{ g.path_prefix || '/' }}
687
- </span>
688
- </div>
978
+ <div class="fe-share__list">
979
+ <h4 class="fe-share__listhead">{{ L('Erişimi olanlar', 'People with access') }}</h4>
980
+ <p v-if="!direct.length && !inherited.length" class="fe-share__empty">
981
+ {{ L('Henüz kimseyle paylaşılmadı.', 'Not shared with anyone yet.') }}
982
+ </p>
983
+ <div v-for="g in direct" :key="'d' + g.id" class="fe-share__row">
984
+ <span class="fe-share__av">{{ ginitial(g) }}</span>
985
+ <span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
986
+ <select class="fe-share__select fe-share__select--sm" :value="g.level"
987
+ @change="changeLevel(g, ($event.target as HTMLSelectElement).value)">
988
+ <option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
989
+ </select>
990
+ <button type="button" class="fe-share__del" :disabled="busy" :title="L('Kaldır', 'Remove')"
991
+ :aria-label="L('Kaldır', 'Remove')" @click="removeGrant(g)">
992
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
993
+ </button>
994
+ </div>
995
+ <div v-for="g in inherited" :key="'i' + g.id" class="fe-share__row fe-share__row--dim">
996
+ <span class="fe-share__av fe-share__av--dim">{{ ginitial(g) }}</span>
997
+ <span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
998
+ <span class="fe-share__badge">{{ levelLabel(g.level) }}</span>
999
+ <span class="fe-share__from" :title="L('Üst klasörden gelir', 'Inherited from') + ': ' + (g.path_prefix || '/')">
1000
+ {{ g.path_prefix || '/' }}
1001
+ </span>
1002
+ </div>
1003
+ </div>
1004
+ </template>
689
1005
  </div>
690
- </template>
691
- </template>
1006
+ </section>
692
1007
 
693
- <!-- ───────── Share tab ───────── -->
694
- <template v-if="tab === 'share'">
695
- <div class="fx-perm-addcard">
696
- <div class="fx-perm-share-opts">
697
- <label class="fx-perm-check">
698
- <input type="checkbox" v-model="sharePwd" />
699
- {{ L('PIN ile koru', 'Protect with a PIN') }}
700
- </label>
701
- <label class="fx-perm-field">
702
- <span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
703
- <select v-model.number="shareExpiry" class="fx-perm-sel fx-perm-sel--sm" data-testid="share-expiry">
704
- <option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
705
- </select>
706
- </label>
707
- <label class="fx-perm-field">
708
- <span class="fx-perm-muted">{{ L('İndirme limiti', 'Download limit') }}</span>
709
- <select v-model.number="shareMaxDl" class="fx-perm-sel fx-perm-sel--sm">
710
- <option v-for="o in maxDlOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
711
- </select>
712
- </label>
713
- </div>
714
- <p v-if="ttlHint" class="fx-perm-hint fx-perm-ttlhint" data-testid="share-ttl-hint">{{ ttlHint }}</p>
715
- <button class="fx-perm-btn fx-perm-btn--primary fx-perm-create" :disabled="shareBusy" @click="createLink">
716
- {{ L('Bağlantı oluştur', 'Create link') }}
1008
+ <!-- Request files: the inbound drop link (folders only) -->
1009
+ <section v-if="isDir" class="fe-share__section">
1010
+ <button
1011
+ type="button"
1012
+ class="fe-share__sechead"
1013
+ :aria-expanded="open.drop ? 'true' : 'false'"
1014
+ data-testid="share-drop-toggle"
1015
+ @click="toggleSection('drop')"
1016
+ >
1017
+ <svg class="fe-share__caret" :class="{ 'is-open': open.drop }" viewBox="0 0 24 24" fill="none"
1018
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
1019
+ aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
1020
+ <span class="fe-share__seclabel">{{ t('access.section.drop') }}</span>
1021
+ <span class="fe-share__secmeta">{{ dropSummary }}</span>
717
1022
  </button>
718
-
719
- <div v-if="shareErr" class="fx-perm-warn">{{ shareErr }}</div>
720
-
721
- <div v-if="shareResult" class="fx-perm-result">
722
- <div class="fx-perm-linkrow">
723
- <a :href="shareResult.url" target="_blank" rel="noopener" class="fx-perm-link">{{ shareResult.url }}</a>
724
- <button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareResult.url, 'new')">
725
- {{ copied === 'new' ? L('Kopyalandı ', 'Copied ✓') : L('Kopyala', 'Copy') }}
726
- </button>
727
- </div>
728
- <div class="fx-perm-muted fx-perm-validuntil" data-testid="share-valid-until">
729
- {{ validUntil(shareResult) }}
730
- <span v-if="shareResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
731
- </div>
732
- <div v-if="shareResult.pin" class="fx-perm-pin">
733
- <span>PIN: <code>{{ shareResult.pin }}</code></span>
734
- <button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareResult.pin, 'sharepin')">
735
- {{ copied === 'sharepin' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
736
- </button>
1023
+ <div v-if="open.drop" class="fe-share__panel">
1024
+ <p class="fe-share__hint">
1025
+ {{ L('Bu klasöre herkesin dosya YÜKLEYEBİLECEĞİ herkese açık bir bağlantı. Yükleyenler klasördeki mevcut dosyaları göremez.', 'A public link that lets anyone UPLOAD files into this folder. Uploaders never see the folder\'s existing files.') }}
1026
+ </p>
1027
+ <div class="fe-share__opts">
1028
+ <label class="fe-share__check">
1029
+ <input type="checkbox" v-model="dropPwd" />
1030
+ <span>{{ L('PIN ile koru', 'Protect with a PIN') }}</span>
1031
+ </label>
1032
+ <label class="fe-share__field">
1033
+ <span class="fe-share__fieldlabel">{{ L('Süre', 'Expiry') }}</span>
1034
+ <select v-model.number="dropExpiry" class="fe-share__select" data-testid="drop-expiry">
1035
+ <option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
1036
+ </select>
1037
+ </label>
737
1038
  </div>
738
-
739
- <!-- one-line download command, for pulling the file onto a server -->
740
- <div class="fx-perm-cli">
741
- <span class="fx-perm-clilabel">{{ L('Komut satırı', 'Command line') }}</span>
742
- <div class="fx-perm-clirow">
743
- <code class="fx-perm-clicmd" :title="shareCli">{{ shareCli }}</code>
744
- <button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareCli, 'sharecli')">
745
- {{ copied === 'sharecli' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
746
- </button>
747
- </div>
748
- </div>
749
-
750
- <!-- send by email (one or more, comma/space separated) -->
751
- <div class="fx-perm-mailrow">
752
- <input v-model="shareMailTo" type="text" class="fx-perm-input" autocomplete="off"
753
- :placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendShareMail" />
754
- <button class="fx-perm-btn" :disabled="shareMailBusy" @click="sendShareMail">
755
- {{ L('Gönder', 'Send') }}
756
- </button>
757
- </div>
758
- <div v-if="shareMailNotice" class="fx-perm-notice">{{ shareMailNotice }}</div>
759
-
760
- <!-- native share (OS share sheet) — same as the fishapp Share button -->
761
- <button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(shareBody())">
762
- <span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
1039
+ <p v-if="ttlHint" class="fe-share__hint">{{ ttlHint }}</p>
1040
+ <button type="button" class="fe-share__btn fe-share__btn--primary fe-share__btn--wide" data-testid="drop-create" :disabled="dropBusy" @click="createDropLink">
1041
+ {{ L('Bağlantı oluştur', 'Create link') }}
763
1042
  </button>
764
- </div>
765
- <p v-else-if="shareMailTo" class="fx-perm-hint">
766
- {{ L('Aşağıdan bir bağlantı oluşturun, ardından', 'Create a link below, then it will be sent to') }}
767
- <strong>{{ shareMailTo }}</strong> {{ L('adresine gönderin.', '.') }}
768
- </p>
769
- </div>
770
1043
 
771
- <div class="fx-perm-section">
772
- <h4>{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
773
- <div v-if="!shares.length" class="fx-perm-empty">{{ L('Yok', 'None') }}</div>
774
- <div v-for="s in shares" :key="s.uuid" class="fx-perm-person">
775
- <span class="fx-perm-user fx-perm-link" :title="s.url">{{ s.url }}</span>
776
- <button class="fx-perm-btn fx-perm-btn--sm" @click="copy(s.url, s.uuid)">
777
- {{ copied === s.uuid ? L('✓', '✓') : L('Kopyala', 'Copy') }}
1044
+ <button type="button" class="fe-share__linkbtn" :aria-expanded="dropShowAdv ? 'true' : 'false'" @click="dropShowAdv = !dropShowAdv">
1045
+ {{ dropShowAdv ? L('Yükleme sınırlarını gizle', 'Hide upload limits') : L('Yükleme sınırları', 'Upload limits') }}
778
1046
  </button>
779
- <button class="fx-perm-del" :disabled="shareBusy" :title="L('İptal', 'Revoke')" @click="revoke(s)">✕</button>
780
- </div>
781
- </div>
782
- </template>
1047
+ <div v-if="dropShowAdv" class="fe-share__adv">
1048
+ <label class="fe-share__advrow">
1049
+ <span class="fe-share__fieldlabel">{{ L('En fazla dosya', 'Max files') }}</span>
1050
+ <input v-model="dropMaxFiles" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="20" />
1051
+ </label>
1052
+ <label class="fe-share__advrow">
1053
+ <span class="fe-share__fieldlabel">{{ L('Dosya başı MB', 'MB / file') }}</span>
1054
+ <input v-model="dropMaxSizeMB" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="500" />
1055
+ </label>
1056
+ <label class="fe-share__advrow">
1057
+ <span class="fe-share__fieldlabel">{{ L('İzinli türler', 'Allowed types') }}</span>
1058
+ <input v-model="dropAllowedExt" type="text" class="fe-share__input fe-share__input--sm" :placeholder="L('hepsi (örn. pdf, jpg)', 'all (e.g. pdf, jpg)')" />
1059
+ </label>
1060
+ <label class="fe-share__check">
1061
+ <input type="checkbox" v-model="dropAskName" />
1062
+ <span>{{ L('Yükleyenin adını sor', 'Ask uploader name') }}</span>
1063
+ </label>
1064
+ </div>
783
1065
 
784
- <!-- ───────── File-drop (upload link) tab ───────── -->
785
- <template v-if="tab === 'drop'">
786
- <div class="fx-perm-addcard">
787
- <p class="fx-perm-muted fx-perm-dropintro">
788
- {{ L('Bu klasöre herkesin dosya YÜKLEYEBİLECEĞİ herkese açık bir bağlantı. Yükleyenler klasördeki mevcut dosyaları göremez.', 'A public link that lets anyone UPLOAD files into this folder. Uploaders never see the folder\'s existing files.') }}
789
- </p>
790
- <div class="fx-perm-share-opts">
791
- <label class="fx-perm-check">
792
- <input type="checkbox" v-model="dropPwd" />
793
- {{ L('PIN ile koru', 'Protect with a PIN') }}
794
- </label>
795
- <label class="fx-perm-field">
796
- <span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
797
- <select v-model.number="dropExpiry" class="fx-perm-sel fx-perm-sel--sm" data-testid="drop-expiry">
798
- <option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
799
- </select>
800
- </label>
801
- </div>
802
- <p v-if="ttlHint" class="fx-perm-hint fx-perm-ttlhint">{{ ttlHint }}</p>
803
- <button class="fx-perm-btn fx-perm-btn--primary fx-perm-create" :disabled="dropBusy" @click="createDropLink">
804
- {{ L('Bağlantı oluştur', 'Create link') }}
805
- </button>
1066
+ <div v-if="dropErr" class="fe-share__warn">{{ dropErr }}</div>
806
1067
 
807
- <button type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-advtoggle" @click="dropShowAdv = !dropShowAdv">
808
- {{ dropShowAdv ? L('Gelişmiş ▲', 'Advanced ▲') : L('Gelişmiş ▼', 'Advanced ▼') }}
809
- </button>
810
- <div v-if="dropShowAdv" class="fx-perm-adv">
811
- <label class="fx-perm-adv-row">
812
- <span class="fx-perm-muted">{{ L('En fazla dosya', 'Max files') }}</span>
813
- <input v-model="dropMaxFiles" type="number" min="1" class="fx-perm-input fx-perm-input--sm" placeholder="20" />
814
- </label>
815
- <label class="fx-perm-adv-row">
816
- <span class="fx-perm-muted">{{ L('Dosya başı MB', 'MB / file') }}</span>
817
- <input v-model="dropMaxSizeMB" type="number" min="1" class="fx-perm-input fx-perm-input--sm" placeholder="500" />
818
- </label>
819
- <label class="fx-perm-adv-row">
820
- <span class="fx-perm-muted">{{ L('İzinli türler', 'Allowed types') }}</span>
821
- <input v-model="dropAllowedExt" type="text" class="fx-perm-input fx-perm-input--sm" :placeholder="L('hepsi (örn. pdf, jpg)', 'all (e.g. pdf, jpg)')" />
822
- </label>
823
- <label class="fx-perm-check">
824
- <input type="checkbox" v-model="dropAskName" />
825
- {{ L('Yükleyenin adını sor', 'Ask uploader name') }}
826
- </label>
827
- </div>
1068
+ <template v-if="dropResult">
1069
+ <div class="fe-share__linkrow">
1070
+ <a :href="dropResult.url" target="_blank" rel="noopener" class="fe-share__url" :title="dropResult.url">{{ dropResult.url }}</a>
1071
+ <button type="button" class="fe-share__copy" data-testid="drop-copy" @click="copy(dropResult.url, 'drop')">
1072
+ <span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
1073
+ <span>{{ copied === 'drop' ? t('access.copied') : t('access.copy') }}</span>
1074
+ </button>
1075
+ </div>
1076
+ <p class="fe-share__detail">
1077
+ {{ validUntil(dropResult) }}
1078
+ <span v-if="dropResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
1079
+ </p>
1080
+ <div v-if="dropResult.pin" class="fe-share__pinrow">
1081
+ <span class="fe-share__pinlabel">PIN</span>
1082
+ <code class="fe-share__pin">{{ dropResult.pin }}</code>
1083
+ <button type="button" class="fe-share__mini" @click="copy(dropResult.pin, 'droppin')">
1084
+ {{ copied === 'droppin' ? t('access.copied') : t('access.copy') }}
1085
+ </button>
1086
+ </div>
828
1087
 
829
- <div v-if="dropErr" class="fx-perm-warn">{{ dropErr }}</div>
1088
+ <!-- email the upload link to one or more people -->
1089
+ <div class="fe-share__mailrow">
1090
+ <input v-model="dropMailTo" type="text" class="fe-share__input" autocomplete="off"
1091
+ :placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendDropMail" />
1092
+ <button type="button" class="fe-share__btn" :disabled="dropMailBusy" @click="sendDropMail">
1093
+ {{ L('Gönder', 'Send') }}
1094
+ </button>
1095
+ </div>
1096
+ <div v-if="dropMailNotice" class="fe-share__notice">{{ dropMailNotice }}</div>
830
1097
 
831
- <div v-if="dropResult" class="fx-perm-result">
832
- <div class="fx-perm-linkrow">
833
- <a :href="dropResult.url" target="_blank" rel="noopener" class="fx-perm-link">{{ dropResult.url }}</a>
834
- <button class="fx-perm-btn fx-perm-btn--sm" @click="copy(dropResult.url, 'drop')">
835
- {{ copied === 'drop' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
1098
+ <!-- native share (OS share sheet) — same as the fishapp Share button -->
1099
+ <button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(dropBody())">
1100
+ <span aria-hidden="true" v-html="actionIconSvg('access')"></span>
1101
+ <span>{{ L('Paylaş', 'Share') }}</span>
836
1102
  </button>
837
- </div>
838
- <div class="fx-perm-muted fx-perm-validuntil">
839
- {{ validUntil(dropResult) }}
840
- <span v-if="dropResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
841
- </div>
842
- <div v-if="dropResult.pin" class="fx-perm-pin">
843
- <span>PIN: <code>{{ dropResult.pin }}</code></span>
844
- <button class="fx-perm-btn fx-perm-btn--sm" @click="copy(dropResult.pin, 'droppin')">
845
- {{ copied === 'droppin' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
846
- </button>
847
- </div>
1103
+ </template>
848
1104
 
849
- <!-- email the upload link to one or more people -->
850
- <div class="fx-perm-mailrow">
851
- <input v-model="dropMailTo" type="text" class="fx-perm-input" autocomplete="off"
852
- :placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) comma separated')" @keyup.enter="sendDropMail" />
853
- <button class="fx-perm-btn" :disabled="dropMailBusy" @click="sendDropMail">
854
- {{ L('Gönder', 'Send') }}
855
- </button>
1105
+ <div class="fe-share__list">
1106
+ <h4 class="fe-share__listhead">{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
1107
+ <p v-if="!dropShares.length" class="fe-share__empty">{{ L('Yok', 'None') }}</p>
1108
+ <div v-for="s in dropShares" :key="s.uuid" class="fe-share__row">
1109
+ <span class="fe-share__url" :title="s.url">{{ s.url }}</span>
1110
+ <button type="button" class="fe-share__mini" @click="copy(s.url, s.uuid)">
1111
+ {{ copied === s.uuid ? t('access.copied') : t('access.copy') }}
1112
+ </button>
1113
+ <button type="button" class="fe-share__del" :disabled="shareBusy" :title="L('İptal', 'Revoke')"
1114
+ :aria-label="L('İptal', 'Revoke')" @click="revoke(s)">
1115
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
1116
+ </button>
1117
+ </div>
856
1118
  </div>
857
- <div v-if="dropMailNotice" class="fx-perm-notice">{{ dropMailNotice }}</div>
858
-
859
- <!-- native share (OS share sheet) — same as the fishapp Share button -->
860
- <button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(dropBody())">
861
- <span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
862
- </button>
863
1119
  </div>
864
- </div>
865
- </template>
1120
+ </section>
1121
+ </div>
866
1122
  </div>
1123
+
1124
+ <footer class="fe-share__foot">
1125
+ <button type="button" class="fe-share__btn fe-share__btn--primary" data-testid="share-done" @click="emit('close')">
1126
+ {{ t('access.done') }}
1127
+ </button>
1128
+ </footer>
867
1129
  </div>
868
1130
  </div>
869
1131
  </template>
870
-
871
- <style>
872
- /* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to
873
- `.cls[data-v-HASH]`, and in the web-component build the hash baked into
874
- this CSS does not match the one Vue stamps onto the DOM — so every rule
875
- here silently stopped applying. Measured in the desktop app: the share
876
- dialog had `position: static`, no background and no radius, i.e. raw
877
- unstyled HTML, in EVERY embedded surface.
878
- Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so
879
- there is nothing here that can leak into a host page. */
880
- .fx-perm-overlay {
881
- position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5);
882
- display: flex; align-items: center; justify-content: center; z-index: 10000;
883
- font-family: var(--fe-font);
884
- }
885
- .fx-perm-modal {
886
- display: flex; flex-direction: column;
887
- background: var(--fe-bg); color: var(--fe-text);
888
- width: min(520px, 94vw); max-height: 86vh;
889
- border: 1px solid var(--fe-border); border-radius: var(--fe-radius-lg, 14px);
890
- box-shadow: var(--fe-shadow); font-size: 14px; overflow: hidden;
891
- }
892
- /* header + tabs are fixed; only the body scrolls */
893
- .fx-perm-head {
894
- display: flex; align-items: center; justify-content: space-between; gap: 10px;
895
- padding: 16px 18px 12px; flex: none;
896
- }
897
- .fx-perm-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
898
- .fx-perm-ico { font-size: 22px; line-height: 1; }
899
- .fx-perm-titletext { display: flex; flex-direction: column; min-width: 0; }
900
- .fx-perm-title h3 { margin: 0; font-size: 15px; font-weight: 600; color: var(--fe-text); }
901
- .fx-perm-sub {
902
- font-size: 12px; color: var(--fe-text); max-width: 340px;
903
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
904
- }
905
- .fx-perm-subdim { color: var(--fe-text-muted); }
906
- .fx-perm-x {
907
- background: none; border: none; font-size: 17px; cursor: pointer;
908
- color: var(--fe-text-muted); flex: none; line-height: 1; padding: 2px 4px; border-radius: 6px;
909
- }
910
- .fx-perm-x:hover { color: var(--fe-text); background: var(--fe-bg-hover); }
911
- .fx-perm-tabs { display: flex; gap: 2px; padding: 0 18px; border-bottom: 1px solid var(--fe-border); flex: none; }
912
- .fx-perm-tab {
913
- background: none; border: none; border-bottom: 2px solid transparent;
914
- padding: 8px 12px; cursor: pointer; color: var(--fe-text-muted); font-size: 13px;
915
- font-family: inherit; font-weight: 500;
916
- }
917
- .fx-perm-tab:hover { color: var(--fe-text); }
918
- .fx-perm-tab.is-active { color: var(--fe-primary); border-bottom-color: var(--fe-primary); }
919
- .fx-perm-body { flex: 1 1 auto; overflow-y: auto; padding: 14px 18px 18px; }
920
-
921
- .fx-perm-addcard {
922
- padding: 12px; border-radius: var(--fe-radius-md);
923
- background: var(--fe-bg-elev); border: 1px solid var(--fe-border);
924
- }
925
- .fx-perm-section { margin-top: 16px; }
926
- .fx-perm-section h4 {
927
- margin: 0 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fe-text-muted);
928
- }
929
- .fx-perm-empty { color: var(--fe-text-muted); font-size: 13px; padding: 8px 2px; }
930
-
931
- /* person rows */
932
- .fx-perm-person { display: flex; align-items: center; gap: 10px; padding: 7px 2px; }
933
- .fx-perm-person + .fx-perm-person { border-top: 1px solid var(--fe-border); }
934
- .fx-perm-inh { color: var(--fe-text-muted); }
935
- .fx-perm-av {
936
- width: 28px; height: 28px; flex: none; border-radius: 50%;
937
- display: flex; align-items: center; justify-content: center;
938
- background: var(--fe-primary); color: var(--fe-text-on-primary, #fff);
939
- font-size: 12px; font-weight: 600;
940
- }
941
- .fx-perm-av--dim { background: var(--fe-bg-hover); color: var(--fe-text-muted); }
942
- .fx-perm-user { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fe-text); min-width: 0; }
943
- .fx-perm-from { font-size: 11px; color: var(--fe-text-muted); white-space: nowrap; }
944
- .fx-perm-badge {
945
- font-size: 11px; padding: 2px 9px; border-radius: 999px;
946
- background: var(--fe-bg-hover); color: var(--fe-text);
947
- }
948
-
949
- /* inputs / selects / buttons */
950
- .fx-perm-sel, .fx-perm-input {
951
- padding: 7px 9px; border-radius: var(--fe-radius-sm, 7px); border: 1px solid var(--fe-border);
952
- background: var(--fe-bg); color: var(--fe-text); font-size: 13px; font-family: inherit;
953
- }
954
- .fx-perm-sel--sm { padding: 5px 7px; font-size: 12px; }
955
- .fx-perm-sel:focus, .fx-perm-input:focus { outline: none; border-color: var(--fe-primary); }
956
- .fx-perm-add { display: flex; gap: 8px; align-items: stretch; }
957
- .fx-perm-emailwrap { position: relative; flex: 1; min-width: 120px; }
958
- .fx-perm-input { width: 100%; box-sizing: border-box; }
959
- .fx-perm-suggest {
960
- position: absolute; top: calc(100% + 3px); left: 0; right: 0; z-index: 5; margin: 0; padding: 4px;
961
- list-style: none; background: var(--fe-bg); border: 1px solid var(--fe-border);
962
- border-radius: var(--fe-radius-sm, 7px); box-shadow: var(--fe-shadow); max-height: 210px; overflow: auto;
963
- }
964
- .fx-perm-suggest li { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
965
- .fx-perm-suggest li:hover { background: var(--fe-bg-hover); }
966
- .fx-perm-suggest-av {
967
- width: 24px; height: 24px; flex: none; border-radius: 50%; display: flex; align-items: center; justify-content: center;
968
- background: var(--fe-bg-hover); color: var(--fe-text); font-size: 11px; font-weight: 600;
969
- }
970
- .fx-perm-suggest-txt { display: flex; flex-direction: column; min-width: 0; }
971
- .fx-perm-suggest-name { color: var(--fe-text); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
972
- .fx-perm-suggest-meta { color: var(--fe-text-muted); font-size: 11px; }
973
- .fx-perm-check { display: inline-flex; gap: 6px; align-items: center; font-size: 13px; color: var(--fe-text); cursor: pointer; }
974
- .fx-perm-btn {
975
- padding: 7px 13px; border-radius: var(--fe-radius-sm, 7px); border: 1px solid var(--fe-border);
976
- background: var(--fe-bg); color: var(--fe-text); font-size: 13px; font-family: inherit; cursor: pointer; white-space: nowrap;
977
- }
978
- .fx-perm-btn:hover:not(:disabled) { background: var(--fe-bg-hover); }
979
- .fx-perm-btn:disabled { opacity: 0.55; cursor: default; }
980
- .fx-perm-btn--sm { padding: 5px 10px; font-size: 12px; }
981
- .fx-perm-btn--primary { background: var(--fe-primary); border-color: var(--fe-primary); color: var(--fe-text-on-primary, #fff); }
982
- .fx-perm-btn--primary:hover:not(:disabled) { background: var(--fe-primary-hover, var(--fe-primary)); filter: brightness(1.05); }
983
- .fx-perm-btn--ghost { background: none; border-color: transparent; color: var(--fe-primary); padding-left: 2px; }
984
- .fx-perm-btn--ghost:hover:not(:disabled) { background: none; text-decoration: underline; }
985
- .fx-perm-del { background: none; border: none; color: var(--fe-danger); cursor: pointer; font-size: 14px; flex: none; padding: 2px 4px; }
986
- .fx-perm-del:hover:not(:disabled) { filter: brightness(1.15); }
987
-
988
- /* invite sub-flow */
989
- .fx-perm-invite {
990
- margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--fe-border);
991
- }
992
- .fx-perm-invite-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
993
- .fx-perm-invite-row { display: flex; gap: 8px; align-items: stretch; }
994
- .fx-perm-invite-row .fx-perm-btn--primary { flex: 1; }
995
-
996
- /* share options + result
997
- ⚠ These options used to be one wrapping flex row with the primary button
998
- pushed to its right end by `margin-left: auto`. That works with two controls
999
- and breaks with three: at the modal's 520px the row wraps and the button
1000
- drops onto a line of its own, right-aligned and looking dislodged — which is
1001
- exactly what adding the download limit did. A two-column grid of fields with
1002
- the action underneath holds its shape whatever we add next. */
1003
- .fx-perm-share-opts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; align-items: end; }
1004
- .fx-perm-share-opts > .fx-perm-check { grid-column: 1 / -1; }
1005
- .fx-perm-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; min-width: 0; }
1006
- .fx-perm-field .fx-perm-sel { width: 100%; }
1007
- .fx-perm-create { width: 100%; margin-top: 10px; }
1008
- @media (max-width: 380px) { .fx-perm-share-opts { grid-template-columns: 1fr; } }
1009
- .fx-perm-result { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--fe-border); }
1010
- .fx-perm-linkrow { display: flex; gap: 8px; align-items: center; }
1011
- .fx-perm-link { color: var(--fe-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; text-decoration: none; }
1012
- .fx-perm-link:hover { text-decoration: underline; }
1013
- .fx-perm-pin { margin: 8px 0 0; font-size: 13px; color: var(--fe-text); display: flex; align-items: center; gap: 8px; }
1014
- .fx-perm-pin code, .fx-perm-reveal code { font-family: var(--fe-font-mono, monospace); background: var(--fe-bg-hover); padding: 1px 6px; border-radius: 5px; }
1015
- /* one-line curl for the fresh link */
1016
- .fx-perm-cli { margin-top: 10px; }
1017
- .fx-perm-clilabel { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fe-text-muted); margin-bottom: 4px; }
1018
- .fx-perm-clirow { display: flex; gap: 8px; align-items: center; }
1019
- .fx-perm-clicmd {
1020
- flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1021
- font-family: var(--fe-font-mono, monospace); font-size: 12px;
1022
- background: var(--fe-bg-hover); color: var(--fe-text);
1023
- padding: 6px 8px; border-radius: var(--fe-radius-sm, 7px);
1024
- }
1025
- .fx-perm-mailrow { display: flex; gap: 8px; margin-top: 10px; }
1026
- .fx-perm-mailrow .fx-perm-input { flex: 1; }
1027
- .fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
1028
- .fx-perm-ttlhint { margin: 4px 0 8px; }
1029
- .fx-perm-validuntil { margin: 4px 0 6px; }
1030
- /* native "Share" button — sits under the mail row, full width */
1031
- .fx-perm-sharebtn { display: flex; width: 100%; align-items: center; justify-content: center; gap: 6px; margin-top: 8px; }
1032
-
1033
- /* file-drop tab */
1034
- .fx-perm-dropintro { margin: 0 0 12px; line-height: 1.4; }
1035
- .fx-perm-advtoggle { margin-top: 10px; padding-left: 2px; }
1036
- .fx-perm-adv {
1037
- margin-top: 8px; padding-top: 10px; border-top: 1px dashed var(--fe-border);
1038
- display: flex; flex-direction: column; gap: 8px;
1039
- }
1040
- .fx-perm-adv-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; }
1041
- .fx-perm-input--sm { width: 130px; padding: 5px 8px; font-size: 12px; }
1042
-
1043
- .fx-perm-warn {
1044
- background: rgba(245, 158, 11, 0.14); border: 1px solid rgba(245, 158, 11, 0.4);
1045
- border-radius: var(--fe-radius-sm, 7px); padding: 8px 10px; font-size: 13px; margin-bottom: 10px; color: var(--fe-text);
1046
- }
1047
- .fx-perm-notice { margin-top: 8px; font-size: 13px; color: var(--fe-text-muted); }
1048
- .fx-perm-reveal { margin-top: 8px; font-size: 13px; word-break: break-all; color: var(--fe-text); }
1049
- .fx-perm-muted { color: var(--fe-text-muted); font-size: 13px; }
1050
- </style>