@brftech/filex-core 0.41.0 → 0.41.2

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 (45) hide show
  1. package/README.md +8 -0
  2. package/dist/{ArchiveViewer-BNqukFg8.js → ArchiveViewer-Dq1c4-G5.js} +2 -2
  3. package/dist/{ArchiveViewer-BNqukFg8.js.map → ArchiveViewer-Dq1c4-G5.js.map} +1 -1
  4. package/dist/{CsvViewer-ii_-MgmG.js → CsvViewer-CeHslAc0.js} +2 -2
  5. package/dist/{CsvViewer-ii_-MgmG.js.map → CsvViewer-CeHslAc0.js.map} +1 -1
  6. package/dist/{DrawioViewer-B2tuu4rX.js → DrawioViewer-p03u7Oda.js} +2 -2
  7. package/dist/{DrawioViewer-B2tuu4rX.js.map → DrawioViewer-p03u7Oda.js.map} +1 -1
  8. package/dist/{EpubViewer-p4B8iiWb.js → EpubViewer-D08Y-Tww.js} +2 -2
  9. package/dist/{EpubViewer-p4B8iiWb.js.map → EpubViewer-D08Y-Tww.js.map} +1 -1
  10. package/dist/{IpynbViewer-D_qYRJhu.js → IpynbViewer-mG7p3Sil.js} +2 -2
  11. package/dist/{IpynbViewer-D_qYRJhu.js.map → IpynbViewer-mG7p3Sil.js.map} +1 -1
  12. package/dist/{MermaidViewer-3ZkfNM8v.js → MermaidViewer-CCPg1Q3W.js} +2 -2
  13. package/dist/{MermaidViewer-3ZkfNM8v.js.map → MermaidViewer-CCPg1Q3W.js.map} +1 -1
  14. package/dist/{PsdViewer-CuV-OPSd.js → PsdViewer-CDEHTiUN.js} +2 -2
  15. package/dist/{PsdViewer-CuV-OPSd.js.map → PsdViewer-CDEHTiUN.js.map} +1 -1
  16. package/dist/{TiffViewer-BLeoF4Be.js → TiffViewer-DnWRUGOK.js} +2 -2
  17. package/dist/{TiffViewer-BLeoF4Be.js.map → TiffViewer-DnWRUGOK.js.map} +1 -1
  18. package/dist/{Viewer3D-DHhhU76E.js → Viewer3D-COCQp53D.js} +2 -2
  19. package/dist/{Viewer3D-DHhhU76E.js.map → Viewer3D-COCQp53D.js.map} +1 -1
  20. package/dist/filex-core.js +81 -80
  21. package/dist/filex-core.umd.cjs +47 -47
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/{index-BraG7Cz4.js → index-JUiZzLmF.js} +10418 -10350
  24. package/dist/index-JUiZzLmF.js.map +1 -0
  25. package/dist/index.d.ts +41 -38
  26. package/package.json +1 -1
  27. package/src/components/FilePane.vue +46 -5
  28. package/src/components/GalleryView.vue +10 -26
  29. package/src/components/GridView.vue +16 -27
  30. package/src/components/ListView.vue +17 -29
  31. package/src/components/PendingOpsTray.vue +4 -5
  32. package/src/components/TimeZoneDialog.vue +18 -10
  33. package/src/components/TokensPanel.vue +20 -11
  34. package/src/composables/useExplorerTimeZone.ts +7 -3
  35. package/src/composables/useLocale.ts +31 -15
  36. package/src/composables/usePendingOps.ts +5 -0
  37. package/src/composables/useRowTouch.ts +126 -0
  38. package/src/index.ts +5 -0
  39. package/src/lib/opProgress.ts +43 -0
  40. package/src/lib/shareTtl.ts +21 -2
  41. package/src/lib/timezone.ts +46 -1
  42. package/src/locales/en.ts +2 -0
  43. package/src/locales/tr.ts +2 -0
  44. package/src/modals/PermissionsModal.vue +16 -5
  45. package/dist/index-BraG7Cz4.js.map +0 -1
@@ -25,10 +25,10 @@
25
25
  * printing the viewer's chosen one.
26
26
  */
27
27
 
28
- import { computed, type Ref } from 'vue';
28
+ import { computed, getCurrentInstance, inject, type Ref } from 'vue';
29
29
  import type { LocaleCode } from '../types/ExplorerConfig';
30
30
  import { messages } from '../locales';
31
- import { activeTimeZone, deviceTimeZone } from '../lib/timezone';
31
+ import { EXPLORER_CLOCK, activeTimeZoneFor, deviceTimeZone } from '../lib/timezone';
32
32
 
33
33
  /* ── the tag ──────────────────────────────────────────────────────────── */
34
34
 
@@ -181,8 +181,12 @@ const dtfCache = new Map<string, Intl.DateTimeFormat>();
181
181
  * was opened in. Reading `activeTimeZone()` on every call is also what keeps
182
182
  * a template that formats a date reactive to the preference changing.
183
183
  */
184
- function zonedFormatter(tag: string, opts: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
185
- const zone = activeTimeZone();
184
+ function zonedFormatter(
185
+ tag: string,
186
+ opts: Intl.DateTimeFormatOptions,
187
+ owner?: symbol,
188
+ ): Intl.DateTimeFormat {
189
+ const zone = activeTimeZoneFor(owner);
186
190
  const key = `${tag}|${zone ?? `device:${deviceTimeZone()}`}|${JSON.stringify(opts)}`;
187
191
  let fmt = dtfCache.get(key);
188
192
  if (!fmt) {
@@ -201,9 +205,11 @@ export function formatInstant(
201
205
  d: Date,
202
206
  code: LocaleCode | string | undefined,
203
207
  opts: Intl.DateTimeFormatOptions,
208
+ /** The explorer whose clock applies (EXPLORER_CLOCK); omitted = page-wide. */
209
+ owner?: symbol,
204
210
  ): string {
205
211
  try {
206
- return zonedFormatter(localeTag(code), opts).format(d);
212
+ return zonedFormatter(localeTag(code), opts, owner).format(d);
207
213
  } catch {
208
214
  return d.toISOString();
209
215
  }
@@ -260,6 +266,11 @@ export function useLocale(localeRef: Ref<LocaleCode> | (() => LocaleCode)) {
260
266
 
261
267
  const lookup = computed(() => messages[code()] ?? messages.en);
262
268
 
269
+ // The explorer this component sits in, whose clock its dates are read on
270
+ // (lib/timezone, timeZoneSourcesFor). Outside a component, or outside any
271
+ // explorer, the page-wide answer.
272
+ const clock = getCurrentInstance() ? inject(EXPLORER_CLOCK, undefined) : undefined;
273
+
263
274
  function t(key: string, vars: Record<string, string | number> = {}): string {
264
275
  const table = lookup.value;
265
276
  const raw = table[countedKey(key, vars, (k) => k in table)] ?? key;
@@ -319,13 +330,18 @@ export function useLocale(localeRef: Ref<LocaleCode> | (() => LocaleCode)) {
319
330
  function formatDate(ms: number | undefined | null, opts: { time?: boolean } = {}): string {
320
331
  const d = toDate(ms);
321
332
  if (!d) return '';
322
- const date = formatInstant(d, code(), {
323
- month: 'short',
324
- day: 'numeric',
325
- year: 'numeric',
326
- });
333
+ const date = formatInstant(
334
+ d,
335
+ code(),
336
+ {
337
+ month: 'short',
338
+ day: 'numeric',
339
+ year: 'numeric',
340
+ },
341
+ clock,
342
+ );
327
343
  if (!opts.time) return date;
328
- return `${date}, ${formatInstant(d, code(), { hour: 'numeric', minute: '2-digit' })}`;
344
+ return `${date}, ${formatInstant(d, code(), { hour: 'numeric', minute: '2-digit' }, clock)}`;
329
345
  }
330
346
 
331
347
  /**
@@ -342,8 +358,8 @@ export function useLocale(localeRef: Ref<LocaleCode> | (() => LocaleCode)) {
342
358
  function formatDateFull(ms: number | undefined | null): string {
343
359
  const d = toDate(ms);
344
360
  if (!d) return '';
345
- const zone = activeTimeZone() ?? deviceTimeZone();
346
- const stamp = formatInstant(d, code(), { dateStyle: 'full', timeStyle: 'long' });
361
+ const zone = activeTimeZoneFor(clock) ?? deviceTimeZone();
362
+ const stamp = formatInstant(d, code(), { dateStyle: 'full', timeStyle: 'long' }, clock);
347
363
  return `${stamp} (${zone})`;
348
364
  }
349
365
 
@@ -358,7 +374,7 @@ export function useLocale(localeRef: Ref<LocaleCode> | (() => LocaleCode)) {
358
374
  function formatMonthYear(value: number | Date | undefined | null): string {
359
375
  const d = value instanceof Date ? value : toDate(value);
360
376
  if (!d) return '';
361
- return formatInstant(d, code(), { month: 'long', year: 'numeric' });
377
+ return formatInstant(d, code(), { month: 'long', year: 'numeric' }, clock);
362
378
  }
363
379
 
364
380
  /**
@@ -373,7 +389,7 @@ export function useLocale(localeRef: Ref<LocaleCode> | (() => LocaleCode)) {
373
389
  const d = value instanceof Date ? value : toDate(value);
374
390
  if (!d) return '';
375
391
  try {
376
- return zonedFormatter('en-CA', { year: 'numeric', month: '2-digit' }).format(d);
392
+ return zonedFormatter('en-CA', { year: 'numeric', month: '2-digit' }, clock).format(d);
377
393
  } catch {
378
394
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
379
395
  }
@@ -23,6 +23,9 @@ export interface PendingOp {
23
23
  status: 'pending' | 'running' | 'done' | 'error';
24
24
  progress_total: number;
25
25
  progress_done: number;
26
+ /** Running cross-storage transfer's bytes (issue #27); absent otherwise. */
27
+ bytes_total?: number;
28
+ bytes_done?: number;
26
29
  target_path: string | null;
27
30
  source_dir: string | null;
28
31
  source_count: number;
@@ -69,6 +72,8 @@ function normalizeOp(raw: Record<string, unknown>): PendingOp {
69
72
  status,
70
73
  progress_total: num(raw.progress_total, raw.total, sources.length),
71
74
  progress_done: num(raw.progress_done, raw.done),
75
+ bytes_total: num(raw.bytes_total),
76
+ bytes_done: num(raw.bytes_done),
72
77
  target_path: str(raw.target_path, raw.dest),
73
78
  source_dir: str(raw.source_dir),
74
79
  source_count: num(raw.source_count, sources.length),
@@ -0,0 +1,126 @@
1
+ /**
2
+ * useRowTouch — the one touch grammar every file view speaks (list, grid,
3
+ * gallery).
4
+ *
5
+ * A mouse selects with a click and opens with a double-click. A finger has no
6
+ * double-click: the browser swallows the second tap into a zoom or never sends
7
+ * `dblclick` at all, so on a phone the desktop grammar left nothing openable
8
+ * (issue #26). What a finger does instead is what every mobile file manager
9
+ * does:
10
+ *
11
+ * - a long press opens the item's menu (and, through it, selects the item);
12
+ * - a tap is reported AS a tap, so the host can open instead of select.
13
+ *
14
+ * The views only report. What a tap means is decided once, in FilePane.
15
+ *
16
+ * Issue #26, second round (the reporter's rule, on EVERY device):
17
+ * - a press on the item's NAME opens it — mouse click or finger tap, with or
18
+ * without a selection;
19
+ * - a long press (finger) or a right click (mouse) opens the menu;
20
+ * - the checkbox selects, exactly as before.
21
+ * So the views also report whether the click landed on the name (`name`).
22
+ *
23
+ * ⚠ A tap is judged from the gesture that produced the click, never from the
24
+ * screen: `pointerType` where the browser sets it on click (Chromium, Firefox),
25
+ * and the touchend that just preceded the click where it does not (older
26
+ * WebKit). A touch laptop's trackpad therefore keeps the desktop grammar while
27
+ * its screen gets the phone one. A `(pointer: coarse)` media query cannot tell
28
+ * those two apart.
29
+ *
30
+ * ⚠ It used to live as three identical copies of the long-press timer, one per
31
+ * view; the tap rule is exactly the kind of addition a copy misses.
32
+ */
33
+ import { onBeforeUnmount } from 'vue';
34
+
35
+ /** How long a finger rests before the press becomes a long press. */
36
+ export const LONG_PRESS_MS = 500;
37
+ /** A click this soon after a touchend is the tap that touchend ended. */
38
+ const TAP_WINDOW_MS = 800;
39
+ /** A finger that travels further than this is scrolling, not pressing. */
40
+ const MOVE_TOLERANCE_PX = 10;
41
+
42
+ export interface TouchPoint {
43
+ clientX: number;
44
+ clientY: number;
45
+ }
46
+
47
+ /** What a view reports about a click on one of its items. */
48
+ export interface ClickMod {
49
+ ctrl: boolean;
50
+ shift: boolean;
51
+ /** The click was a finger's tap. */
52
+ touch?: boolean;
53
+ /** The click landed on the item's name. */
54
+ name?: boolean;
55
+ }
56
+
57
+ /**
58
+ * The one place a view turns a click into a `ClickMod`. `nameSelector` is the
59
+ * view's own name element (`.fe-list__name`, `.fe-grid__label`, …).
60
+ */
61
+ export function clickMod(ev: MouseEvent, touch: boolean, nameSelector: string): ClickMod {
62
+ const target = ev.target as Element | null;
63
+ return {
64
+ ctrl: ev.ctrlKey || ev.metaKey,
65
+ shift: ev.shiftKey,
66
+ touch,
67
+ name: typeof target?.closest === 'function' && target.closest(nameSelector) !== null,
68
+ };
69
+ }
70
+
71
+ export function useRowTouch<T>(onLongPress: (item: T, at: TouchPoint) => void) {
72
+ let timer: ReturnType<typeof setTimeout> | undefined;
73
+ let target: T | null = null;
74
+ let origin: TouchPoint = { clientX: 0, clientY: 0 };
75
+ let longPressed = false;
76
+ let tapEndedAt = 0;
77
+
78
+ function stopTimer() {
79
+ if (timer) clearTimeout(timer);
80
+ timer = undefined;
81
+ }
82
+
83
+ function onTouchStart(item: T, ev: TouchEvent) {
84
+ const t0 = ev.touches[0];
85
+ if (!t0) return;
86
+ stopTimer();
87
+ target = item;
88
+ longPressed = false;
89
+ origin = { clientX: t0.clientX, clientY: t0.clientY };
90
+ timer = setTimeout(() => {
91
+ timer = undefined;
92
+ if (target === null) return;
93
+ longPressed = true;
94
+ onLongPress(target, origin);
95
+ }, LONG_PRESS_MS);
96
+ }
97
+
98
+ function onTouchMove(ev: TouchEvent) {
99
+ const t0 = ev.touches[0];
100
+ if (
101
+ !t0 ||
102
+ Math.abs(t0.clientX - origin.clientX) > MOVE_TOLERANCE_PX ||
103
+ Math.abs(t0.clientY - origin.clientY) > MOVE_TOLERANCE_PX
104
+ ) {
105
+ stopTimer();
106
+ target = null;
107
+ }
108
+ }
109
+
110
+ function onTouchEnd() {
111
+ stopTimer();
112
+ if (target !== null && !longPressed) tapEndedAt = Date.now();
113
+ target = null;
114
+ }
115
+
116
+ /** Whether this click is a finger's tap. */
117
+ function isTap(ev: MouseEvent): boolean {
118
+ const kind = (ev as PointerEvent).pointerType;
119
+ if (kind) return kind === 'touch';
120
+ return Date.now() - tapEndedAt < TAP_WINDOW_MS;
121
+ }
122
+
123
+ onBeforeUnmount(stopTimer);
124
+
125
+ return { onTouchStart, onTouchMove, onTouchEnd, isTap };
126
+ }
package/src/index.ts CHANGED
@@ -55,6 +55,11 @@ export {
55
55
  } from './lib/destinationTree';
56
56
  export type { DestinationRow } from './lib/destinationTree';
57
57
 
58
+ /* issue #27 — the one "how far along is this queued op" rule, shared by the
59
+ * explorer's operations center and a host's own tray (the admin app's). */
60
+ export { opPercent } from './lib/opProgress';
61
+ export type { OpProgressLike } from './lib/opProgress';
62
+
58
63
  /* tasi:m1 — "download the selection as one archive". Exported so a host that
59
64
  * draws its own selection bar gets the real two-step flow (authorized mint,
60
65
  * then a navigation that streams) instead of reaching for window.open per
@@ -0,0 +1,43 @@
1
+ /**
2
+ * opProgress — the one rule for "how far along is this queued operation".
3
+ *
4
+ * Both progress surfaces read it: the explorer's operations center
5
+ * (PendingOpsTray → OperationsCenter) and the admin app's bottom-right tray.
6
+ * Two copies of this rule are how one tray starts saying something the other
7
+ * does not (filex lesson #119).
8
+ *
9
+ * Issue #27: a queued op counts SOURCES, so moving one large file — or one
10
+ * folder of a thousand files — is "0 of 1" until the very end. Drawn as a
11
+ * percentage that is a bar frozen at 0% that then vanishes, which the reporter
12
+ * read as stuck. So:
13
+ *
14
+ * - a running cross-storage transfer reports bytes (`bytes_done` /
15
+ * `bytes_total`); when the total is known that is the percentage;
16
+ * - bytes moving with no total yet, or a single source with nothing but a
17
+ * source count, has NO honest percentage: `null`, and the surface shows
18
+ * an indeterminate indicator instead of a fake 0%;
19
+ * - several sources still report per source, as before.
20
+ */
21
+
22
+ export interface OpProgressLike {
23
+ status: string;
24
+ progress_total: number;
25
+ progress_done: number;
26
+ bytes_total?: number;
27
+ bytes_done?: number;
28
+ }
29
+
30
+ function clampPercent(n: number): number {
31
+ return Math.max(0, Math.min(100, Math.round(n)));
32
+ }
33
+
34
+ /** 0–100, or `null` when there is no honest number to draw. */
35
+ export function opPercent(op: OpProgressLike): number | null {
36
+ if (op.status === 'done') return 100;
37
+ const bytesTotal = op.bytes_total ?? 0;
38
+ const bytesDone = op.bytes_done ?? 0;
39
+ if (bytesTotal > 0) return clampPercent((bytesDone / bytesTotal) * 100);
40
+ if (bytesDone > 0) return null;
41
+ if (op.progress_total <= 1) return null;
42
+ return clampPercent((op.progress_done / op.progress_total) * 100);
43
+ }
@@ -74,7 +74,12 @@ export function expiryInputMax(maxDays: number | undefined, now: Date = new Date
74
74
  }
75
75
 
76
76
  /** "Valid until 30 Aug 2026, 14:05" / "Does not expire" from the server's `expires_at`. */
77
- export function validUntilLine(expiresAt: string | null | undefined, locale: 'tr' | 'en'): string {
77
+ export function validUntilLine(
78
+ expiresAt: string | null | undefined,
79
+ locale: 'tr' | 'en',
80
+ /** The explorer whose clock applies (lib/timezone EXPLORER_CLOCK). */
81
+ clock?: symbol,
82
+ ): string {
78
83
  if (!expiresAt) return locale === 'tr' ? 'Bu bağlantının süresi yoktur.' : 'This link does not expire.';
79
84
  const d = new Date(expiresAt);
80
85
  // zaman:z1 — the viewer's chosen clock, not the browser's. "Valid until
@@ -87,7 +92,7 @@ export function validUntilLine(expiresAt: string | null | undefined, locale: 'tr
87
92
  // dialog and "Sep 20, 2026, 1:53 PM" in the listing behind it.
88
93
  const when = Number.isNaN(d.getTime())
89
94
  ? expiresAt
90
- : formatInstant(d, locale, { dateStyle: 'medium', timeStyle: 'short' });
95
+ : formatInstant(d, locale, { dateStyle: 'medium', timeStyle: 'short' }, clock);
91
96
  return locale === 'tr' ? `Bu bağlantı ${when} tarihine kadar geçerli.` : `This link is valid until ${when}.`;
92
97
  }
93
98
 
@@ -99,3 +104,17 @@ export function ttlCeilingHint(maxDays: number | undefined, locale: 'tr' | 'en')
99
104
  ? `Bağlantılar en fazla ${max} gün geçerli olabilir (sunucu ayarı).`
100
105
  : `Links can be valid for at most ${max} day${max === 1 ? '' : 's'} (server setting).`;
101
106
  }
107
+
108
+ /**
109
+ * The share dialog's muted detail line: its facts joined with " · ".
110
+ *
111
+ * ⚠ The first fact is a whole sentence ("This link is valid until …, 10:00
112
+ * AM.") and the ones after it are fragments ("3 downloads"), so a plain join
113
+ * printed "10:00 AM. · 3 downloads" — a full stop in the middle of a line. A
114
+ * fact that is followed by another loses its closing full stop; the last one
115
+ * keeps whatever it has.
116
+ */
117
+ export function shareDetailLine(facts: Array<string | null | undefined>): string {
118
+ const kept = facts.filter((f): f is string => !!f);
119
+ return kept.map((f, i) => (i < kept.length - 1 ? f.replace(/\.\s*$/, '') : f)).join(' · ');
120
+ }
@@ -36,7 +36,7 @@
36
36
  * that is shared state by nature — the browser's.
37
37
  */
38
38
 
39
- import { computed, ref, type Ref } from 'vue';
39
+ import { computed, ref, type InjectionKey, type Ref } from 'vue';
40
40
 
41
41
  /* ── the order ────────────────────────────────────────────────────────── */
42
42
 
@@ -312,6 +312,11 @@ function ownedTier() {
312
312
  const list = entries.value;
313
313
  return list.length ? list[list.length - 1].zone : '';
314
314
  },
315
+ /** The newest answer among the owners `pick` accepts. */
316
+ currentWhere(pick: (owner: symbol) => boolean): string {
317
+ const list = entries.value.filter((e) => pick(e.owner));
318
+ return list.length ? list[list.length - 1].zone : '';
319
+ },
315
320
  };
316
321
  }
317
322
 
@@ -345,6 +350,46 @@ export function resolvedTimeZone(): ResolvedTimeZone {
345
350
  return resolved.value;
346
351
  }
347
352
 
353
+ /**
354
+ * The owner key an explorer PROVIDES to its components, so the dates they
355
+ * print resolve against that explorer's own tiers (useLocale injects it).
356
+ */
357
+ export const EXPLORER_CLOCK: InjectionKey<symbol> = Symbol('filex-explorer-clock');
358
+
359
+ /**
360
+ * What the tiers hold for ONE explorer.
361
+ *
362
+ * ⚠⚠ The page-wide tiers answer with the NEWEST write, which is right for a
363
+ * surface outside every explorer and wrong inside one: two explorers on one
364
+ * page with different `config.timeZone` both printed the zone of whichever
365
+ * mounted last. The host tier is by definition one explorer's setting, so an
366
+ * explorer reads only its own. Its account tier is its own too; the only other
367
+ * account it may inherit is the one a HOST APP remembered for the person
368
+ * signed in (the admin app's session, `remember: true`) — never a sibling
369
+ * explorer's, which may belong to a different key altogether. The viewer tier
370
+ * is this browser's, shared by everything on the page.
371
+ *
372
+ * `owner` undefined is the page-wide answer, unchanged.
373
+ */
374
+ export function timeZoneSourcesFor(owner: symbol | undefined): Required<TimeZoneSources> {
375
+ if (!owner) return timeZoneSources();
376
+ return {
377
+ viewer: viewerZone.value,
378
+ host: hostTier.of(owner) ?? '',
379
+ account: accountTier.of(owner) ?? accountTier.currentWhere((o) => rememberingOwners.has(o)),
380
+ };
381
+ }
382
+
383
+ /** resolvedTimeZone for ONE explorer (see timeZoneSourcesFor). Reactive. */
384
+ export function resolvedTimeZoneFor(owner: symbol | undefined): ResolvedTimeZone {
385
+ return owner ? resolveTimeZone(timeZoneSourcesFor(owner)) : resolved.value;
386
+ }
387
+
388
+ /** activeTimeZone for ONE explorer (see timeZoneSourcesFor). Reactive. */
389
+ export function activeTimeZoneFor(owner: symbol | undefined): string | undefined {
390
+ return resolvedTimeZoneFor(owner).zone;
391
+ }
392
+
348
393
  /** What each tier currently holds (`''` = nothing). Reactive. */
349
394
  export function timeZoneSources(): Required<TimeZoneSources> {
350
395
  return {
package/src/locales/en.ts CHANGED
@@ -1081,6 +1081,8 @@ export const en: Record<string, string> = {
1081
1081
  'conn.tokens.rootPlaceholder': 'storage://folder',
1082
1082
  'conn.tokens.expiry': 'Expires in (days)',
1083
1083
  'conn.tokens.expiryNever': 'never',
1084
+ 'conn.tokens.defaultName': 'API key {date}',
1085
+ 'conn.tokens.namePlaceholder': 'Name — e.g. backup script',
1084
1086
  'conn.tokens.capNote': 'Your account and its permissions are the ceiling — asking for more than you have is refused, not granted.',
1085
1087
  /* === /gezinti:g1 === */
1086
1088
 
package/src/locales/tr.ts CHANGED
@@ -1079,6 +1079,8 @@ export const tr: Record<string, string> = {
1079
1079
  'conn.tokens.rootPlaceholder': 'depo://klasör',
1080
1080
  'conn.tokens.expiry': 'Geçerlilik (gün)',
1081
1081
  'conn.tokens.expiryNever': 'süresiz',
1082
+ 'conn.tokens.defaultName': 'API anahtarı {date}',
1083
+ 'conn.tokens.namePlaceholder': 'Ad — ör. yedekleme betiği',
1082
1084
  'conn.tokens.capNote': 'Tavan hesabın ve yetkilerin — sahip olduğundan fazlasını istemek reddedilir, verilmez.',
1083
1085
  /* === /gezinti:g1 === */
1084
1086
 
@@ -27,11 +27,19 @@
27
27
  * (`gorunum:v2-share`). A scoped style block here is silently dropped in the
28
28
  * web-component build — see web/tests/api/scopedStyles.test.ts.
29
29
  */
30
- import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
30
+ import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue';
31
31
  import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
32
32
  import type { ShareInfo } from '../types/FileNode';
33
33
  import { shareCliCommand } from '../lib/shareCli';
34
- 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';
35
43
  import { resolveLocale } from '../locales/resolve';
36
44
  import { formatByteSize, useLocale } from '../composables/useLocale';
37
45
  import { actionIconSvg } from '../lib/actionIcons';
@@ -636,8 +644,11 @@ function expiryLine(days: number): string {
636
644
  }
637
645
  // What the server actually stored — shown under a fresh link so the real
638
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
+
639
650
  function validUntil(r: { expiresAt?: string | null } | null): string {
640
- return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en');
651
+ return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en', clock);
641
652
  }
642
653
 
643
654
  /* ── the top tier: one switch, one sentence, one link ──────────────────── */
@@ -671,10 +682,10 @@ const linkDetail = computed(() => {
671
682
  if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
672
683
  } else if (downloadShares.value[0]) {
673
684
  const s = downloadShares.value[0];
674
- bits.push(validUntilLine(s.expires_at ?? null, tr.value ? 'tr' : 'en'));
685
+ bits.push(validUntil({ expiresAt: s.expires_at ?? null }));
675
686
  if (s.max_downloads) bits.push(maxDlOptions.find((o) => o.v === s.max_downloads)?.l ?? String(s.max_downloads));
676
687
  }
677
- return bits.filter(Boolean).join(' · ');
688
+ return shareDetailLine(bits);
678
689
  });
679
690
 
680
691
  /* Section summaries — a named section still has to say what is inside it, or