@brftech/filex-core 0.4.2 → 0.6.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 (58) hide show
  1. package/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
  2. package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
  3. package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-BvriJ42M.js} +2 -2
  4. package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
  5. package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-B_bSPsWX.js} +2 -2
  6. package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
  7. package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-hl68h4gG.js} +2 -2
  8. package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
  9. package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-lhwOjXK7.js} +2 -2
  10. package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
  11. package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-H2Z6AYXg.js} +2 -2
  12. package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
  13. package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Bsk-11yH.js} +2 -2
  14. package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
  15. package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-D-j9vycG.js} +2 -2
  16. package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
  17. package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-Bcr70rTe.js} +2 -2
  18. package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
  19. package/dist/filex-core.js +50 -25
  20. package/dist/filex-core.umd.cjs +59 -58
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-C_ZSnk_I.js +11109 -0
  23. package/dist/index-C_ZSnk_I.js.map +1 -0
  24. package/dist/index.d.ts +597 -4
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +636 -15
  28. package/src/components/CommandPalette.vue +24 -1
  29. package/src/components/ContextMenu.vue +114 -8
  30. package/src/components/GalleryView.vue +214 -0
  31. package/src/components/GridView.vue +34 -1
  32. package/src/components/InspectorPanel.vue +170 -0
  33. package/src/components/ListView.vue +40 -6
  34. package/src/components/OnboardingTour.vue +332 -0
  35. package/src/components/OperationsCenter.vue +284 -0
  36. package/src/components/PendingOpsTray.vue +50 -66
  37. package/src/components/QuickLook.vue +142 -0
  38. package/src/components/SecondaryPane.vue +411 -0
  39. package/src/components/ShortcutSettings.vue +270 -0
  40. package/src/components/ShortcutsHelp.vue +30 -9
  41. package/src/components/TabBar.vue +174 -0
  42. package/src/components/ThemeGallery.vue +253 -0
  43. package/src/components/Toolbar.vue +75 -11
  44. package/src/components/UploadProgress.vue +49 -57
  45. package/src/composables/useFileApi.ts +53 -0
  46. package/src/composables/useKeyboardShortcuts.ts +340 -137
  47. package/src/composables/useOperations.ts +291 -0
  48. package/src/composables/useTabs.ts +222 -0
  49. package/src/index.ts +51 -0
  50. package/src/lib/dragGhost.ts +41 -0
  51. package/src/lib/themes.ts +505 -0
  52. package/src/locales/en.ts +125 -0
  53. package/src/locales/tr.ts +125 -0
  54. package/src/modals/Modal.vue +41 -5
  55. package/src/styles/base.css +1210 -0
  56. package/src/types/FileNode.ts +1 -1
  57. package/dist/index-BLocdbmc.js +0 -7993
  58. package/dist/index-BLocdbmc.js.map +0 -1
@@ -0,0 +1,505 @@
1
+ /**
2
+ * themes.ts — wiring:c1 theme registry + application engine.
3
+ *
4
+ * A "theme" is a palette-only preset: a map of `--fe-*` CSS custom
5
+ * properties in TWO variants (light + dark). Picking a theme is fully
6
+ * independent from the light/dark MODE — the mode (config.theme prop /
7
+ * OS preference) keeps deciding which variant of the selected theme is
8
+ * active, exactly like the stock palette in styles/variables.css.
9
+ *
10
+ * Application strategy (two cooperating layers):
11
+ *
12
+ * 1. Inline `--fe-*` variables on the explorer ROOT element
13
+ * (`applyThemeToEl`) — the documented "consumer overrides by setting
14
+ * --fe-* on a higher scope" philosophy, resolved to the currently
15
+ * active variant. Covers the whole `.fe` subtree, works in embeds
16
+ * without touching the host page's own theme.
17
+ *
18
+ * 2. A singleton injected `<style data-filex-theme>` (`syncThemeStyle`)
19
+ * that mirrors variables.css' EXACT selector cascade with the
20
+ * selected theme's values. This is required because some surfaces
21
+ * re-declare the variables on themselves and would otherwise shadow
22
+ * the inherited inline values back to the stock palette:
23
+ * - the Teleport-ed context menu backdrop (`.fe-ctx-backdrop…`
24
+ * lives under <body>, outside the root element entirely), and
25
+ * - modal backdrops, which carry their own `.fe` /
26
+ * `.fe--theme-dark` classes (dark-mode selectors like
27
+ * `.dark .fe` re-set the tokens on those descendants).
28
+ * Because the injected element is appended to <head> at runtime it
29
+ * always comes AFTER the bundled stylesheet, so equal-specificity
30
+ * rules win by source order — the same mechanism variables.css uses
31
+ * internally. No `!important` needed.
32
+ *
33
+ * The `default` theme applies NOTHING (inline vars removed, style
34
+ * element emptied) so host-level `--fe-*` overrides keep working
35
+ * untouched — its token maps below exist only to render the gallery
36
+ * preview card.
37
+ *
38
+ * Persistence: localStorage `filex.theme` (absent/invalid → default),
39
+ * shared reactively across every explorer instance on the page and
40
+ * synced across tabs via the `storage` event.
41
+ *
42
+ * All palettes were contrast-checked (WCAG 2.1): text/bg ≥ 7:1,
43
+ * text on elevated/hover/selected surfaces + muted text + text-on-primary
44
+ * ≥ 4.5:1, primary & danger vs bg ≥ 3:1 — in BOTH variants.
45
+ */
46
+
47
+ import { ref, type Ref } from 'vue';
48
+
49
+ /** Map of `--fe-*` custom property → value. */
50
+ export type ThemeTokenMap = Record<string, string>;
51
+
52
+ export interface ThemeDef {
53
+ /** Stable id — persisted in localStorage, never shown to users. */
54
+ id: string;
55
+ /** i18n catalogue key for the display name (tr + en). */
56
+ nameKey: string;
57
+ /** Palette applied when the resolved mode is light. */
58
+ light: ThemeTokenMap;
59
+ /** Palette applied when the resolved mode is dark. */
60
+ dark: ThemeTokenMap;
61
+ }
62
+
63
+ export const THEME_LS_KEY = 'filex.theme';
64
+ export const DEFAULT_THEME_ID = 'default';
65
+
66
+ /* ------------------------------------------------------------------ */
67
+ /* Registry */
68
+ /* ------------------------------------------------------------------ */
69
+
70
+ export const THEMES: ThemeDef[] = [
71
+ {
72
+ // Stock palette — the maps duplicate variables.css ONLY for the
73
+ // gallery preview; applying this theme clears all overrides.
74
+ id: DEFAULT_THEME_ID,
75
+ nameKey: 'theme.name.default',
76
+ light: {
77
+ '--fe-bg': '#ffffff',
78
+ '--fe-bg-elev': '#f7f8fa',
79
+ '--fe-bg-hover': '#edf0f5',
80
+ '--fe-bg-selected': '#dfe8ff',
81
+ '--fe-border': '#e2e6ed',
82
+ '--fe-border-strong': '#c7ced9',
83
+ '--fe-text': '#1a1e27',
84
+ '--fe-text-muted': '#5a6475',
85
+ '--fe-text-on-primary': '#ffffff',
86
+ '--fe-primary': '#3b82f6',
87
+ '--fe-primary-hover': '#2563eb',
88
+ '--fe-danger': '#dc2626',
89
+ '--fe-danger-hover': '#b91c1c',
90
+ },
91
+ dark: {
92
+ '--fe-bg': '#0f1419',
93
+ '--fe-bg-elev': '#161c25',
94
+ '--fe-bg-hover': '#1f2733',
95
+ '--fe-bg-selected': '#23324a',
96
+ '--fe-border': '#2a323e',
97
+ '--fe-border-strong': '#3a4453',
98
+ '--fe-text': '#e5e9f0',
99
+ '--fe-text-muted': '#8b95a7',
100
+ '--fe-text-on-primary': '#ffffff',
101
+ '--fe-primary': '#60a5fa',
102
+ '--fe-primary-hover': '#3b82f6',
103
+ '--fe-danger': '#f87171',
104
+ '--fe-danger-hover': '#ef4444',
105
+ },
106
+ },
107
+ {
108
+ // Gece Mavisi — deep indigo/navy.
109
+ id: 'night',
110
+ nameKey: 'theme.name.night',
111
+ light: {
112
+ '--fe-bg': '#f6f8fd',
113
+ '--fe-bg-elev': '#edf1fa',
114
+ '--fe-bg-hover': '#e2e9f7',
115
+ '--fe-bg-selected': '#d2ddf4',
116
+ '--fe-border': '#d6deee',
117
+ '--fe-border-strong': '#aebfda',
118
+ '--fe-text': '#151f38',
119
+ '--fe-text-muted': '#49587a',
120
+ '--fe-text-on-primary': '#ffffff',
121
+ '--fe-primary': '#2c4a9e',
122
+ '--fe-primary-hover': '#213a80',
123
+ '--fe-danger': '#c62828',
124
+ '--fe-danger-hover': '#a51f1f',
125
+ },
126
+ dark: {
127
+ '--fe-bg': '#0a1024',
128
+ '--fe-bg-elev': '#111a35',
129
+ '--fe-bg-hover': '#1a2547',
130
+ '--fe-bg-selected': '#243665',
131
+ '--fe-border': '#26335a',
132
+ '--fe-border-strong': '#3c4e84',
133
+ '--fe-text': '#dfe6f7',
134
+ '--fe-text-muted': '#96a5cc',
135
+ '--fe-text-on-primary': '#050b26',
136
+ '--fe-primary': '#6d8dfc',
137
+ '--fe-primary-hover': '#5273f2',
138
+ '--fe-danger': '#f87171',
139
+ '--fe-danger-hover': '#ef4444',
140
+ },
141
+ },
142
+ {
143
+ // Orman — calm greens.
144
+ id: 'forest',
145
+ nameKey: 'theme.name.forest',
146
+ light: {
147
+ '--fe-bg': '#f5faf5',
148
+ '--fe-bg-elev': '#eaf3eb',
149
+ '--fe-bg-hover': '#dcecdf',
150
+ '--fe-bg-selected': '#c8e2ce',
151
+ '--fe-border': '#d2e2d4',
152
+ '--fe-border-strong': '#a2c3a8',
153
+ '--fe-text': '#182b1d',
154
+ '--fe-text-muted': '#42604a',
155
+ '--fe-text-on-primary': '#ffffff',
156
+ '--fe-primary': '#2b7a41',
157
+ '--fe-primary-hover': '#226334',
158
+ '--fe-danger': '#c62828',
159
+ '--fe-danger-hover': '#a51f1f',
160
+ },
161
+ dark: {
162
+ '--fe-bg': '#0d1611',
163
+ '--fe-bg-elev': '#132019',
164
+ '--fe-bg-hover': '#1b2d23',
165
+ '--fe-bg-selected': '#264532',
166
+ '--fe-border': '#28402f',
167
+ '--fe-border-strong': '#3e5f49',
168
+ '--fe-text': '#dcebe0',
169
+ '--fe-text-muted': '#93b09b',
170
+ '--fe-text-on-primary': '#04180b',
171
+ '--fe-primary': '#54c17a',
172
+ '--fe-primary-hover': '#3fae66',
173
+ '--fe-danger': '#f87171',
174
+ '--fe-danger-hover': '#ef4444',
175
+ },
176
+ },
177
+ {
178
+ // Kehribar — warm amber/bronze.
179
+ id: 'amber',
180
+ nameKey: 'theme.name.amber',
181
+ light: {
182
+ '--fe-bg': '#fdf9f0',
183
+ '--fe-bg-elev': '#f7efdd',
184
+ '--fe-bg-hover': '#f0e3c8',
185
+ '--fe-bg-selected': '#ead6ab',
186
+ '--fe-border': '#e6dabf',
187
+ '--fe-border-strong': '#c6b184',
188
+ '--fe-text': '#33270f',
189
+ '--fe-text-muted': '#655631',
190
+ '--fe-text-on-primary': '#ffffff',
191
+ '--fe-primary': '#9a4b00',
192
+ '--fe-primary-hover': '#7c3c00',
193
+ '--fe-danger': '#c62828',
194
+ '--fe-danger-hover': '#a51f1f',
195
+ },
196
+ dark: {
197
+ '--fe-bg': '#191307',
198
+ '--fe-bg-elev': '#231a0c',
199
+ '--fe-bg-hover': '#2f2312',
200
+ '--fe-bg-selected': '#44331b',
201
+ '--fe-border': '#3b2e17',
202
+ '--fe-border-strong': '#5b4a29',
203
+ '--fe-text': '#f2e7d4',
204
+ '--fe-text-muted': '#bda887',
205
+ '--fe-text-on-primary': '#2a1c02',
206
+ '--fe-primary': '#f5a524',
207
+ '--fe-primary-hover': '#ffb84d',
208
+ '--fe-danger': '#f87171',
209
+ '--fe-danger-hover': '#ef4444',
210
+ },
211
+ },
212
+ {
213
+ // Leylak — soft purple.
214
+ id: 'lilac',
215
+ nameKey: 'theme.name.lilac',
216
+ light: {
217
+ '--fe-bg': '#faf7fd',
218
+ '--fe-bg-elev': '#f2ecfa',
219
+ '--fe-bg-hover': '#e8ddf5',
220
+ '--fe-bg-selected': '#dccaf0',
221
+ '--fe-border': '#e1d6ee',
222
+ '--fe-border-strong': '#bda6d9',
223
+ '--fe-text': '#241a33',
224
+ '--fe-text-muted': '#584871',
225
+ '--fe-text-on-primary': '#ffffff',
226
+ '--fe-primary': '#7231e0',
227
+ '--fe-primary-hover': '#5f21c4',
228
+ '--fe-danger': '#c62828',
229
+ '--fe-danger-hover': '#a51f1f',
230
+ },
231
+ dark: {
232
+ '--fe-bg': '#130f1c',
233
+ '--fe-bg-elev': '#1b1527',
234
+ '--fe-bg-hover': '#261d37',
235
+ '--fe-bg-selected': '#37294f',
236
+ '--fe-border': '#302546',
237
+ '--fe-border-strong': '#4b3a6d',
238
+ '--fe-text': '#e9e2f5',
239
+ '--fe-text-muted': '#a999c5',
240
+ '--fe-text-on-primary': '#180d33',
241
+ '--fe-primary': '#b197fa',
242
+ '--fe-primary-hover': '#9c7cf4',
243
+ '--fe-danger': '#f87171',
244
+ '--fe-danger-hover': '#ef4444',
245
+ },
246
+ },
247
+ {
248
+ // Yüksek Kontrast — black/white with strong borders, saturated accents.
249
+ id: 'contrast',
250
+ nameKey: 'theme.name.contrast',
251
+ light: {
252
+ '--fe-bg': '#ffffff',
253
+ '--fe-bg-elev': '#f2f2f2',
254
+ '--fe-bg-hover': '#e0e0e0',
255
+ '--fe-bg-selected': '#c9dcff',
256
+ '--fe-border': '#5c5c5c',
257
+ '--fe-border-strong': '#000000',
258
+ '--fe-text': '#000000',
259
+ '--fe-text-muted': '#3d3d3d',
260
+ '--fe-text-on-primary': '#ffffff',
261
+ '--fe-primary': '#003d99',
262
+ '--fe-primary-hover': '#002a6b',
263
+ '--fe-danger': '#a80000',
264
+ '--fe-danger-hover': '#7d0000',
265
+ '--fe-shadow': '0 0 0 1px #000000, 0 10px 32px rgba(0, 0, 0, 0.25)',
266
+ '--fe-shadow-sm': '0 0 0 1px #000000',
267
+ },
268
+ dark: {
269
+ '--fe-bg': '#000000',
270
+ '--fe-bg-elev': '#0d0d0d',
271
+ '--fe-bg-hover': '#212121',
272
+ '--fe-bg-selected': '#003d80',
273
+ '--fe-border': '#8f8f8f',
274
+ '--fe-border-strong': '#ffffff',
275
+ '--fe-text': '#ffffff',
276
+ '--fe-text-muted': '#d6d6d6',
277
+ '--fe-text-on-primary': '#001430',
278
+ '--fe-primary': '#7ab8ff',
279
+ '--fe-primary-hover': '#9ccaff',
280
+ '--fe-danger': '#ff7575',
281
+ '--fe-danger-hover': '#ff9999',
282
+ '--fe-shadow': '0 0 0 1px #ffffff, 0 12px 32px rgba(0, 0, 0, 0.65)',
283
+ '--fe-shadow-sm': '0 0 0 1px #ffffff',
284
+ },
285
+ },
286
+ {
287
+ // Yumuşak Gri — desaturated, quiet neutral.
288
+ id: 'gray',
289
+ nameKey: 'theme.name.gray',
290
+ light: {
291
+ '--fe-bg': '#f7f7f8',
292
+ '--fe-bg-elev': '#efeff1',
293
+ '--fe-bg-hover': '#e4e4e7',
294
+ '--fe-bg-selected': '#d6d7db',
295
+ '--fe-border': '#dfdfe2',
296
+ '--fe-border-strong': '#b9b9c0',
297
+ '--fe-text': '#26272b',
298
+ '--fe-text-muted': '#585a63',
299
+ '--fe-text-on-primary': '#ffffff',
300
+ '--fe-primary': '#4b4b54',
301
+ '--fe-primary-hover': '#38383f',
302
+ '--fe-danger': '#c62828',
303
+ '--fe-danger-hover': '#a51f1f',
304
+ },
305
+ dark: {
306
+ '--fe-bg': '#131316',
307
+ '--fe-bg-elev': '#1b1b1f',
308
+ '--fe-bg-hover': '#26262b',
309
+ '--fe-bg-selected': '#35353c',
310
+ '--fe-border': '#2d2d33',
311
+ '--fe-border-strong': '#4a4a52',
312
+ '--fe-text': '#e6e6e9',
313
+ '--fe-text-muted': '#a4a4ad',
314
+ '--fe-text-on-primary': '#17171b',
315
+ '--fe-primary': '#b0b0ba',
316
+ '--fe-primary-hover': '#c4c4cd',
317
+ '--fe-danger': '#f87171',
318
+ '--fe-danger-hover': '#ef4444',
319
+ },
320
+ },
321
+ {
322
+ // Terminal Yeşili — phosphor green, monospace face for the full CRT vibe.
323
+ id: 'terminal',
324
+ nameKey: 'theme.name.terminal',
325
+ light: {
326
+ '--fe-bg': '#f3f9f4',
327
+ '--fe-bg-elev': '#e5f2e8',
328
+ '--fe-bg-hover': '#d5ead9',
329
+ '--fe-bg-selected': '#bce3c6',
330
+ '--fe-border': '#cbe2d1',
331
+ '--fe-border-strong': '#94c2a0',
332
+ '--fe-text': '#0c2913',
333
+ '--fe-text-muted': '#31573c',
334
+ '--fe-text-on-primary': '#ffffff',
335
+ '--fe-primary': '#116b33',
336
+ '--fe-primary-hover': '#0c5427',
337
+ '--fe-danger': '#c62828',
338
+ '--fe-danger-hover': '#a51f1f',
339
+ '--fe-font': 'ui-monospace, "SF Mono", Consolas, Menlo, monospace',
340
+ },
341
+ dark: {
342
+ '--fe-bg': '#050d07',
343
+ '--fe-bg-elev': '#0a1710',
344
+ '--fe-bg-hover': '#112418',
345
+ '--fe-bg-selected': '#1a3a26',
346
+ '--fe-border': '#1c3a2a',
347
+ '--fe-border-strong': '#316144',
348
+ '--fe-text': '#4fdd8b',
349
+ '--fe-text-muted': '#38a668',
350
+ '--fe-text-on-primary': '#03180a',
351
+ '--fe-primary': '#25c95e',
352
+ '--fe-primary-hover': '#4fdd8b',
353
+ '--fe-danger': '#ff6b62',
354
+ '--fe-danger-hover': '#ff8d86',
355
+ '--fe-font': 'ui-monospace, "SF Mono", Consolas, Menlo, monospace',
356
+ },
357
+ },
358
+ ];
359
+
360
+ export function themeById(id: string | null | undefined): ThemeDef | undefined {
361
+ return THEMES.find((t) => t.id === id);
362
+ }
363
+
364
+ /** Every token key ANY theme touches — used to fully clear inline overrides
365
+ * when switching themes (a theme that skips a token must not inherit the
366
+ * previous theme's value for it). */
367
+ const ALL_TOKEN_KEYS: string[] = Array.from(
368
+ new Set(THEMES.flatMap((t) => [...Object.keys(t.light), ...Object.keys(t.dark)])),
369
+ );
370
+
371
+ /* ------------------------------------------------------------------ */
372
+ /* Shared reactive state + persistence */
373
+ /* ------------------------------------------------------------------ */
374
+
375
+ function readStoredThemeId(): string {
376
+ try {
377
+ const v = localStorage.getItem(THEME_LS_KEY);
378
+ return v && themeById(v) ? v : DEFAULT_THEME_ID;
379
+ } catch {
380
+ return DEFAULT_THEME_ID;
381
+ }
382
+ }
383
+
384
+ // Module-level singleton so every explorer instance on the page follows the
385
+ // same selection instantly.
386
+ const themeId: Ref<string> = ref(
387
+ typeof window === 'undefined' ? DEFAULT_THEME_ID : readStoredThemeId(),
388
+ );
389
+
390
+ export function setTheme(id: string): void {
391
+ const valid = themeById(id) ? id : DEFAULT_THEME_ID;
392
+ themeId.value = valid;
393
+ try {
394
+ if (valid === DEFAULT_THEME_ID) localStorage.removeItem(THEME_LS_KEY);
395
+ else localStorage.setItem(THEME_LS_KEY, valid);
396
+ } catch {
397
+ /* quota / private mode */
398
+ }
399
+ }
400
+
401
+ /** Reactive handle for components: `{ themeId, setTheme }`. */
402
+ export function useThemeState(): { themeId: Ref<string>; setTheme: (id: string) => void } {
403
+ return { themeId, setTheme };
404
+ }
405
+
406
+ // Cross-tab sync — another tab changing the preference updates this one live.
407
+ if (typeof window !== 'undefined') {
408
+ try {
409
+ window.addEventListener('storage', (e) => {
410
+ if (e.key !== THEME_LS_KEY) return;
411
+ themeId.value = e.newValue && themeById(e.newValue) ? e.newValue : DEFAULT_THEME_ID;
412
+ });
413
+ } catch {
414
+ /* non-browser env */
415
+ }
416
+ }
417
+
418
+ /* ------------------------------------------------------------------ */
419
+ /* Application — layer 1: inline vars on the explorer root */
420
+ /* ------------------------------------------------------------------ */
421
+
422
+ /**
423
+ * Set (or clear, for the default theme) the selected theme's tokens as
424
+ * inline CSS variables on the explorer root element, resolved to the
425
+ * active variant. Inline style wins over every stylesheet rule, so the
426
+ * root subtree is always correct regardless of host CSS.
427
+ */
428
+ export function applyThemeToEl(el: HTMLElement, id: string, dark: boolean): void {
429
+ for (const key of ALL_TOKEN_KEYS) el.style.removeProperty(key);
430
+ if (id === DEFAULT_THEME_ID) return;
431
+ const theme = themeById(id);
432
+ if (!theme) return;
433
+ const map = dark ? theme.dark : theme.light;
434
+ for (const [key, value] of Object.entries(map)) el.style.setProperty(key, value);
435
+ }
436
+
437
+ /* ------------------------------------------------------------------ */
438
+ /* Application — layer 2: injected stylesheet for shadowed surfaces */
439
+ /* ------------------------------------------------------------------ */
440
+
441
+ const STYLE_ATTR = 'data-filex-theme';
442
+
443
+ function cssDecls(map: ThemeTokenMap): string {
444
+ return Object.entries(map)
445
+ .map(([k, v]) => `${k}:${v};`)
446
+ .join('');
447
+ }
448
+
449
+ /**
450
+ * Generate a stylesheet that mirrors styles/variables.css' selector
451
+ * cascade 1:1 (light base → explicit-dark selectors → prefers-dark media
452
+ * block) with the theme's palette. Appended after the bundled CSS it
453
+ * overrides every surface — teleported context menus, modal backdrops —
454
+ * in whichever mode they resolve to, without JS having to track them.
455
+ */
456
+ export function generateThemeCss(theme: ThemeDef): string {
457
+ const light = cssDecls(theme.light);
458
+ const dark = cssDecls(theme.dark);
459
+ const darkSelectors = [
460
+ '.fe--theme-dark',
461
+ ":root[data-theme='dark'] .fe",
462
+ '.fe.fe--theme-dark',
463
+ ':root.dark',
464
+ '.dark',
465
+ ':root.dark .fe',
466
+ '.dark .fe',
467
+ '.fe-ctx-backdrop--theme-dark',
468
+ ".fe-ctx-backdrop--theme-auto[data-prefers-dark='1']",
469
+ ].join(',');
470
+ const autoDarkSelectors = [
471
+ '.fe:not(.fe--theme-light)',
472
+ '.fe-ctx-backdrop--theme-auto:not(.fe-ctx-backdrop--theme-light)',
473
+ ].join(',');
474
+ return (
475
+ `/* filex theme: ${theme.id} */` +
476
+ `:root,.fe{${light}}` +
477
+ `${darkSelectors}{${dark}}` +
478
+ `@media (prefers-color-scheme: dark){${autoDarkSelectors}{${dark}}}`
479
+ );
480
+ }
481
+
482
+ /**
483
+ * Create/update/empty the singleton `<style data-filex-theme>` element in
484
+ * <head>. Idempotent — safe to call from every explorer instance.
485
+ */
486
+ export function syncThemeStyle(id: string): void {
487
+ if (typeof document === 'undefined') return;
488
+ let el = document.head.querySelector<HTMLStyleElement>(`style[${STYLE_ATTR}]`);
489
+ const theme = id === DEFAULT_THEME_ID ? undefined : themeById(id);
490
+ if (!theme) {
491
+ if (el) el.textContent = '';
492
+ return;
493
+ }
494
+ if (!el) {
495
+ el = document.createElement('style');
496
+ el.setAttribute(STYLE_ATTR, '');
497
+ document.head.appendChild(el);
498
+ } else if (el !== document.head.lastElementChild) {
499
+ // Keep it AFTER any stylesheet injected later (e.g. the webcomponent's
500
+ // own core-CSS injection) so equal-specificity rules keep losing to us.
501
+ document.head.appendChild(el);
502
+ }
503
+ const css = generateThemeCss(theme);
504
+ if (el.textContent !== css) el.textContent = css;
505
+ }
package/src/locales/en.ts CHANGED
@@ -240,4 +240,129 @@ export const en: Record<string, string> = {
240
240
  'inspector.perm.manage': 'Manage permissions',
241
241
  'inspector.shares.empty': 'No share links',
242
242
  'inspector.shares.copy': 'Copy link',
243
+
244
+ /* wiring:c1 — theme gallery */
245
+ 'theme.menu': 'Theme',
246
+ 'theme.title': 'Theme Gallery',
247
+ 'theme.hint': 'Themes only change the color palette; light/dark mode still applies separately.',
248
+ 'theme.reset': 'Reset to default',
249
+ 'theme.close': 'Close',
250
+ 'theme.selected': 'Selected',
251
+ 'theme.name.default': 'Default',
252
+ 'theme.name.night': 'Night Blue',
253
+ 'theme.name.forest': 'Forest',
254
+ 'theme.name.amber': 'Amber',
255
+ 'theme.name.lilac': 'Lilac',
256
+ 'theme.name.contrast': 'High Contrast',
257
+ 'theme.name.gray': 'Soft Gray',
258
+ 'theme.name.terminal': 'Terminal Green',
259
+ /* === wiring:c2 — customizable shortcuts + quick look === */
260
+ 'shortcuts.quicklook': 'Quick look',
261
+ 'shortcuts.customize': 'Customize',
262
+ 'shortcuts.settings.menu': 'Shortcuts',
263
+ 'shortcuts.settings.title': 'Shortcut Settings',
264
+ 'shortcuts.settings.hint': 'To remap a shortcut, press "Change" and then the new key combination. Changes are stored in this browser only.',
265
+ 'shortcuts.settings.change': 'Change',
266
+ 'shortcuts.settings.capture': 'Press keys… (Esc cancels)',
267
+ 'shortcuts.settings.reset': 'Reset to default',
268
+ 'shortcuts.settings.reset_all': 'Reset all',
269
+ 'shortcuts.settings.unbound': 'Not set',
270
+ 'shortcuts.settings.fixed': 'Fixed',
271
+ 'shortcuts.settings.customized': 'Customized',
272
+ 'shortcuts.settings.conflict': 'is already used by "{name}".',
273
+ 'shortcuts.settings.conflict_fixed': 'is reserved by the fixed shortcut "{name}" — it cannot be used.',
274
+ 'shortcuts.settings.conflict_take': 'Unassign old and use here',
275
+ 'shortcuts.settings.conflict_cancel': 'Cancel',
276
+ 'quicklook.hint_close': 'close',
277
+ 'quicklook.hint_nav': 'previous/next',
278
+ 'quicklook.hint_open': 'open',
279
+ /* === /wiring:c2 === */
280
+ /* === wiring:c3 — operations center === */
281
+ 'opc.title': 'Operations',
282
+ 'opc.aria_badge': '{n} operations — toggle operations center',
283
+ 'opc.close': 'Close',
284
+ 'opc.active': 'In progress',
285
+ 'opc.history': 'History',
286
+ 'opc.clear': 'Clear',
287
+ 'opc.empty': 'No operations',
288
+ 'opc.cancel': 'Cancel',
289
+ 'opc.retry': 'Retry',
290
+ 'opc.dismiss': 'Dismiss',
291
+ 'opc.queued': 'Queued',
292
+ 'opc.percent': '{n}%',
293
+ 'opc.status.running': 'In progress',
294
+ 'opc.status.done': 'Done',
295
+ 'opc.status.error': 'Failed',
296
+ 'opc.status.aborted': 'Cancelled',
297
+ 'opc.kind.upload': 'Upload',
298
+ 'opc.kind.copy': 'Copy',
299
+ 'opc.kind.move': 'Move',
300
+ 'opc.kind.delete': 'Delete',
301
+ 'opc.kind.convert': 'Convert',
302
+ 'opc.kind.archive': 'Archive',
303
+ /* === wiring:c4 — onboarding tour + accessibility + error card === */
304
+ 'tour.aria': 'Onboarding tour',
305
+ 'tour.restart': 'Restart the tour',
306
+ 'tour.skip': 'Skip tour',
307
+ 'tour.back': 'Back',
308
+ 'tour.next': 'Next',
309
+ 'tour.done': 'Done',
310
+ 'tour.progress': '{n} / {m}',
311
+ 'tour.step.nav.title': 'Storages & folders',
312
+ 'tour.step.nav.desc':
313
+ 'Your current location lives here. Click any crumb to jump back to parent folders or the storage root.',
314
+ 'tour.step.upload.title': 'Upload files',
315
+ 'tour.step.upload.desc':
316
+ 'Pick files with this button — or simply drag & drop them anywhere in the window.',
317
+ 'tour.step.search.title': 'Search',
318
+ 'tour.step.search.desc':
319
+ 'This box filters the current folder. The Ctrl+K command palette searches across all storages and runs commands.',
320
+ 'tour.step.view.title': 'Switch views',
321
+ 'tour.step.view.desc':
322
+ 'Toggle between the list and grid layout; your choice is remembered.',
323
+ 'tour.step.share.title': 'Sharing',
324
+ 'tour.step.share.desc':
325
+ 'Right-click any file and use "Share / Permissions" to create a link with an optional PIN and expiry.',
326
+ 'tour.step.help.title': 'Shortcuts',
327
+ 'tour.step.help.desc':
328
+ 'Press ? for the shortcut cheat-sheet and Ctrl+K for the command palette. Replay this tour any time via "Restart the tour" in the menu.',
329
+ 'error.hint': 'This may be a temporary network or server problem. Please try again.',
330
+ 'error.details': 'Technical details',
331
+ 'col.star': 'Star',
332
+ 'toolbar.view_label': 'View',
333
+ 'list.aria': 'File list',
334
+ 'grid.aria': 'File grid',
335
+
336
+ /* === wiring:d1 — tabs + split pane === */
337
+ 'tabs.strip': 'Tabs',
338
+ 'tabs.new': 'New tab',
339
+ 'tabs.close': 'Close tab',
340
+ 'tabs.split': 'Split view',
341
+ 'tabs.split_off': 'Close split',
342
+ 'ctx.open_new_tab': 'Open in new tab',
343
+ 'shortcuts.group.tabs': 'Tabs',
344
+ 'shortcuts.tab_new': 'New tab',
345
+ 'shortcuts.tab_close': 'Close tab',
346
+ 'shortcuts.tab_next': 'Next tab',
347
+ 'shortcuts.tab_prev': 'Previous tab',
348
+ 'cmd.tab_new': 'Open a new tab',
349
+ 'cmd.split_toggle': 'Toggle split view',
350
+ 'split.pane': 'Secondary pane',
351
+ 'split.close': 'Close the side pane',
352
+ 'split.error': 'Could not load the listing',
353
+ 'split.retry': 'Retry',
354
+ 'split.copy_queued': 'Copy queued',
355
+ 'split.cross_copy': 'Different storages — copy queued instead',
356
+ 'split.cross_failed': 'Cross-storage copy is not supported by this server',
357
+ /* === /wiring:d1 === */
358
+ /* wiring:d2 — gallery view */
359
+ 'toolbar.view.gallery': 'Gallery',
360
+ 'gallery.aria': 'File gallery',
361
+ /* /wiring:d2 */
362
+ /* wiring:d3 — inspector node comments */
363
+ 'inspector.section.comments': 'Comments',
364
+ 'inspector.comments.empty': 'No comments yet.',
365
+ 'inspector.comments.placeholder': 'Write a comment…',
366
+ 'inspector.comments.send': 'Send',
367
+ 'inspector.comments.delete': 'Delete comment',
243
368
  };