@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
@@ -4,29 +4,78 @@
4
4
  * Consumer overrides by setting `--fe-*` on a higher scope.
5
5
  */
6
6
  :root {
7
+ /* gorunum:v1 — the product's own palette and metrics. Everything below is a
8
+ * measured value, not a taste: the type scale, the control heights and the
9
+ * radii were read off the shell this look follows, in a real browser at
10
+ * 1440, and the palette clears the contrast bar in
11
+ * web/tests/api/themeContrast.test.ts in both variants. */
7
12
  --fe-bg: #ffffff;
8
- --fe-bg-elev: #f7f8fa;
9
- --fe-bg-hover: #edf0f5;
10
- --fe-bg-selected: #dfe8ff;
11
- --fe-border: #e2e6ed;
12
- --fe-border-strong: #c7ced9;
13
- --fe-text: #1a1e27;
14
- --fe-text-muted: #5a6475;
13
+ --fe-bg-elev: #f7f8fb;
14
+ --fe-bg-hover: #f3f4f6;
15
+ --fe-bg-selected: #eef3ff;
16
+ --fe-border: #e5e7eb;
17
+ --fe-border-soft: #eef0f4;
18
+ --fe-border-strong: #d1d5db;
19
+ --fe-text: #1f2937;
20
+ --fe-text-muted: #6b7280;
15
21
  --fe-text-on-primary: #ffffff;
16
- --fe-primary: #2f6fe0; /* ui-fix: beyaz metinle WCAG AA (eski #3b82f6 = 3.68) */
17
- --fe-primary-hover: #2563eb;
22
+ --fe-primary: #2f6ceb;
23
+ --fe-primary-hover: #2559c9;
24
+ --fe-primary-soft: #e6ecfa;
25
+ /* Ink for text and icons ON the soft-primary ground — the active sidebar
26
+ row, the active scope tab, the active segment of a segmented control.
27
+ ⚠ It is a token of its own because `--fe-primary` cannot carry that job:
28
+ measured, the stock primary on the stock tint is 3.97:1, under the 4.5 the
29
+ palette file claims, and no tint rescues it (the primary is only 4.70:1 on
30
+ white to begin with). It is not `--fe-primary-hover` either, though that is
31
+ the value HERE: three of the shipped palettes darken their hover in dark
32
+ mode, toward the tint rather than away from it, and Night dark measures
33
+ 3.78:1 that way. Each palette names the ink that clears the bar on its own
34
+ tint; web/tests/api/themeContrast.test.ts pins the pairing. */
35
+ --fe-primary-ink: #2559c9;
18
36
  --fe-danger: #dc2626;
19
37
  --fe-danger-hover: #b91c1c;
20
38
  --fe-keep-ok: #16a34a; /* selective-sync "on this computer" check */
21
- --fe-shadow: 0 10px 32px rgba(15, 23, 42, 0.14);
22
- --fe-shadow-sm: 0 2px 6px rgba(15, 23, 42, 0.08);
39
+ --fe-shadow: 0 20px 60px rgba(17, 24, 39, 0.18);
40
+ --fe-shadow-sm: 0 8px 24px rgba(17, 24, 39, 0.12);
23
41
  --fe-radius: 8px;
24
- --fe-radius-sm: 4px;
42
+ --fe-radius-sm: 6px;
25
43
  --fe-radius-md: 10px;
26
44
  --fe-radius-lg: 12px;
27
- --fe-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
45
+ /* Control heights. A listing is a stack of rows and chips; when each one
46
+ * picks its own height the column edges wander and the page reads as
47
+ * hand-assembled. Three sizes, and every control picks one. */
48
+ --fe-h-sm: 28px;
49
+ --fe-h-md: 34px;
50
+ --fe-h-lg: 40px;
51
+ /* gorunum:v3-shell — the navigation panel's expanded width. A token because
52
+ * TWO places have to agree on it: the panel itself, and the gutter the top
53
+ * bar reserves at its left so the search field starts where the content
54
+ * area does. Written once in each place, they were one redesign away from
55
+ * disagreeing — and the failure is silent, a header that no longer lines up
56
+ * with the column under it.
57
+ * ⚠ The rail and the drawer keep their own widths; only the expanded column
58
+ * follows this.
59
+ * ⚠ 192px, not 255px (owner, 2026-09-13: "şu an default'ta çok açık geliyor
60
+ * yan menü, biraz kısaltabiliriz bence"). It is the reference build's own
61
+ * number, measured off it at 1280, and it is the WHOLE change: the top bar's
62
+ * gutter is `calc(this - 12px)`, so the search field moves left with the
63
+ * column instead of the header drifting out of line with it. Checked at the
64
+ * new width in Turkish, the longer of the two catalogues — the widest row
65
+ * label ("Paylaşılanlar") clears it, and so does the quota line. */
66
+ --fe-sidenav-w: 192px;
67
+ /* Type scale. Small and tight on purpose: the shell packs a filename, a
68
+ * size, a date and a menu into one row, and 14px with normal leading pushes
69
+ * that to two lines at any realistic column width. */
70
+ --fe-text-xs: 12px;
71
+ --fe-text-sm: 12.5px;
72
+ --fe-text-md: 13px;
73
+ --fe-font: Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
28
74
  --fe-font-mono: ui-monospace, "SF Mono", Consolas, Menlo, monospace;
75
+ --fe-gap-xs: 4px;
76
+ --fe-gap-sm: 8px;
29
77
  --fe-gap: 12px;
78
+ --fe-gap-lg: 16px;
30
79
  }
31
80
 
32
81
  .fe--theme-dark,
@@ -54,16 +103,22 @@
54
103
  * as white-on-white inside dark hosts. */
55
104
  .fe-ctx-backdrop--theme-dark,
56
105
  .fe-ctx-backdrop--theme-auto[data-prefers-dark='1'] {
57
- --fe-bg: #0f1419;
58
- --fe-bg-elev: #161c25;
59
- --fe-bg-hover: #1f2733;
60
- --fe-bg-selected: #23324a;
61
- --fe-border: #2a323e;
62
- --fe-border-strong: #3a4453;
63
- --fe-text: #e5e9f0;
64
- --fe-text-muted: #8b95a7;
65
- --fe-primary: #60a5fa;
66
- --fe-primary-hover: #3b82f6;
106
+ --fe-bg: #15171c;
107
+ --fe-bg-elev: #1a1d23;
108
+ --fe-bg-hover: #1f232a;
109
+ --fe-bg-selected: #1c2740;
110
+ --fe-border: #2e333c;
111
+ --fe-border-soft: #262a32;
112
+ --fe-border-strong: #3d444f;
113
+ --fe-text: #e6e8ec;
114
+ --fe-text-muted: #888f9b;
115
+ /* Dark ink on the light-blue button. White measures 3.16:1 against it —
116
+ * the bar for text on a primary surface is 4.5, and #15171c measures 5.67. */
117
+ --fe-text-on-primary: #15171c;
118
+ --fe-primary: #5b8cff;
119
+ --fe-primary-hover: #7ba3ff;
120
+ --fe-primary-soft: #1c2740;
121
+ --fe-primary-ink: #7ba3ff;
67
122
  --fe-danger: #f87171;
68
123
  --fe-danger-hover: #ef4444;
69
124
  --fe-keep-ok: #4ade80;
@@ -75,16 +130,21 @@
75
130
  .fe:not(.fe--theme-light),
76
131
  /* Same trick for the teleported backdrop in auto mode. */
77
132
  .fe-ctx-backdrop--theme-auto:not(.fe-ctx-backdrop--theme-light) {
78
- --fe-bg: #0f1419;
79
- --fe-bg-elev: #161c25;
80
- --fe-bg-hover: #1f2733;
81
- --fe-bg-selected: #23324a;
82
- --fe-border: #2a323e;
83
- --fe-border-strong: #3a4453;
84
- --fe-text: #e5e9f0;
85
- --fe-text-muted: #8b95a7;
86
- --fe-primary: #60a5fa;
87
- --fe-primary-hover: #3b82f6;
133
+ --fe-bg: #15171c;
134
+ --fe-bg-elev: #1a1d23;
135
+ --fe-bg-hover: #1f232a;
136
+ --fe-bg-selected: #1c2740;
137
+ --fe-border: #2e333c;
138
+ --fe-border-soft: #262a32;
139
+ --fe-border-strong: #3d444f;
140
+ --fe-text: #e6e8ec;
141
+ --fe-text-muted: #888f9b;
142
+ /* See the note on the class-based dark block above. */
143
+ --fe-text-on-primary: #15171c;
144
+ --fe-primary: #5b8cff;
145
+ --fe-primary-hover: #7ba3ff;
146
+ --fe-primary-soft: #1c2740;
147
+ --fe-primary-ink: #7ba3ff;
88
148
  --fe-danger: #f87171;
89
149
  --fe-danger-hover: #ef4444;
90
150
  --fe-keep-ok: #4ade80;
@@ -95,18 +155,34 @@
95
155
 
96
156
  /* === cila:a — file-type icon accent tokens (lib/fileIcons families) === */
97
157
  :root {
98
- --fe-icon-folder: #d97706;
99
- --fe-icon-image: #7c3aed;
100
- --fe-icon-video: #db2777;
158
+ /* gorunum:v1 — the folder yellow, same value in both variants. A folder is
159
+ * the only glyph that is filled, and the one people find by colour before
160
+ * they read the name; darkening it for the light theme made it read as an
161
+ * archive box. */
162
+ --fe-icon-folder: #f4b400;
163
+ /* gorunum:v1 — these are now the fill of a chip, not the stroke of a glyph,
164
+ * so they are read as a block of colour and a family has to be one glance
165
+ * apart from its neighbours. Documents and code share the blue on purpose:
166
+ * both are "something you open and read", and splitting them made every
167
+ * listing look like a paint chart. */
168
+ --fe-icon-image: #2f6ceb;
169
+ --fe-icon-video: #7c3aed;
101
170
  --fe-icon-audio: #0d9488;
102
171
  --fe-icon-pdf: #dc2626;
103
- --fe-icon-doc: #2563eb;
172
+ --fe-icon-doc: #2f6ceb;
104
173
  --fe-icon-sheet: #16a34a;
105
174
  --fe-icon-slides: #ea580c;
106
- --fe-icon-archive: #78716c;
107
- --fe-icon-code: #4f46e5;
108
- --fe-icon-text: #64748b;
109
- --fe-icon-unknown: #64748b; /* ui-fix: 3:1 UI contrast on the light theme */
175
+ --fe-icon-archive: #6b7280;
176
+ --fe-icon-code: #2f6ceb;
177
+ --fe-icon-text: #374151;
178
+ --fe-icon-unknown: #6b7280;
179
+ /* Not file types: the two rows that are PLACES. A drive is infrastructure
180
+ * rather than content, so it takes a slate that sits beside the type
181
+ * colours without competing with any of them — it is the first thing on the
182
+ * multi-storage root and should read as a shelf, not as another document.
183
+ * The bin is deliberately the quietest chip in the set. */
184
+ --fe-icon-storage: #475569;
185
+ --fe-icon-trash: #6b7280;
110
186
  }
111
187
 
112
188
  /* Dark values — same selector strategy as the palette blocks above. */
@@ -120,34 +196,38 @@
120
196
  .fe-ctx-backdrop--theme-dark,
121
197
  .fe-ctx-backdrop--theme-auto[data-prefers-dark='1'] {
122
198
  --fe-icon-folder: #fbbf24;
123
- --fe-icon-image: #a78bfa;
124
- --fe-icon-video: #f472b6;
199
+ --fe-icon-image: #4d7ff0;
200
+ --fe-icon-video: #9061f0;
125
201
  --fe-icon-audio: #2dd4bf;
126
- --fe-icon-pdf: #f87171;
127
- --fe-icon-doc: #60a5fa;
128
- --fe-icon-sheet: #4ade80;
202
+ --fe-icon-pdf: #e05252;
203
+ --fe-icon-doc: #4d7ff0;
204
+ --fe-icon-sheet: #2eab63;
129
205
  --fe-icon-slides: #fb923c;
130
- --fe-icon-archive: #a8a29e;
131
- --fe-icon-code: #818cf8;
132
- --fe-icon-text: #94a3b8;
133
- --fe-icon-unknown: #8b95a7;
206
+ --fe-icon-archive: #79808d;
207
+ --fe-icon-code: #4d7ff0;
208
+ --fe-icon-text: #5b6472;
209
+ --fe-icon-unknown: #79808d;
210
+ --fe-icon-storage: #8b95a6;
211
+ --fe-icon-trash: #79808d;
134
212
  }
135
213
 
136
214
  @media (prefers-color-scheme: dark) {
137
215
  .fe:not(.fe--theme-light),
138
216
  .fe-ctx-backdrop--theme-auto:not(.fe-ctx-backdrop--theme-light) {
139
- --fe-icon-folder: #fbbf24;
140
- --fe-icon-image: #a78bfa;
141
- --fe-icon-video: #f472b6;
217
+ --fe-icon-folder: #f4b400;
218
+ --fe-icon-image: #4d7ff0;
219
+ --fe-icon-video: #9061f0;
142
220
  --fe-icon-audio: #2dd4bf;
143
- --fe-icon-pdf: #f87171;
144
- --fe-icon-doc: #60a5fa;
145
- --fe-icon-sheet: #4ade80;
221
+ --fe-icon-pdf: #e05252;
222
+ --fe-icon-doc: #4d7ff0;
223
+ --fe-icon-sheet: #2eab63;
146
224
  --fe-icon-slides: #fb923c;
147
- --fe-icon-archive: #a8a29e;
148
- --fe-icon-code: #818cf8;
149
- --fe-icon-text: #94a3b8;
150
- --fe-icon-unknown: #8b95a7;
225
+ --fe-icon-archive: #79808d;
226
+ --fe-icon-code: #4d7ff0;
227
+ --fe-icon-text: #5b6472;
228
+ --fe-icon-unknown: #79808d;
229
+ --fe-icon-storage: #8b95a6;
230
+ --fe-icon-trash: #79808d;
151
231
  }
152
232
  }
153
233
 
@@ -54,8 +54,17 @@ export interface StorageField {
54
54
  aliases?: string[];
55
55
  }
56
56
 
57
+ /**
58
+ * A driver's runtime feature set, from `backend/internal/storage.Capabilities`.
59
+ *
60
+ * ⚠ `range` was missing from BOTH hand-written copies of this (here and in
61
+ * web/src/api/types.ts) while the server has always sent it — the exact way a
62
+ * type re-declared per package drifts from the struct it describes.
63
+ */
57
64
  export interface StorageDriverCapabilities {
58
65
  read?: boolean;
66
+ /** Ranged reads: the difference between 206/Content-Range and a whole-object copy. */
67
+ range?: boolean;
59
68
  write?: boolean;
60
69
  move?: boolean;
61
70
  copy?: boolean;
@@ -17,6 +17,10 @@
17
17
  * keeping the auto-derived rest).
18
18
  */
19
19
 
20
+ import type { UiProfile } from '../lib/uiProfile';
21
+
22
+ export type { UiProfile };
23
+
20
24
  /**
21
25
  * Auth strategy. Discriminated union so request building is type-safe.
22
26
  *
@@ -185,6 +189,21 @@ export interface ExplorerConfig {
185
189
  /** UI dil kodu */
186
190
  locale?: LocaleCode;
187
191
 
192
+ /**
193
+ * The clock this embed's visitors read dates on by default — an IANA id,
194
+ * `'Asia/Tokyo'`, `'Europe/Istanbul'`, `'UTC'`.
195
+ *
196
+ * Only which clock an instant is READ on changes; the instant itself does
197
+ * not. Unset, a visitor sees the zone of the account behind the credential
198
+ * when that credential is a person's, and otherwise their browser's own.
199
+ *
200
+ * ⚠ A default, not a lock. A visitor who picks a zone in the explorer's own
201
+ * "⋯" → Time zone keeps their pick, in their browser. The whole order, and
202
+ * why it is that order, is `TIME_ZONE_TIERS` in `lib/timezone`. An id the
203
+ * browser does not accept is ignored rather than guessed at.
204
+ */
205
+ timeZone?: string;
206
+
188
207
  /** OnlyOffice iframe base (e.g. `https://docs.example.com`). */
189
208
  onlyOfficeBase?: string;
190
209
 
@@ -259,7 +278,21 @@ export interface ExplorerConfig {
259
278
  */
260
279
  tabStrip?: 'auto' | 'always';
261
280
 
262
- /** Initial path (storage-prefix included: `local://`). Default: root. */
281
+ /**
282
+ * Initial path (storage-prefix included: `local://`). Default: root.
283
+ *
284
+ * gorunum:v3-shell — a VIEW is addressable here too, by its sentinel:
285
+ * `'.home'` opens the overview (storages · recent · starred), and
286
+ * `'.recent'` / `'.starred'` / `'.shared'` / `'.trash'` / `'.tag~invoices'`
287
+ * open theirs. That is how our own `/home` route opens Home without a second
288
+ * mechanism — the sentinels are the same strings the address-bar hash, the
289
+ * tab strip and a restored session already speak (lib/listing
290
+ * VIRTUAL_SEGMENTS), so there is one answer to "where does this explorer
291
+ * start" rather than a path answer and a view answer that can disagree.
292
+ *
293
+ * ⚠ A sentinel this build does not know keeps going to the backend as a
294
+ * folder name, so a host cannot invent views by passing strings.
295
+ */
263
296
  initialPath?: string;
264
297
 
265
298
  /**
@@ -270,6 +303,42 @@ export interface ExplorerConfig {
270
303
  */
271
304
  rootPath?: string;
272
305
 
306
+ /**
307
+ * gorunum:v3-shell — the product mark at the far left of the top bar: the
308
+ * wordmark beside it, and an image for the mark itself.
309
+ *
310
+ * ⚠⚠ This exists because a `<slot>` IS NOT REACHABLE from a host that mounts
311
+ * `<filex-explorer>`, and that is not a bug anybody can fix in a line.
312
+ * Measured, 2026-09-13, in a real browser with Vue's own
313
+ * `defineCustomElement`: a `<span slot="brand">` placed inside the element
314
+ * leaves `Object.keys(slots)` EMPTY in the element's `setup` — with the
315
+ * wrapper forwarding slots, without it, when the node is added after mount,
316
+ * and with `shadowRoot: true` as well. Vue projects light DOM into a custom
317
+ * element only through a NATIVE `<slot>` element inside a shadow root, which
318
+ * this package cannot have: its entire look is one global stylesheet
319
+ * (`styles/base.css`), and a shadow root would leave every embed unstyled.
320
+ *
321
+ * So the slot is for hosts that mount the Vue SFC (our admin app does, and
322
+ * its `#brand` still wins when it is filled); this is for everybody else —
323
+ * and "everybody else" includes OUR OWN DESKTOP APP, which mounts the web
324
+ * component. A logo the web app has and the desktop app silently lacks is
325
+ * exactly the one-surface split this package exists to prevent.
326
+ *
327
+ * ⚠ An `<img src>`, not markup. The mark is drawn from a URL — a file, or a
328
+ * `data:image/svg+xml;base64,…` URI for an inline logo — so a host never
329
+ * hands this package a string to inject. There is no `v-html` on the path.
330
+ *
331
+ * ⚠ Both halves are optional and independent: a name with no mark renders
332
+ * the wordmark alone, a mark with no name renders the glyph alone, and
333
+ * neither renders nothing at all (the collapse control still sits there).
334
+ */
335
+ brand?: {
336
+ /** Wordmark text, e.g. `'filex'`. Printed verbatim, never translated. */
337
+ name?: string;
338
+ /** URL of the mark: a path, an absolute URL, or a `data:` URI. */
339
+ markUrl?: string;
340
+ };
341
+
273
342
  /**
274
343
  * Whether the info-panel (inspector) toggle is shown in the toolbar.
275
344
  * Default true. The inspector itself stays reachable from the context menu.
@@ -277,44 +346,42 @@ export interface ExplorerConfig {
277
346
  showInfoPanel?: boolean;
278
347
 
279
348
  /**
280
- * Which set of chrome the explorer presents. A PRESET, not a feature switch:
281
- * nothing is removed from the code, and every capability stays reachable —
282
- * `'simple'` only changes what is on screen by default.
283
- *
284
- * 'standard' (default) everything: tab strip, split pane, all three view
285
- * modes. The tool the explorer has always been.
286
- * 'simple' — one pane, one folder, list/grid only. The
287
- * navigation panel starts expanded, the tab strip
288
- * and split pane are off, the gallery view mode and
289
- * the host's "How to connect" surface are hidden.
290
- * 'drive' everything `simple` does, plus the shell an end
291
- * user already knows: one primary "New" menu, one
292
- * search field in the header (with its ⌘K/Ctrl+K
293
- * escalation into the command palette), a filter row
294
- * under the breadcrumb, Folders and Files as
295
- * labelled sections in grid view, the details panel
296
- * split into Details / Activity, and a storage line
297
- * under the navigation.
298
- *
299
- * `drive` is a SUPERSET of `simple`, not a sibling: everything `simple`
300
- * turns off stays off, and the code asks `simpleUi` for those questions so a
301
- * later change to `simple` cannot silently miss `drive`. It is a third value
302
- * rather than a second boolean because "which chrome" is ONE question with
303
- * three answers a `driveShell: true` next to `uiProfile: 'standard'` would
304
- * be a combination nobody can describe, and keeping that question single is
305
- * why `uiProfile` was a preset to begin with.
306
- *
307
- * Why it exists (GitHub #14): the reporter's users are not in IT and read
308
- * split panes, tabs and mount instructions as a file manager they would have
309
- * to relearn. The answer was NOT a second UI — one explorer, configured, so
310
- * a fix lands in one place for every surface that mounts this package.
311
- *
312
- * It does not gate the navigation panel. The panel ships in both profiles,
313
- * for administrators too; only its default expanded/collapsed state and the
314
- * rest of the chrome differ. A viewer's own collapse choice, once made,
315
- * outranks the profile — it is a per-viewer preference, not a policy.
316
- */
317
- uiProfile?: 'standard' | 'simple' | 'drive';
349
+ * How much of the explorer to put on screen. A REDUCTION, and only that.
350
+ *
351
+ * 'standard' (default) everything: the tab strip, the split pane and all
352
+ * three view modes, on top of the shell below.
353
+ * 'simple' one pane, one folder, list and grid only. The tab
354
+ * strip and the split pane are off, the gallery view
355
+ * mode is hidden, and the "How to connect" /
356
+ * "API keys" entries default to off.
357
+ *
358
+ * There is no third value and no alias. ⚠ Anything else that arrives here —
359
+ * a typo, or the `'drive'` profile that was REMOVED after v0.40.0
360
+ * resolves to `'standard'` and logs one console line naming it
361
+ * (`lib/uiProfile.resolveUiProfile`, which explains why that direction and
362
+ * not the other). **If you passed `'drive'`, pass `'simple'`.**
363
+ *
364
+ * ⚠⚠ IT DOES NOT DECIDE THE LOOK, and that is the whole of this option.
365
+ * The header with its one wide search field, the "+ New" menu, the filter
366
+ * row, the Folders/Files sections, the Details/Activity tabs and the storage
367
+ * line USED to be gated behind a profile. They are what filex is now — the
368
+ * admin panel, the desktop app and every `<filex-explorer>` embed draw them,
369
+ * with no string passed. Owner's decision, 2026-09-12, verbatim (translated
370
+ * from Turkish): "their app and our app will be one to one. The admin gets
371
+ * one extra button, nothing else. The things we have over them — tabs, split
372
+ * pane, theme choice, icon choice stay."
373
+ *
374
+ * Why the reduction exists at all (GitHub #14): the reporter's users are not
375
+ * in IT and read split panes, tabs and mount instructions as a file manager
376
+ * they would have to relearn. The answer was NOT a second UI one explorer,
377
+ * configured, so a fix lands in one place for every surface that mounts this
378
+ * package.
379
+ *
380
+ * ⚠ It does not gate the navigation panel. The panel ships in every profile,
381
+ * for administrators too; only a viewer's own collapse choice moves it, and
382
+ * that is a per-viewer preference, not a policy.
383
+ */
384
+ uiProfile?: UiProfile;
318
385
 
319
386
  /**
320
387
  * Render the navigation panel (Upload · Recent / Starred / Shared with me /
@@ -449,11 +516,49 @@ export interface ExplorerConfig {
449
516
  * + (optional) display label / read-only flag. The SFC mirrors
450
517
  * each entry as a virtual `dir` row at "/".
451
518
  */
519
+ /**
520
+ * tablo:t1 — remember how each folder was last viewed (view mode + sort),
521
+ * Windows Explorer style. Default ON.
522
+ *
523
+ * ⚠ The opt-out exists for embeds. A product mounting filex in a two-inch
524
+ * panel has one shape it wants and no room to argue with a gallery view
525
+ * arriving from the person's main window; setting this false makes every
526
+ * folder open in the host's chosen default and writes nothing. The columns
527
+ * are NOT covered by this flag — their widths are a global preference about
528
+ * the reader's screen, and a panel that narrow sheds them for want of room
529
+ * before any preference is consulted.
530
+ */
531
+ rememberFolderView?: boolean;
452
532
  storages?: Array<{
453
533
  name: string;
534
+ /**
535
+ * tablo:t1 — the storage's immutable uid, when the host knows it.
536
+ *
537
+ * ⚠ A storage's NAME is editable — that is the point of a name — so it is
538
+ * not a stable address, which is why every protocol also accepts the uid
539
+ * as a path's first segment (`backend/internal/storageref`, filex issue
540
+ * #21). Anything keyed on a storage for the long term should prefer this:
541
+ * per-folder view memory does (`lib/viewPrefs.folderKey`), and folders
542
+ * under a renamed storage keep their remembered view the moment a host
543
+ * starts filling it in. Absent = the name is used and a rename loses the
544
+ * memory, which is a mild, self-healing loss.
545
+ */
546
+ uid?: string;
454
547
  label?: string;
455
548
  driver?: string;
456
549
  readOnly?: boolean;
550
+ /**
551
+ * gorunum:v3-shell — bytes this storage holds, when the host knows. Drawn
552
+ * as the caption on the Home view's storage card; absent means the card
553
+ * names the kind of thing instead.
554
+ *
555
+ * ⚠ It must be the SAME quantity for every caller who gets it (in our own
556
+ * app: `/api/admin/storages` for an operator, the RBAC-filtered
557
+ * `/api/files/quota/storages` for everybody else). The per-USER quota is a
558
+ * sum across every storage, so passing that here would print a number
559
+ * about the person under a label about the drive.
560
+ */
561
+ usedBytes?: number;
457
562
  }>;
458
563
 
459
564
  /**
@@ -76,7 +76,31 @@ export interface ExternalServiceStatus {
76
76
  detail?: string;
77
77
  }
78
78
 
79
+ /**
80
+ * One document type the SERVER can create, from `capabilities.newdoc_types`.
81
+ *
82
+ * ⚠ `requires` is the whole point of shipping this list instead of hardcoding
83
+ * one in the client. The server knows it holds template bytes for `.docx`; it
84
+ * does NOT know whether this deployment has a document server that can open
85
+ * one. So it names the dependency and the client — which already resolves
86
+ * OnlyOffice/drawio, config override included — answers it. A client that
87
+ * re-derived "docx needs OnlyOffice" from a list of its own would be a second
88
+ * source of truth, and the one that rots first.
89
+ */
90
+ export interface NewDocType {
91
+ /** Extension without the dot, lowercase. Also the key the create call sends. */
92
+ ext: string;
93
+ /** Coarse family, used for the picker's section headings. */
94
+ group: 'document' | 'text' | 'diagram';
95
+ mime: string;
96
+ /** External service the editor for this type needs; absent = built-in. */
97
+ requires?: 'onlyoffice' | 'drawio';
98
+ }
99
+
79
100
  export interface Capabilities {
101
+ /** Document types this build can create. Absent on a server older than the
102
+ * "New document" feature — hosts must treat that as "offer nothing". */
103
+ newdoc_types?: NewDocType[];
80
104
  ffmpeg?: boolean;
81
105
  ghostscript?: boolean;
82
106
  libreoffice?: boolean;
@@ -94,6 +118,10 @@ export interface Capabilities {
94
118
  * ExplorerConfig.callerKind. Absent on a server older than the app/user
95
119
  * token split, which is why every reader treats "missing" as a person. */
96
120
  caller_kind?: 'user' | 'app';
121
+ /** The address this deployment is reached at — only when it is real (the
122
+ * operator configured it, or the request came in on a tenant's host).
123
+ * Read by the connection guides; see `connectionsOrigin`. */
124
+ public_url?: string;
97
125
  external?: {
98
126
  onlyoffice?: ExternalServiceStatus;
99
127
  drawio?: ExternalServiceStatus;
@@ -10,6 +10,8 @@
10
10
  * decide whether to extract or download.
11
11
  */
12
12
  import { computed, onMounted, ref, watch } from 'vue';
13
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
14
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
13
15
 
14
16
  interface ArchiveEntry {
15
17
  name: string;
@@ -22,7 +24,7 @@ const props = defineProps<{
22
24
  url: string;
23
25
  filePath?: string;
24
26
  ext: string;
25
- t?: (key: string) => string;
27
+ t?: (key: string, vars?: Record<string, string | number>) => string;
26
28
  authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
27
29
  authCredentials?: RequestCredentials;
28
30
  /** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',
@@ -37,8 +39,11 @@ const entries = ref<ArchiveEntry[]>([]);
37
39
  const loading = ref(true);
38
40
  const error = ref<string | null>(null);
39
41
 
40
- function tt(key: string, fallback: string): string {
41
- return props.t ? props.t(key) : fallback;
42
+ /** ⚠ `vars` go THROUGH `t()`, not into a `.replace()` afterwards: `t()` is what
43
+ * picks the singular (`viewer.archive.entries_one`) from the count, and it can
44
+ * only do that if it is told the count. Same shape CsvViewer's helper has. */
45
+ function tt(key: string, fallback: string, vars?: Record<string, string | number>): string {
46
+ return props.t ? props.t(key, vars) : fallback;
42
47
  }
43
48
 
44
49
  function fmtSize(n: number): string {
@@ -87,25 +92,54 @@ watch(() => props.filePath, load);
87
92
 
88
93
  const totalSize = computed(() => entries.value.reduce((sum, e) => sum + (e.size || 0), 0));
89
94
  const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
95
+
96
+ /* === ikon:emoji — the fallback screen's mark ==========================
97
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
98
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
99
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
100
+ * the person just clicked is wearing, so the fallback is recognisably about
101
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
102
+ * still picture can say "still going". */
103
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
104
+
105
+ /** A row inside the archive, drawn the way the listing draws the same kind. */
106
+ function entryTile(e: ArchiveEntry): string {
107
+ const name = e.name || '';
108
+ const dot = name.lastIndexOf('.');
109
+ return fileIconTile({
110
+ type: e.is_dir ? 'dir' : 'file',
111
+ extension: dot > 0 ? name.slice(dot + 1) : '',
112
+ basename: name,
113
+ });
114
+ }
90
115
  </script>
91
116
 
92
117
  <template>
93
118
  <div class="filex-viewer-archive">
94
119
  <div v-if="error" class="filex-viewer-fallback">
95
- <span class="filex-viewer-fallback__icon">🗜️</span>
120
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
121
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
96
122
  <p>{{ error }}</p>
97
123
  </div>
98
124
  <div v-else-if="loading" class="filex-viewer-fallback">
99
- <span class="filex-viewer-fallback__icon">⏳</span>
125
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
126
+ <span
127
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
128
+ aria-hidden="true"
129
+ v-html="actionIconSvg('progress')"
130
+ ></span>
100
131
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
101
132
  </div>
102
133
  <div v-else-if="entries.length === 0" class="filex-viewer-fallback">
103
- <span class="filex-viewer-fallback__icon">🗜️</span>
134
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
135
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
104
136
  <p>{{ tt('viewer.archive.empty', 'Archive is empty.') }}</p>
105
137
  </div>
106
138
  <div v-else class="filex-viewer-archive__pane">
107
139
  <div class="filex-viewer-archive__summary">
108
- {{ fileCount }} {{ tt('viewer.archive.entries', '{n} files').replace('{n}', String(fileCount)) }}
140
+ <!-- The count is IN the message ("{n} files"). A `{{ fileCount }}`
141
+ printed in front of it read "3 3 files". -->
142
+ {{ tt('viewer.archive.entries', `${fileCount} files`, { n: fileCount }) }}
109
143
  · {{ fmtSize(totalSize) }}
110
144
  </div>
111
145
  <table class="filex-viewer-archive__table">
@@ -118,7 +152,11 @@ const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
118
152
  <tbody>
119
153
  <tr v-for="(e, i) in entries" :key="i" :data-dir="e.is_dir ? '1' : '0'">
120
154
  <td>
121
- <span class="filex-viewer-archive__icon">{{ e.is_dir ? '📁' : '📄' }}</span>
155
+ <!-- ikon:emoji the same tile the listing draws for the same
156
+ kind of file, so an archive's contents and the folder it
157
+ came from read as one product. -->
158
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
159
+ <span class="filex-viewer-archive__icon" aria-hidden="true" v-html="entryTile(e)"></span>
122
160
  {{ e.name }}
123
161
  </td>
124
162
  <td class="filex-viewer-archive__size">{{ e.is_dir ? '' : fmtSize(e.size) }}</td>