@brftech/filex-core 0.39.1 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +32 -25
  2. package/dist/ArchiveViewer-BNqukFg8.js +124 -0
  3. package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
  4. package/dist/CsvViewer-ii_-MgmG.js +141 -0
  5. package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
  6. package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
  7. package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
  8. package/dist/EpubViewer-p4B8iiWb.js +163 -0
  9. package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
  10. package/dist/IpynbViewer-D_qYRJhu.js +184 -0
  11. package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
  12. package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
  13. package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
  14. package/dist/PsdViewer-CuV-OPSd.js +122 -0
  15. package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
  16. package/dist/TiffViewer-BLeoF4Be.js +142 -0
  17. package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
  18. package/dist/Viewer3D-DHhhU76E.js +75 -0
  19. package/dist/Viewer3D-DHhhU76E.js.map +1 -0
  20. package/dist/filex-core.js +217 -20077
  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-BraG7Cz4.js +26664 -0
  25. package/dist/index-BraG7Cz4.js.map +1 -0
  26. package/dist/index.d.ts +4276 -851
  27. package/dist/style.css +1 -1
  28. package/package.json +1 -1
  29. package/src/FileExplorer.vue +2170 -492
  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 +57 -2
  35. package/src/components/EncryptedFolderModal.vue +31 -25
  36. package/src/components/FilePane.vue +939 -0
  37. package/src/components/FilterBar.vue +568 -72
  38. package/src/components/GalleryView.vue +162 -30
  39. package/src/components/GridView.vue +322 -58
  40. package/src/components/HomeView.vue +331 -0
  41. package/src/components/InspectorPanel.vue +296 -53
  42. package/src/components/ListView.vue +1158 -167
  43. package/src/components/NFSExportsPanel.vue +7 -4
  44. package/src/components/OnboardingTour.vue +156 -33
  45. package/src/components/QuickLook.vue +34 -5
  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 +43 -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 +127 -0
  58. package/src/components/TimeZonePicker.vue +696 -0
  59. package/src/components/TokensPanel.vue +16 -4
  60. package/src/components/Toolbar.vue +965 -469
  61. package/src/components/ViewSwitcher.vue +9 -4
  62. package/src/composables/useConnections.ts +35 -4
  63. package/src/composables/useExplorerTimeZone.ts +85 -0
  64. package/src/composables/useFileApi.ts +53 -4
  65. package/src/composables/useKeyboardShortcuts.ts +122 -0
  66. package/src/composables/useLocale.ts +365 -25
  67. package/src/index.ts +182 -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 +11 -1
  78. package/src/lib/sortOrder.ts +459 -0
  79. package/src/lib/themes.ts +152 -23
  80. package/src/lib/timezone.ts +456 -0
  81. package/src/lib/uiProfile.ts +82 -0
  82. package/src/lib/viewPrefs.ts +1036 -0
  83. package/src/locales/en.ts +432 -25
  84. package/src/locales/tr.ts +425 -25
  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 +543 -473
  91. package/src/modals/PreviewModal.vue +516 -156
  92. package/src/styles/base.css +5435 -8
  93. package/src/styles/variables.css +140 -60
  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,41 @@
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).
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
+ */
7
30
  import { ref, onMounted, onBeforeUnmount, computed, 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
34
  import { STOCK_EXPIRY_DAYS, clampExpiryOptions, defaultExpiryDays, ttlCeilingHint, validUntilLine } from '../lib/shareTtl';
12
35
  import { resolveLocale } from '../locales/resolve';
36
+ import { formatByteSize, useLocale } from '../composables/useLocale';
37
+ import { actionIconSvg } from '../lib/actionIcons';
38
+ import { fileIconTile } from '../lib/fileIcons';
13
39
 
14
40
  const props = defineProps<{
15
41
  api: FileApi;
@@ -21,23 +47,24 @@ const props = defineProps<{
21
47
  * undefined/0 = no ceiling. The expiry choices are derived from it. */
22
48
  shareMaxTtlDays?: number;
23
49
  /**
24
- * surucu:d1 — which tab this modal opens on. Absent = 'perms', the behaviour
25
- * every existing caller has.
50
+ * surucu:d1 — which part of the dialog the caller wants opened. Absent =
51
+ * 'perms', the behaviour every existing caller has.
26
52
  *
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.
53
+ * The drive shell's "Request files" needs 'drop': the file-drop link lives
54
+ * in this dialog, and a menu entry that opened it anywhere else would be a
55
+ * menu entry that lied about what it does.
31
56
  *
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.
57
+ * ⚠ A hint, not a lock: `load()` still falls back to the link tier when the
58
+ * caller turns out not to be an owner, because the grants section is not
59
+ * theirs to see.
35
60
  */
36
61
  initialTab?: 'perms' | 'share' | 'drop';
37
62
  }>();
38
63
  const emit = defineEmits<{ (e: 'close'): void }>();
39
64
 
40
- const tr = computed(() => (resolveLocale(props.locale)) !== 'en');
65
+ const localeCode = computed(() => resolveLocale(props.locale));
66
+ const { t } = useLocale(() => localeCode.value);
67
+ const tr = computed(() => localeCode.value !== 'en');
41
68
  function L(t: string, e: string): string {
42
69
  return tr.value ? t : e;
43
70
  }
@@ -50,10 +77,23 @@ const pathParts = computed(() => {
50
77
  const segs = rel.split('/').filter(Boolean);
51
78
  return { adapter, name: segs.length ? segs[segs.length - 1] : adapter, rel };
52
79
  });
80
+ // The type tile beside the title — the same badge the listing draws, so the
81
+ // dialog is visibly about the row the user right-clicked.
82
+ const titleTile = computed(() => {
83
+ const name = pathParts.value.name;
84
+ const dot = name.lastIndexOf('.');
85
+ return fileIconTile({
86
+ type: props.isDir ? 'dir' : 'file',
87
+ extension: !props.isDir && dot > 0 ? name.slice(dot + 1) : '',
88
+ });
89
+ });
53
90
 
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
91
+ type Section = 'link' | 'people' | 'drop';
92
+ const open = ref<Record<Section, boolean>>({ link: false, people: false, drop: false });
93
+ function toggleSection(s: Section) {
94
+ open.value = { ...open.value, [s]: !open.value[s] };
95
+ }
96
+ const canManage = ref(false); // owner/admin → can see the grants section
57
97
 
58
98
  // ── permissions state ──
59
99
  const loading = ref(true);
@@ -94,6 +134,23 @@ const shareMailTo = ref('');
94
134
  const shareMailBusy = ref(false);
95
135
  const shareMailNotice = ref('');
96
136
 
137
+ /**
138
+ * ⚠ `listShares` returns BOTH kinds of link — a download link (`/s/…`) and a
139
+ * file-drop link (`/d/…`) are the same row with a different `kind`. They used
140
+ * to be listed together under "Existing links", which meant an upload link
141
+ * appeared in the download tab and the "link sharing" state could not be read
142
+ * off the list at all. Split at the source: each section owns its own kind.
143
+ *
144
+ * `kind` is absent from the `ShareInfo` type but present on the wire (see
145
+ * backend/internal/api/handlers/share.go), so it is read through a narrow cast
146
+ * rather than by widening a shared type this dialog does not own.
147
+ */
148
+ function shareKind(s: ShareInfo): string {
149
+ return (s as ShareInfo & { kind?: string }).kind ?? 'download';
150
+ }
151
+ const downloadShares = computed(() => shares.value.filter((s) => shareKind(s) !== 'drop'));
152
+ const dropShares = computed(() => shares.value.filter((s) => shareKind(s) === 'drop'));
153
+
97
154
  // ⚠ Derived from the server's ceiling, never a fixed list: offering "30 days"
98
155
  // on a server that keeps links for 7 would show a choice that is not one.
99
156
  // The same helper drives every surface (see lib/shareTtl.ts).
@@ -195,14 +252,15 @@ async function reload() {
195
252
  storageRbac.value = r.storage_rbac;
196
253
  canManage.value = true;
197
254
  } catch (e) {
198
- // 403 = caller is editor (not owner): no permissions tab, share only.
255
+ // 403 = caller is editor (not owner): no grants section, link only.
199
256
  const st = (e as { status?: number }).status;
200
257
  if (st === 403) {
201
258
  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';
259
+ // Editor (not owner): the grants section is not theirs close it and
260
+ // leave the link tier, which is the whole dialog for them.
261
+ // ⚠ Unless the caller asked for the drop link, which an editor may
262
+ // perfectly well mint.
263
+ open.value = { ...open.value, people: false };
206
264
  } else {
207
265
  err.value = e instanceof Error ? e.message : String(e);
208
266
  }
@@ -218,12 +276,29 @@ async function reloadShares() {
218
276
  shares.value = [];
219
277
  }
220
278
  }
279
+ function onKey(e: KeyboardEvent) {
280
+ if (e.key === 'Escape') emit('close');
281
+ }
221
282
  onMounted(async () => {
283
+ // surucu:d1 — the caller's entry point decides which section opens with the
284
+ // dialog.
285
+ //
286
+ // ⚠ The DEFAULT (no `initialTab`, i.e. the plain "Share / Permissions"
287
+ // action) now opens NOTHING. It used to land on the permissions tab, which
288
+ // meant the common case — turn the link on, copy it — arrived buried under a
289
+ // grant editor. The link tier is the answer to the default question; the
290
+ // grants are one click away with their own state ("2 people") readable from
291
+ // the closed heading. A caller that explicitly wants the grants still says
292
+ // so, and "Request files" still lands on the drop link.
293
+ if (props.initialTab === 'drop') open.value = { ...open.value, drop: true };
294
+ else if (props.initialTab === 'perms') open.value = { ...open.value, people: true };
295
+ document.addEventListener('keydown', onKey);
222
296
  await reload();
223
297
  await reloadShares();
224
298
  });
225
299
  onBeforeUnmount(() => {
226
300
  if (searchTimer) clearTimeout(searchTimer);
301
+ document.removeEventListener('keydown', onKey);
227
302
  });
228
303
 
229
304
  function onEmailInput() {
@@ -315,13 +390,14 @@ async function inviteCreateUser() {
315
390
  busy.value = false;
316
391
  }
317
392
  }
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.
393
+ // "Just send a share link" → open the link options with the address prefilled
394
+ // so the owner creates a link (with their chosen expiry/PIN) and mails it
395
+ // from there.
320
396
  function gotoShareWithMail() {
321
397
  shareMailTo.value = email.value.trim().toLowerCase();
322
398
  noAccount.value = false;
323
399
  notice.value = '';
324
- tab.value = 'share';
400
+ open.value = { ...open.value, link: true };
325
401
  }
326
402
  async function changeLevel(g: Grant, newLevel: string) {
327
403
  if (newLevel === g.level) return;
@@ -375,6 +451,39 @@ async function createLink() {
375
451
  shareBusy.value = false;
376
452
  }
377
453
  }
454
+
455
+ /**
456
+ * gorunum:v2-share — the switch.
457
+ *
458
+ * ON with nothing minted mints one with whatever the options tier currently
459
+ * says (that is the default: the server's ceiling as the expiry, no PIN, no
460
+ * cap). OFF revokes every DOWNLOAD link on this item, because that is what
461
+ * "link sharing is off" has to mean — a switch that left a live link behind
462
+ * would be the most dangerous control in the dialog.
463
+ *
464
+ * ⚠ Drop links are deliberately untouched: they are an inbound door managed by
465
+ * its own section, and revoking someone's upload invitation because the owner
466
+ * turned off downloads is not what either control says.
467
+ */
468
+ async function toggleLink() {
469
+ if (shareBusy.value) return;
470
+ if (linkOn.value) {
471
+ shareBusy.value = true;
472
+ shareErr.value = '';
473
+ try {
474
+ for (const s of downloadShares.value) await props.api.revokeShare(s.uuid);
475
+ shareResult.value = null;
476
+ await reloadShares();
477
+ } catch (e) {
478
+ shareErr.value = e instanceof Error ? e.message : String(e);
479
+ } finally {
480
+ shareBusy.value = false;
481
+ }
482
+ return;
483
+ }
484
+ await createLink();
485
+ }
486
+
378
487
  async function sendShareMail() {
379
488
  const list = splitEmails(shareMailTo.value);
380
489
  if (!list.length) {
@@ -437,6 +546,7 @@ async function createDropLink() {
437
546
  expiresAt: r.share.expires_at ?? null,
438
547
  clamped: !!r.share.expiry_clamped,
439
548
  };
549
+ await reloadShares();
440
550
  } catch (e) {
441
551
  dropErr.value = e instanceof Error ? e.message : String(e);
442
552
  } finally {
@@ -475,8 +585,15 @@ async function sendDropMail() {
475
585
  }
476
586
  async function revoke(s: ShareInfo) {
477
587
  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); }
588
+ try {
589
+ await props.api.revokeShare(s.uuid);
590
+ // ⚠ The lead tier reads `shareResult` first. Revoking the link that is
591
+ // showing up there has to clear it too, or the dialog goes on offering a
592
+ // Copy button — and a switch reading ON — for a link the server has just
593
+ // thrown away.
594
+ if (shareResult.value && shareResult.value.url === s.url) shareResult.value = null;
595
+ await reloadShares();
596
+ } catch (e) { shareErr.value = e instanceof Error ? e.message : String(e); }
480
597
  finally { shareBusy.value = false; }
481
598
  }
482
599
  function copy(text: string, tag = 'url') {
@@ -493,15 +610,24 @@ function copy(text: string, tag = 'url') {
493
610
  // same as an emailed link.
494
611
  const canShare = computed(() => typeof navigator !== 'undefined' && typeof navigator.share === 'function');
495
612
 
496
- // humanSize mirrors the Go humanSize() used in the email body (1.4 MB).
613
+ /**
614
+ * The size line in the share message, spelled the way the SERVER spells it.
615
+ *
616
+ * ⚠ This one really does have to mirror `humanSize()` in
617
+ * backend/internal/api/handlers/mail_templates.go — the same file forwarded
618
+ * by e-mail and by the OS share sheet must not carry two different sizes. So
619
+ * the mirroring is stated as arguments to the one formatter (1024, always one
620
+ * decimal, a dot and English letters, because that is what Go's `%.1f %cB`
621
+ * prints) instead of being a fifth private copy of the arithmetic.
622
+ *
623
+ * ⚠ It is therefore the one place in the UI that is NOT decimal, so a share
624
+ * message reads "1.4 MB" where the listing row reads "1.5 MB". The register
625
+ * entry for this was right about the real fix: the e-mail should render from
626
+ * one source rather than the UI copying its rounding.
627
+ */
497
628
  function humanSize(b?: number): string {
498
629
  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`;
630
+ return formatByteSize(b, { base: 1024, digits: 'fixed1', numberLocale: null });
505
631
  }
506
632
 
507
633
  function expiryLine(days: number): string {
@@ -514,6 +640,60 @@ function validUntil(r: { expiresAt?: string | null } | null): string {
514
640
  return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en');
515
641
  }
516
642
 
643
+ /* ── the top tier: one switch, one sentence, one link ──────────────────── */
644
+
645
+ /** The link the header row shows: the one just minted, else the oldest live one. */
646
+ const primaryLink = computed(() => shareResult.value?.url ?? downloadShares.value[0]?.url ?? '');
647
+ const linkOn = computed(() => !!primaryLink.value);
648
+
649
+ /**
650
+ * The plain sentence. It says who can open the item RIGHT NOW, which is the
651
+ * question the dialog exists to answer.
652
+ *
653
+ * ⚠ The PIN claim is only made about a link this dialog minted: `listShares`
654
+ * does not return `password_pin` (by design — the server will not re-serve a
655
+ * PIN), so for a link that was already there we say how many exist rather than
656
+ * guessing at its protection. A confident wrong sentence about who can read a
657
+ * file is worse than a vaguer true one.
658
+ */
659
+ const whoLine = computed(() => {
660
+ if (!linkOn.value) return t('access.who.private');
661
+ if (shareResult.value) return shareResult.value.pin ? t('access.who.pin') : t('access.who.link');
662
+ return t('access.who.existing', { n: downloadShares.value.length });
663
+ });
664
+
665
+ /** The muted second line under it: what the server actually stored. */
666
+ const linkDetail = computed(() => {
667
+ const bits: string[] = [];
668
+ if (shareResult.value) {
669
+ bits.push(validUntil(shareResult.value));
670
+ if (shareResult.value.clamped) bits.push(L('(sunucu sınırı uygulandı)', '(server limit applied)'));
671
+ if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
672
+ } else if (downloadShares.value[0]) {
673
+ const s = downloadShares.value[0];
674
+ bits.push(validUntilLine(s.expires_at ?? null, tr.value ? 'tr' : 'en'));
675
+ if (s.max_downloads) bits.push(maxDlOptions.find((o) => o.v === s.max_downloads)?.l ?? String(s.max_downloads));
676
+ }
677
+ return bits.filter(Boolean).join(' · ');
678
+ });
679
+
680
+ /* Section summaries — a named section still has to say what is inside it, or
681
+ the reader has to open all three to find the control they came for. */
682
+ const linkSummary = computed(() => {
683
+ const bits: string[] = [];
684
+ bits.push(sharePwd.value ? t('access.sum.pin_on') : t('access.sum.pin_off'));
685
+ bits.push(expiryLabel(shareExpiry.value));
686
+ if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
687
+ return bits.filter(Boolean).join(' · ');
688
+ });
689
+ const peopleSummary = computed(() => {
690
+ const n = direct.value.length + inherited.value.length;
691
+ return n ? t('access.sum.people', { n }) : t('access.sum.people_none');
692
+ });
693
+ const dropSummary = computed(() =>
694
+ dropShares.value.length ? t('access.sum.drop', { n: dropShares.value.length }) : t('access.sum.drop_none'),
695
+ );
696
+
517
697
  // Text + title for a download-share link, mirroring shareMailText().
518
698
  function shareBody(): { title: string; text: string } {
519
699
  const name = pathParts.value.name;
@@ -573,478 +753,368 @@ async function nativeShare(body: { title: string; text: string }) {
573
753
  </script>
574
754
 
575
755
  <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>
756
+ <div class="fx-perm-overlay fe-share__scrim" @click.self="emit('close')">
757
+ <!-- ⚠ `fx-perm-modal` is kept alongside the new class: e2e/shots/capture.mjs
758
+ and the embedders' own harnesses address the dialog by it. -->
759
+ <div class="fx-perm-modal fe-share" role="dialog" aria-modal="true" :aria-label="t('access.title', { name: pathParts.name })">
760
+ <header class="fe-share__head">
761
+ <span class="fe-share__tile" aria-hidden="true" v-html="titleTile"></span>
762
+ <div class="fe-share__headtext">
763
+ <h3 class="fe-share__title">{{ t('access.title', { name: pathParts.name }) }}</h3>
764
+ <span class="fe-share__path" :title="path">{{ pathParts.adapter }}</span>
585
765
  </div>
586
- <button class="fx-perm-x" @click="emit('close')" aria-label="close">✕</button>
766
+ <button type="button" class="fe-share__close" :aria-label="t('access.close')" :title="t('access.close')" @click="emit('close')">
767
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
768
+ </button>
587
769
  </header>
588
770
 
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.') }}
771
+ <div class="fe-share__body">
772
+ <!-- ───────── tier 1: the link, the whole reason people open this ───── -->
773
+ <div class="fe-share__lead">
774
+ <div class="fe-share__switchrow">
775
+ <span class="fe-share__leadicon" aria-hidden="true" v-html="actionIconSvg('access')"></span>
776
+ <span class="fe-share__leadlabel">{{ t('access.link.switch') }}</span>
777
+ <button
778
+ type="button"
779
+ class="fe-share__switch"
780
+ role="switch"
781
+ :aria-checked="linkOn ? 'true' : 'false'"
782
+ :aria-label="t('access.link.switch')"
783
+ data-testid="share-switch"
784
+ :disabled="shareBusy"
785
+ @click="toggleLink"
786
+ ><span class="fe-share__knob"></span></button>
614
787
  </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>
788
+ <p class="fe-share__who" data-testid="share-who">{{ whoLine }}</p>
789
+
790
+ <template v-if="linkOn">
791
+ <div class="fe-share__linkrow">
792
+ <a :href="primaryLink" target="_blank" rel="noopener" class="fe-share__url" :title="primaryLink">{{ primaryLink }}</a>
793
+ <button type="button" class="fe-share__copy" data-testid="share-copy" @click="copy(primaryLink, 'new')">
794
+ <span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
795
+ <span>{{ copied === 'new' ? t('access.copied') : t('access.copy') }}</span>
796
+ </button>
797
+ </div>
798
+ <p v-if="linkDetail" class="fe-share__detail" data-testid="share-valid-until">{{ linkDetail }}</p>
799
+ <div v-if="shareResult?.pin" class="fe-share__pinrow">
800
+ <span class="fe-share__pinlabel">PIN</span>
801
+ <code class="fe-share__pin">{{ shareResult?.pin }}</code>
802
+ <button type="button" class="fe-share__mini" @click="copy(shareResult?.pin ?? '', 'sharepin')">
803
+ {{ copied === 'sharepin' ? t('access.copied') : t('access.copy') }}
804
+ </button>
805
+ </div>
806
+ </template>
807
+ <div v-if="shareErr" class="fe-share__warn">{{ shareErr }}</div>
808
+ </div>
809
+
810
+ <!-- ───────── tier 2: named sections, one click each ───────────────── -->
811
+ <div class="fe-share__sections">
812
+ <!-- Link options: PIN · expiry · download cap · curl · e-mail · links -->
813
+ <section class="fe-share__section">
814
+ <button
815
+ type="button"
816
+ class="fe-share__sechead"
817
+ :aria-expanded="open.link ? 'true' : 'false'"
818
+ data-testid="share-options-toggle"
819
+ @click="toggleSection('link')"
820
+ >
821
+ <svg class="fe-share__caret" :class="{ 'is-open': open.link }" viewBox="0 0 24 24" fill="none"
822
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
823
+ aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
824
+ <span class="fe-share__seclabel">{{ t('access.section.link') }}</span>
825
+ <span class="fe-share__secmeta">{{ linkSummary }}</span>
826
+ </button>
827
+ <div v-if="open.link" class="fe-share__panel">
828
+ <div class="fe-share__opts">
829
+ <label class="fe-share__check">
830
+ <input type="checkbox" v-model="sharePwd" />
831
+ <span>{{ L('PIN ile koru', 'Protect with a PIN') }}</span>
832
+ </label>
833
+ <label class="fe-share__field">
834
+ <span class="fe-share__fieldlabel">{{ L('Süre', 'Expiry') }}</span>
835
+ <select v-model.number="shareExpiry" class="fe-share__select" data-testid="share-expiry">
836
+ <option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
837
+ </select>
838
+ </label>
839
+ <label class="fe-share__field">
840
+ <span class="fe-share__fieldlabel">{{ L('İndirme limiti', 'Download limit') }}</span>
841
+ <select v-model.number="shareMaxDl" class="fe-share__select">
842
+ <option v-for="o in maxDlOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
843
+ </select>
844
+ </label>
845
+ </div>
846
+ <p v-if="ttlHint" class="fe-share__hint" data-testid="share-ttl-hint">{{ ttlHint }}</p>
847
+ <button type="button" class="fx-perm-create fe-share__btn fe-share__btn--primary fe-share__btn--wide" :disabled="shareBusy" @click="createLink">
848
+ {{ L('Bağlantı oluştur', 'Create link') }}
849
+ </button>
850
+
851
+ <p v-if="!shareResult && shareMailTo" class="fe-share__hint">
852
+ {{ L('Bir bağlantı oluşturun, ardından', 'Create a link, then it will be sent to') }}
853
+ <strong>{{ shareMailTo }}</strong> {{ L('adresine gönderin.', '.') }}
854
+ </p>
855
+
856
+ <template v-if="shareResult">
857
+ <!-- one-line download command, for pulling the file onto a server -->
858
+ <div class="fx-perm-cli fe-share__cli">
859
+ <span class="fe-share__fieldlabel">{{ L('Komut satırı', 'Command line') }}</span>
860
+ <div class="fe-share__clirow">
861
+ <code class="fe-share__clicmd" :title="shareCli">{{ shareCli }}</code>
862
+ <button type="button" class="fe-share__mini" @click="copy(shareCli, 'sharecli')">
863
+ {{ copied === 'sharecli' ? t('access.copied') : t('access.copy') }}
864
+ </button>
865
+ </div>
634
866
  </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') }}
867
+
868
+ <!-- send by email (one or more, comma/space separated) -->
869
+ <div class="fe-share__mailrow">
870
+ <input v-model="shareMailTo" type="text" class="fe-share__input" autocomplete="off"
871
+ :placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendShareMail" />
872
+ <button type="button" class="fe-share__btn" :disabled="shareMailBusy" @click="sendShareMail">
873
+ {{ L('Gönder', 'Send') }}
874
+ </button>
875
+ </div>
876
+ <div v-if="shareMailNotice" class="fe-share__notice">{{ shareMailNotice }}</div>
877
+
878
+ <!-- native share (OS share sheet) — same as the fishapp Share button -->
879
+ <button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(shareBody())">
880
+ <span aria-hidden="true" v-html="actionIconSvg('access')"></span>
881
+ <span>{{ L('Paylaş', 'Share') }}</span>
640
882
  </button>
883
+ </template>
884
+
885
+ <div class="fe-share__list">
886
+ <h4 class="fe-share__listhead">{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
887
+ <p v-if="!downloadShares.length" class="fe-share__empty">{{ L('Yok', 'None') }}</p>
888
+ <div v-for="s in downloadShares" :key="s.uuid" class="fe-share__row">
889
+ <span class="fe-share__url" :title="s.url">{{ s.url }}</span>
890
+ <button type="button" class="fe-share__mini" @click="copy(s.url, s.uuid)">
891
+ {{ copied === s.uuid ? t('access.copied') : t('access.copy') }}
892
+ </button>
893
+ <button type="button" class="fe-share__del" :disabled="shareBusy" :title="L('İptal', 'Revoke')"
894
+ :aria-label="L('İptal', 'Revoke')" @click="revoke(s)">
895
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
896
+ </button>
897
+ </div>
641
898
  </div>
899
+ </div>
900
+ </section>
642
901
 
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">
902
+ <!-- People with access: the per-user / inherited grants -->
903
+ <section v-if="canManage" class="fe-share__section">
904
+ <button
905
+ type="button"
906
+ class="fe-share__sechead"
907
+ :aria-expanded="open.people ? 'true' : 'false'"
908
+ data-testid="share-people-toggle"
909
+ @click="toggleSection('people')"
910
+ >
911
+ <svg class="fe-share__caret" :class="{ 'is-open': open.people }" viewBox="0 0 24 24" fill="none"
912
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
913
+ aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
914
+ <span class="fe-share__seclabel">{{ t('access.section.people') }}</span>
915
+ <span class="fe-share__secmeta">{{ peopleSummary }}</span>
916
+ </button>
917
+ <div v-if="open.people" class="fe-share__panel">
918
+ <div v-if="!storageRbac" class="fe-share__warn">
919
+ {{ 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.') }}
920
+ </div>
921
+ <div v-if="err" class="fe-share__warn">{{ err }}</div>
922
+ <p v-if="loading" class="fe-share__hint">{{ L('Yükleniyor…', 'Loading…') }}</p>
923
+ <template v-else>
924
+ <div class="fe-share__add">
925
+ <div class="fe-share__emailwrap">
926
+ <input v-model="email" type="email" class="fe-share__input" autocomplete="off"
927
+ :placeholder="L('İsim veya e-posta', 'Name or email')"
928
+ @input="onEmailInput" @keyup.enter="submitEmail" @focus="onEmailInput" />
929
+ <ul v-if="showSuggest" class="fe-share__suggest">
930
+ <li v-for="u in suggestions" :key="u.id" @mousedown.prevent="pickUser(u)">
931
+ <span class="fe-share__av fe-share__av--sm">{{ (u.display_name || u.email).charAt(0).toUpperCase() }}</span>
932
+ <span class="fe-share__suggesttxt">
933
+ <span class="fe-share__suggestname">{{ u.display_name || u.email }}</span>
934
+ <span class="fe-share__suggestmeta">{{ u.email }} · {{ u.role }}</span>
935
+ </span>
936
+ </li>
937
+ </ul>
938
+ </div>
939
+ <select v-model="level" class="fe-share__select" :title="levels.find(o => o.v === level)?.d">
940
+ <option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
941
+ </select>
942
+ <button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="submitEmail">
943
+ {{ L('Ekle', 'Add') }}
944
+ </button>
945
+ </div>
946
+
947
+ <div v-if="noAccount" class="fe-share__invite">
948
+ <p class="fe-share__hint">{{ L('Bu e-postada hesap yok. Ne yapmak istersiniz?', 'No account for this email — what next?') }}</p>
949
+ <div class="fe-share__inviterow">
950
+ <select v-model="createRole" class="fe-share__select">
648
951
  <option value="user">{{ L('Kullanıcı', 'User') }}</option>
649
952
  <option value="viewer">{{ L('Görüntüleyen', 'Viewer') }}</option>
650
953
  </select>
651
- <button class="fx-perm-btn fx-perm-btn--primary" :disabled="busy" @click="inviteCreateUser">
954
+ <button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="inviteCreateUser">
652
955
  {{ L('Kullanıcı oluştur + yetki ver', 'Create user + grant') }}
653
956
  </button>
654
957
  </div>
655
- <button class="fx-perm-btn fx-perm-btn--ghost" :disabled="busy" @click="gotoShareWithMail">
958
+ <button type="button" class="fe-share__linkbtn" :disabled="busy" @click="gotoShareWithMail">
656
959
  {{ L('Sadece paylaşım linki gönder →', 'Just send a share link →') }}
657
960
  </button>
658
961
  </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>
962
+ <div v-if="notice" class="fe-share__notice">{{ notice }}</div>
963
+ <div v-if="inviteResult?.tempPassword" class="fe-share__notice">
964
+ {{ L('Geçici parola:', 'Temp password:') }} <code class="fe-share__pin">{{ inviteResult.tempPassword }}</code>
965
+ </div>
665
966
 
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>
967
+ <div class="fe-share__list">
968
+ <h4 class="fe-share__listhead">{{ L('Erişimi olanlar', 'People with access') }}</h4>
969
+ <p v-if="!direct.length && !inherited.length" class="fe-share__empty">
970
+ {{ L('Henüz kimseyle paylaşılmadı.', 'Not shared with anyone yet.') }}
971
+ </p>
972
+ <div v-for="g in direct" :key="'d' + g.id" class="fe-share__row">
973
+ <span class="fe-share__av">{{ ginitial(g) }}</span>
974
+ <span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
975
+ <select class="fe-share__select fe-share__select--sm" :value="g.level"
976
+ @change="changeLevel(g, ($event.target as HTMLSelectElement).value)">
977
+ <option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
978
+ </select>
979
+ <button type="button" class="fe-share__del" :disabled="busy" :title="L('Kaldır', 'Remove')"
980
+ :aria-label="L('Kaldır', 'Remove')" @click="removeGrant(g)">
981
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
982
+ </button>
983
+ </div>
984
+ <div v-for="g in inherited" :key="'i' + g.id" class="fe-share__row fe-share__row--dim">
985
+ <span class="fe-share__av fe-share__av--dim">{{ ginitial(g) }}</span>
986
+ <span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
987
+ <span class="fe-share__badge">{{ levelLabel(g.level) }}</span>
988
+ <span class="fe-share__from" :title="L('Üst klasörden gelir', 'Inherited from') + ': ' + (g.path_prefix || '/')">
989
+ {{ g.path_prefix || '/' }}
990
+ </span>
991
+ </div>
992
+ </div>
993
+ </template>
689
994
  </div>
690
- </template>
691
- </template>
995
+ </section>
692
996
 
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') }}
997
+ <!-- Request files: the inbound drop link (folders only) -->
998
+ <section v-if="isDir" class="fe-share__section">
999
+ <button
1000
+ type="button"
1001
+ class="fe-share__sechead"
1002
+ :aria-expanded="open.drop ? 'true' : 'false'"
1003
+ data-testid="share-drop-toggle"
1004
+ @click="toggleSection('drop')"
1005
+ >
1006
+ <svg class="fe-share__caret" :class="{ 'is-open': open.drop }" viewBox="0 0 24 24" fill="none"
1007
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
1008
+ aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
1009
+ <span class="fe-share__seclabel">{{ t('access.section.drop') }}</span>
1010
+ <span class="fe-share__secmeta">{{ dropSummary }}</span>
717
1011
  </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>
1012
+ <div v-if="open.drop" class="fe-share__panel">
1013
+ <p class="fe-share__hint">
1014
+ {{ 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.') }}
1015
+ </p>
1016
+ <div class="fe-share__opts">
1017
+ <label class="fe-share__check">
1018
+ <input type="checkbox" v-model="dropPwd" />
1019
+ <span>{{ L('PIN ile koru', 'Protect with a PIN') }}</span>
1020
+ </label>
1021
+ <label class="fe-share__field">
1022
+ <span class="fe-share__fieldlabel">{{ L('Süre', 'Expiry') }}</span>
1023
+ <select v-model.number="dropExpiry" class="fe-share__select" data-testid="drop-expiry">
1024
+ <option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
1025
+ </select>
1026
+ </label>
737
1027
  </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') }}
1028
+ <p v-if="ttlHint" class="fe-share__hint">{{ ttlHint }}</p>
1029
+ <button type="button" class="fe-share__btn fe-share__btn--primary fe-share__btn--wide" data-testid="drop-create" :disabled="dropBusy" @click="createDropLink">
1030
+ {{ L('Bağlantı oluştur', 'Create link') }}
763
1031
  </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
1032
 
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') }}
1033
+ <button type="button" class="fe-share__linkbtn" :aria-expanded="dropShowAdv ? 'true' : 'false'" @click="dropShowAdv = !dropShowAdv">
1034
+ {{ dropShowAdv ? L('Yükleme sınırlarını gizle', 'Hide upload limits') : L('Yükleme sınırları', 'Upload limits') }}
778
1035
  </button>
779
- <button class="fx-perm-del" :disabled="shareBusy" :title="L('İptal', 'Revoke')" @click="revoke(s)">✕</button>
780
- </div>
781
- </div>
782
- </template>
1036
+ <div v-if="dropShowAdv" class="fe-share__adv">
1037
+ <label class="fe-share__advrow">
1038
+ <span class="fe-share__fieldlabel">{{ L('En fazla dosya', 'Max files') }}</span>
1039
+ <input v-model="dropMaxFiles" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="20" />
1040
+ </label>
1041
+ <label class="fe-share__advrow">
1042
+ <span class="fe-share__fieldlabel">{{ L('Dosya başı MB', 'MB / file') }}</span>
1043
+ <input v-model="dropMaxSizeMB" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="500" />
1044
+ </label>
1045
+ <label class="fe-share__advrow">
1046
+ <span class="fe-share__fieldlabel">{{ L('İzinli türler', 'Allowed types') }}</span>
1047
+ <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)')" />
1048
+ </label>
1049
+ <label class="fe-share__check">
1050
+ <input type="checkbox" v-model="dropAskName" />
1051
+ <span>{{ L('Yükleyenin adını sor', 'Ask uploader name') }}</span>
1052
+ </label>
1053
+ </div>
783
1054
 
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>
1055
+ <div v-if="dropErr" class="fe-share__warn">{{ dropErr }}</div>
806
1056
 
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>
1057
+ <template v-if="dropResult">
1058
+ <div class="fe-share__linkrow">
1059
+ <a :href="dropResult.url" target="_blank" rel="noopener" class="fe-share__url" :title="dropResult.url">{{ dropResult.url }}</a>
1060
+ <button type="button" class="fe-share__copy" data-testid="drop-copy" @click="copy(dropResult.url, 'drop')">
1061
+ <span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
1062
+ <span>{{ copied === 'drop' ? t('access.copied') : t('access.copy') }}</span>
1063
+ </button>
1064
+ </div>
1065
+ <p class="fe-share__detail">
1066
+ {{ validUntil(dropResult) }}
1067
+ <span v-if="dropResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
1068
+ </p>
1069
+ <div v-if="dropResult.pin" class="fe-share__pinrow">
1070
+ <span class="fe-share__pinlabel">PIN</span>
1071
+ <code class="fe-share__pin">{{ dropResult.pin }}</code>
1072
+ <button type="button" class="fe-share__mini" @click="copy(dropResult.pin, 'droppin')">
1073
+ {{ copied === 'droppin' ? t('access.copied') : t('access.copy') }}
1074
+ </button>
1075
+ </div>
828
1076
 
829
- <div v-if="dropErr" class="fx-perm-warn">{{ dropErr }}</div>
1077
+ <!-- email the upload link to one or more people -->
1078
+ <div class="fe-share__mailrow">
1079
+ <input v-model="dropMailTo" type="text" class="fe-share__input" autocomplete="off"
1080
+ :placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendDropMail" />
1081
+ <button type="button" class="fe-share__btn" :disabled="dropMailBusy" @click="sendDropMail">
1082
+ {{ L('Gönder', 'Send') }}
1083
+ </button>
1084
+ </div>
1085
+ <div v-if="dropMailNotice" class="fe-share__notice">{{ dropMailNotice }}</div>
830
1086
 
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') }}
1087
+ <!-- native share (OS share sheet) — same as the fishapp Share button -->
1088
+ <button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(dropBody())">
1089
+ <span aria-hidden="true" v-html="actionIconSvg('access')"></span>
1090
+ <span>{{ L('Paylaş', 'Share') }}</span>
836
1091
  </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>
1092
+ </template>
848
1093
 
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>
1094
+ <div class="fe-share__list">
1095
+ <h4 class="fe-share__listhead">{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
1096
+ <p v-if="!dropShares.length" class="fe-share__empty">{{ L('Yok', 'None') }}</p>
1097
+ <div v-for="s in dropShares" :key="s.uuid" class="fe-share__row">
1098
+ <span class="fe-share__url" :title="s.url">{{ s.url }}</span>
1099
+ <button type="button" class="fe-share__mini" @click="copy(s.url, s.uuid)">
1100
+ {{ copied === s.uuid ? t('access.copied') : t('access.copy') }}
1101
+ </button>
1102
+ <button type="button" class="fe-share__del" :disabled="shareBusy" :title="L('İptal', 'Revoke')"
1103
+ :aria-label="L('İptal', 'Revoke')" @click="revoke(s)">
1104
+ <span aria-hidden="true" v-html="actionIconSvg('close')"></span>
1105
+ </button>
1106
+ </div>
856
1107
  </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
1108
  </div>
864
- </div>
865
- </template>
1109
+ </section>
1110
+ </div>
866
1111
  </div>
1112
+
1113
+ <footer class="fe-share__foot">
1114
+ <button type="button" class="fe-share__btn fe-share__btn--primary" data-testid="share-done" @click="emit('close')">
1115
+ {{ t('access.done') }}
1116
+ </button>
1117
+ </footer>
867
1118
  </div>
868
1119
  </div>
869
1120
  </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>