@brftech/filex-core 0.40.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +32 -25
  2. package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
  3. package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
  4. package/dist/CsvViewer-CyuwENI_.js +141 -0
  5. package/dist/CsvViewer-CyuwENI_.js.map +1 -0
  6. package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-BIYsTX97.js} +48 -42
  7. package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
  8. package/dist/EpubViewer-K9PjCMmS.js +163 -0
  9. package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
  10. package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
  11. package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
  12. package/dist/MermaidViewer-Bp73aId8.js +137 -0
  13. package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
  14. package/dist/PsdViewer-C2eLivw1.js +122 -0
  15. package/dist/PsdViewer-C2eLivw1.js.map +1 -0
  16. package/dist/TiffViewer-DsHg5gcz.js +142 -0
  17. package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
  18. package/dist/Viewer3D-C_dc2HN9.js +75 -0
  19. package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
  20. package/dist/filex-core.js +218 -20361
  21. package/dist/filex-core.js.map +1 -1
  22. package/dist/filex-core.umd.cjs +61 -62
  23. package/dist/filex-core.umd.cjs.map +1 -1
  24. package/dist/index-vxMZpYb-.js +26700 -0
  25. package/dist/index-vxMZpYb-.js.map +1 -0
  26. package/dist/index.d.ts +4212 -862
  27. package/dist/style.css +1 -1
  28. package/package.json +1 -1
  29. package/src/FileExplorer.vue +2154 -506
  30. package/src/components/AdvancedSearch.vue +870 -0
  31. package/src/components/Breadcrumb.vue +191 -19
  32. package/src/components/CommandPalette.vue +66 -22
  33. package/src/components/ConnectionsPanel.vue +6 -2
  34. package/src/components/ContextMenu.vue +39 -2
  35. package/src/components/EncryptedFolderModal.vue +31 -25
  36. package/src/components/FilePane.vue +951 -0
  37. package/src/components/FilterBar.vue +568 -72
  38. package/src/components/GalleryView.vue +172 -56
  39. package/src/components/GridView.vue +331 -82
  40. package/src/components/HomeView.vue +331 -0
  41. package/src/components/InspectorPanel.vue +296 -53
  42. package/src/components/ListView.vue +1173 -194
  43. package/src/components/NFSExportsPanel.vue +7 -4
  44. package/src/components/OnboardingTour.vue +156 -33
  45. package/src/components/QuickLook.vue +26 -1
  46. package/src/components/RecentlyOpened.vue +17 -2
  47. package/src/components/S3KeysPanel.vue +7 -4
  48. package/src/components/SSHKeysPanel.vue +7 -4
  49. package/src/components/ShortcutSettings.vue +6 -2
  50. package/src/components/ShortcutsHelp.vue +4 -1
  51. package/src/components/SideNav.vue +206 -140
  52. package/src/components/StorageFields.vue +19 -1
  53. package/src/components/TabBar.vue +87 -1
  54. package/src/components/TagPicker.vue +77 -6
  55. package/src/components/ThemeGallery.vue +13 -198
  56. package/src/components/ThemePalette.vue +244 -0
  57. package/src/components/TimeZoneDialog.vue +135 -0
  58. package/src/components/TimeZonePicker.vue +696 -0
  59. package/src/components/TokensPanel.vue +35 -14
  60. package/src/components/Toolbar.vue +953 -465
  61. package/src/components/ViewSwitcher.vue +9 -4
  62. package/src/composables/useConnections.ts +35 -4
  63. package/src/composables/useExplorerTimeZone.ts +89 -0
  64. package/src/composables/useFileApi.ts +53 -4
  65. package/src/composables/useLocale.ts +382 -26
  66. package/src/composables/useRowTouch.ts +95 -0
  67. package/src/index.ts +177 -3
  68. package/src/lib/actionIcons.ts +330 -0
  69. package/src/lib/advSearch.ts +162 -0
  70. package/src/lib/dateGroups.ts +197 -0
  71. package/src/lib/destinationTree.ts +235 -0
  72. package/src/lib/downloadSelection.ts +154 -0
  73. package/src/lib/fileFilters.ts +319 -12
  74. package/src/lib/fileIcons.ts +299 -5
  75. package/src/lib/filePreview.ts +605 -0
  76. package/src/lib/listing.ts +72 -0
  77. package/src/lib/shareTtl.ts +31 -2
  78. package/src/lib/sortOrder.ts +459 -0
  79. package/src/lib/themes.ts +151 -116
  80. package/src/lib/timezone.ts +501 -0
  81. package/src/lib/uiProfile.ts +82 -0
  82. package/src/lib/viewPrefs.ts +1036 -0
  83. package/src/locales/en.ts +416 -26
  84. package/src/locales/tr.ts +409 -26
  85. package/src/modals/ConvertModal.vue +23 -3
  86. package/src/modals/DestinationPickerModal.vue +341 -0
  87. package/src/modals/Modal.vue +18 -4
  88. package/src/modals/NewDocumentModal.vue +525 -0
  89. package/src/modals/NewFolderModal.vue +15 -9
  90. package/src/modals/PermissionsModal.vue +557 -476
  91. package/src/modals/PreviewModal.vue +516 -156
  92. package/src/styles/base.css +5409 -8
  93. package/src/styles/variables.css +139 -65
  94. package/src/types/Connections.ts +9 -0
  95. package/src/types/ExplorerConfig.ts +144 -39
  96. package/src/types/FileNode.ts +28 -0
  97. package/src/viewers/ArchiveViewer.vue +46 -8
  98. package/src/viewers/CsvViewer.vue +19 -2
  99. package/src/viewers/DrawioViewer.vue +12 -1
  100. package/src/viewers/EpubViewer.vue +50 -6
  101. package/src/viewers/IpynbViewer.vue +19 -2
  102. package/src/viewers/MermaidViewer.vue +20 -3
  103. package/src/viewers/PdfViewer.vue +24 -4
  104. package/src/viewers/PsdViewer.vue +28 -4
  105. package/src/viewers/TiffViewer.vue +19 -2
  106. package/src/viewers/Viewer3D.vue +36 -2
  107. package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
  108. package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
  109. package/dist/CsvViewer-CqWeV8VO.js +0 -131
  110. package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
  111. package/dist/DrawioViewer-BNALOB04.js.map +0 -1
  112. package/dist/EpubViewer-BPmlImG7.js +0 -145
  113. package/dist/EpubViewer-BPmlImG7.js.map +0 -1
  114. package/dist/IpynbViewer-CDlQmuCV.js +0 -175
  115. package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
  116. package/dist/MermaidViewer-sfZWdR-d.js +0 -127
  117. package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
  118. package/dist/PsdViewer-DwuySZsb.js +0 -112
  119. package/dist/PsdViewer-DwuySZsb.js.map +0 -1
  120. package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
  121. package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
  122. package/dist/Viewer3D-CJJJgkuv.js +0 -60
  123. package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
  124. package/src/components/SecondaryPane.vue +0 -447
  125. package/src/modals/ShareModal.vue +0 -139
package/src/lib/themes.ts CHANGED
@@ -35,7 +35,7 @@
35
35
  * untouched — its token maps below exist only to render the gallery
36
36
  * preview card.
37
37
  *
38
- * Persistence: localStorage `filex.theme` (absent/invalid → default),
38
+ * Persistence: localStorage `filex.palette` (absent/invalid → default),
39
39
  * shared reactively across every explorer instance on the page and
40
40
  * synced across tabs via the `storage` event.
41
41
  *
@@ -60,13 +60,78 @@ export interface ThemeDef {
60
60
  dark: ThemeTokenMap;
61
61
  }
62
62
 
63
- export const THEME_LS_KEY = 'filex.theme';
63
+ /**
64
+ * ⚠⚠ `filex.palette`, NOT `filex.theme` — and the rename is a BUG FIX, not
65
+ * tidying.
66
+ *
67
+ * `web/src/lib/theme.ts` has always stored the admin app's light/dark/auto
68
+ * MODE under `filex.theme`, and this module stored the PALETTE id under the
69
+ * same name. Two writers, one key, neither aware of the other — and because
70
+ * both readers fall back silently on a value they do not recognise, nothing
71
+ * ever threw: picking "Night Blue" in the explorer reset the panel's
72
+ * light/dark choice to auto, and choosing Dark in the panel reset the palette
73
+ * to default. It went unseen for as long as the two controls lived on
74
+ * opposite sides of the product; putting them both in the settings modal
75
+ * makes it a one-click round trip.
76
+ */
77
+ export const THEME_LS_KEY = 'filex.palette';
78
+
79
+ /** The key this preference used to share with the app's mode. Read once, to
80
+ * carry a palette across the rename; never written. */
81
+ const LEGACY_THEME_LS_KEY = 'filex.theme';
82
+
64
83
  export const DEFAULT_THEME_ID = 'default';
65
84
 
66
85
  /* ------------------------------------------------------------------ */
67
86
  /* Registry */
68
87
  /* ------------------------------------------------------------------ */
69
88
 
89
+ /**
90
+ * ⚠⚠ Every theme declares the SAME key set, and there is a test for it
91
+ * (`web/tests/api/themeTokenParity.test.ts`). That test exists because of a
92
+ * bug whose whole nature is that nothing notices:
93
+ *
94
+ * `--fe-primary-soft` was declared by variables.css and used by nine rules
95
+ * in base.css — the active tab of a segmented control, a filter chip that is
96
+ * set, an active toolbar button, the advanced-search tabs — and by NO theme
97
+ * map at all. So somebody on Forest, Amber or Lilac got their palette
98
+ * everywhere except the one token that marks "this is the selected thing",
99
+ * which stayed stock blue and clashed with the palette they had just picked.
100
+ * `--fe-border-soft` (ten rules, the quieter separators inside panels) was
101
+ * missing the same way. Neither errored, neither looked broken in the
102
+ * gallery card, and the only way to see it was to pick a theme and look.
103
+ *
104
+ * How the two are derived, so a new theme fills them the same way:
105
+ *
106
+ * --fe-primary-soft = the theme's OWN primary mixed into its OWN background,
107
+ * at the fraction that reproduces the STOCK tint's separation from its
108
+ * background (1.184:1 light, 1.207:1 dark). Separation is the invariant,
109
+ * not the mix fraction: the palettes' primaries run from a near-black
110
+ * green to a pale grey, so a fixed 13% lands anywhere between invisible
111
+ * and a slab. Held that way, every palette's tint reads with the same
112
+ * weight as the stock one. Measured: text on it is 9.44:1 (Terminal dark)
113
+ * to 17.85:1 (High Contrast light), so the 4.5 bar is never in question.
114
+ * --fe-border-soft = the theme's own border mixed toward its own background
115
+ * at the stock fraction (0.60 light / 0.68 dark), which reproduces the
116
+ * stock value exactly. ⚠ High Contrast is the deliberate exception: it
117
+ * takes its own `--fe-border` unchanged, because a *soft* separator is the
118
+ * one thing that theme exists to not have.
119
+ *
120
+ * ⚠ What is deliberately NOT themed, and why:
121
+ * `--fe-ok` / `--fe-warning` / `--fe-keep-ok` — status signals, not taste.
122
+ * They are identical in light and dark on purpose; "ok" that is green in
123
+ * one palette and amber in another stops being a signal.
124
+ * `--fe-icon-*` — file-type accents. A PDF is red and a folder is yellow in
125
+ * every palette, and they are never the only cue (the row carries its name
126
+ * in `--fe-text` too).
127
+ * radii, control heights, the type scale, gaps, `--fe-sidenav-w`,
128
+ * `--fe-font-mono` — metrics, not a palette.
129
+ * `--fe-shadow` / `--fe-shadow-sm` / `--fe-font` — depth and face rather
130
+ * than colour, and the two themes that DO override them (High Contrast's
131
+ * hard 1px outlines, Terminal's monospace face) are extras on top of the
132
+ * shared set, not holes in it. The parity test allows extras and requires
133
+ * the shared set.
134
+ */
70
135
  export const THEMES: ThemeDef[] = [
71
136
  {
72
137
  // Stock palette — the maps duplicate variables.css ONLY for the
@@ -74,38 +139,44 @@ export const THEMES: ThemeDef[] = [
74
139
  id: DEFAULT_THEME_ID,
75
140
  nameKey: 'theme.name.default',
76
141
  light: {
142
+ /* ⚠ Mirrors styles/variables.css exactly — web/tests/api/themeContrast.test.ts
143
+ * fails on any drift. It has drifted before: the stock primary was
144
+ * darkened for contrast and this preview map kept the old blue, so the
145
+ * gallery card advertised a colour the product no longer used. */
77
146
  '--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',
147
+ '--fe-bg-elev': '#f7f8fb',
148
+ '--fe-bg-hover': '#f3f4f6',
149
+ '--fe-bg-selected': '#eef3ff',
150
+ '--fe-border': '#e5e7eb',
151
+ '--fe-border-soft': '#eef0f4',
152
+ '--fe-border-strong': '#d1d5db',
153
+ '--fe-text': '#1f2937',
154
+ '--fe-text-muted': '#6b7280',
85
155
  '--fe-text-on-primary': '#ffffff',
86
- /* ⚠ Mirrors styles/variables.css. It drifted once: the stock primary was
87
- * darkened to #2f6fe0 for WCAG AA against white text and this preview
88
- * map kept the old #3b82f6, so the gallery card advertised a blue the
89
- * product no longer used. */
90
- '--fe-primary': '#2f6fe0',
91
- '--fe-primary-hover': '#2563eb',
156
+ '--fe-primary': '#2f6ceb',
157
+ '--fe-primary-hover': '#2559c9',
158
+ '--fe-primary-soft': '#e6ecfa',
159
+ '--fe-primary-ink': '#2559c9',
92
160
  '--fe-danger': '#dc2626',
93
161
  '--fe-danger-hover': '#b91c1c',
94
162
  },
95
163
  dark: {
96
- '--fe-bg': '#0f1419',
97
- '--fe-bg-elev': '#161c25',
98
- '--fe-bg-hover': '#1f2733',
99
- '--fe-bg-selected': '#23324a',
100
- '--fe-border': '#2a323e',
101
- '--fe-border-strong': '#3a4453',
102
- '--fe-text': '#e5e9f0',
103
- '--fe-text-muted': '#8b95a7',
104
- /* Dark ink, not white: white on #60a5fa measures 2.54 against the 4.5
164
+ '--fe-bg': '#15171c',
165
+ '--fe-bg-elev': '#1a1d23',
166
+ '--fe-bg-hover': '#1f232a',
167
+ '--fe-bg-selected': '#1c2740',
168
+ '--fe-border': '#2e333c',
169
+ '--fe-border-soft': '#262a32',
170
+ '--fe-border-strong': '#3d444f',
171
+ '--fe-text': '#e6e8ec',
172
+ '--fe-text-muted': '#888f9b',
173
+ /* Dark ink, not white: white on the button measures 3.16 against the 4.5
105
174
  * this file claims. Mirrors styles/variables.css. */
106
- '--fe-text-on-primary': '#0f1419',
107
- '--fe-primary': '#60a5fa',
108
- '--fe-primary-hover': '#3b82f6',
175
+ '--fe-text-on-primary': '#15171c',
176
+ '--fe-primary': '#5b8cff',
177
+ '--fe-primary-hover': '#7ba3ff',
178
+ '--fe-primary-soft': '#1c2740',
179
+ '--fe-primary-ink': '#7ba3ff',
109
180
  '--fe-danger': '#f87171',
110
181
  '--fe-danger-hover': '#ef4444',
111
182
  },
@@ -120,12 +191,15 @@ export const THEMES: ThemeDef[] = [
120
191
  '--fe-bg-hover': '#e2e9f7',
121
192
  '--fe-bg-selected': '#d2ddf4',
122
193
  '--fe-border': '#d6deee',
194
+ '--fe-border-soft': '#e3e8f4',
123
195
  '--fe-border-strong': '#aebfda',
124
196
  '--fe-text': '#151f38',
125
197
  '--fe-text-muted': '#49587a',
126
198
  '--fe-text-on-primary': '#ffffff',
127
199
  '--fe-primary': '#2c4a9e',
128
200
  '--fe-primary-hover': '#213a80',
201
+ '--fe-primary-soft': '#e0e6f4',
202
+ '--fe-primary-ink': '#213a80',
129
203
  '--fe-danger': '#c62828',
130
204
  '--fe-danger-hover': '#a51f1f',
131
205
  },
@@ -135,12 +209,15 @@ export const THEMES: ThemeDef[] = [
135
209
  '--fe-bg-hover': '#1a2547',
136
210
  '--fe-bg-selected': '#243665',
137
211
  '--fe-border': '#26335a',
212
+ '--fe-border-soft': '#1d2748',
138
213
  '--fe-border-strong': '#3c4e84',
139
214
  '--fe-text': '#dfe6f7',
140
215
  '--fe-text-muted': '#96a5cc',
141
216
  '--fe-text-on-primary': '#050b26',
142
217
  '--fe-primary': '#6d8dfc',
143
218
  '--fe-primary-hover': '#5273f2',
219
+ '--fe-primary-soft': '#182243',
220
+ '--fe-primary-ink': '#6d8dfc',
144
221
  '--fe-danger': '#f87171',
145
222
  '--fe-danger-hover': '#ef4444',
146
223
  },
@@ -155,12 +232,15 @@ export const THEMES: ThemeDef[] = [
155
232
  '--fe-bg-hover': '#dcecdf',
156
233
  '--fe-bg-selected': '#c8e2ce',
157
234
  '--fe-border': '#d2e2d4',
235
+ '--fe-border-soft': '#e0ece1',
158
236
  '--fe-border-strong': '#a2c3a8',
159
237
  '--fe-text': '#182b1d',
160
238
  '--fe-text-muted': '#42604a',
161
239
  '--fe-text-on-primary': '#ffffff',
162
240
  '--fe-primary': '#2b7a41',
163
241
  '--fe-primary-hover': '#226334',
242
+ '--fe-primary-soft': '#dbe9dc',
243
+ '--fe-primary-ink': '#226334',
164
244
  '--fe-danger': '#c62828',
165
245
  '--fe-danger-hover': '#a51f1f',
166
246
  },
@@ -170,12 +250,15 @@ export const THEMES: ThemeDef[] = [
170
250
  '--fe-bg-hover': '#1b2d23',
171
251
  '--fe-bg-selected': '#264532',
172
252
  '--fe-border': '#28402f',
253
+ '--fe-border-soft': '#1f3225',
173
254
  '--fe-border-strong': '#3e5f49',
174
255
  '--fe-text': '#dcebe0',
175
256
  '--fe-text-muted': '#93b09b',
176
257
  '--fe-text-on-primary': '#04180b',
177
258
  '--fe-primary': '#54c17a',
178
259
  '--fe-primary-hover': '#3fae66',
260
+ '--fe-primary-soft': '#16291d',
261
+ '--fe-primary-ink': '#54c17a',
179
262
  '--fe-danger': '#f87171',
180
263
  '--fe-danger-hover': '#ef4444',
181
264
  },
@@ -190,12 +273,15 @@ export const THEMES: ThemeDef[] = [
190
273
  '--fe-bg-hover': '#f0e3c8',
191
274
  '--fe-bg-selected': '#ead6ab',
192
275
  '--fe-border': '#e6dabf',
276
+ '--fe-border-soft': '#efe6d3',
193
277
  '--fe-border-strong': '#c6b184',
194
278
  '--fe-text': '#33270f',
195
279
  '--fe-text-muted': '#655631',
196
280
  '--fe-text-on-primary': '#ffffff',
197
281
  '--fe-primary': '#9a4b00',
198
282
  '--fe-primary-hover': '#7c3c00',
283
+ '--fe-primary-soft': '#f3e5d7',
284
+ '--fe-primary-ink': '#7c3c00',
199
285
  '--fe-danger': '#c62828',
200
286
  '--fe-danger-hover': '#a51f1f',
201
287
  },
@@ -205,12 +291,15 @@ export const THEMES: ThemeDef[] = [
205
291
  '--fe-bg-hover': '#2f2312',
206
292
  '--fe-bg-selected': '#44331b',
207
293
  '--fe-border': '#3b2e17',
294
+ '--fe-border-soft': '#302512',
208
295
  '--fe-border-strong': '#5b4a29',
209
296
  '--fe-text': '#f2e7d4',
210
297
  '--fe-text-muted': '#bda887',
211
298
  '--fe-text-on-primary': '#2a1c02',
212
299
  '--fe-primary': '#f5a524',
213
300
  '--fe-primary-hover': '#ffb84d',
301
+ '--fe-primary-soft': '#31230c',
302
+ '--fe-primary-ink': '#ffb84d',
214
303
  '--fe-danger': '#f87171',
215
304
  '--fe-danger-hover': '#ef4444',
216
305
  },
@@ -225,12 +314,15 @@ export const THEMES: ThemeDef[] = [
225
314
  '--fe-bg-hover': '#e8ddf5',
226
315
  '--fe-bg-selected': '#dccaf0',
227
316
  '--fe-border': '#e1d6ee',
317
+ '--fe-border-soft': '#ebe3f4',
228
318
  '--fe-border-strong': '#bda6d9',
229
319
  '--fe-text': '#241a33',
230
320
  '--fe-text-muted': '#584871',
231
321
  '--fe-text-on-primary': '#ffffff',
232
322
  '--fe-primary': '#7231e0',
233
323
  '--fe-primary-hover': '#5f21c4',
324
+ '--fe-primary-soft': '#e9e3fc',
325
+ '--fe-primary-ink': '#5f21c4',
234
326
  '--fe-danger': '#c62828',
235
327
  '--fe-danger-hover': '#a51f1f',
236
328
  },
@@ -240,12 +332,15 @@ export const THEMES: ThemeDef[] = [
240
332
  '--fe-bg-hover': '#261d37',
241
333
  '--fe-bg-selected': '#37294f',
242
334
  '--fe-border': '#302546',
335
+ '--fe-border-soft': '#261e38',
243
336
  '--fe-border-strong': '#4b3a6d',
244
337
  '--fe-text': '#e9e2f5',
245
338
  '--fe-text-muted': '#a999c5',
246
339
  '--fe-text-on-primary': '#180d33',
247
340
  '--fe-primary': '#b197fa',
248
341
  '--fe-primary-hover': '#9c7cf4',
342
+ '--fe-primary-soft': '#272038',
343
+ '--fe-primary-ink': '#b197fa',
249
344
  '--fe-danger': '#f87171',
250
345
  '--fe-danger-hover': '#ef4444',
251
346
  },
@@ -260,12 +355,15 @@ export const THEMES: ThemeDef[] = [
260
355
  '--fe-bg-hover': '#e0e0e0',
261
356
  '--fe-bg-selected': '#c9dcff',
262
357
  '--fe-border': '#5c5c5c',
358
+ '--fe-border-soft': '#5c5c5c',
263
359
  '--fe-border-strong': '#000000',
264
360
  '--fe-text': '#000000',
265
361
  '--fe-text-muted': '#3d3d3d',
266
362
  '--fe-text-on-primary': '#ffffff',
267
363
  '--fe-primary': '#003d99',
268
364
  '--fe-primary-hover': '#002a6b',
365
+ '--fe-primary-soft': '#e7edf7',
366
+ '--fe-primary-ink': '#002a6b',
269
367
  '--fe-danger': '#a80000',
270
368
  '--fe-danger-hover': '#7d0000',
271
369
  '--fe-shadow': '0 0 0 1px #000000, 0 10px 32px rgba(0, 0, 0, 0.25)',
@@ -277,12 +375,15 @@ export const THEMES: ThemeDef[] = [
277
375
  '--fe-bg-hover': '#212121',
278
376
  '--fe-bg-selected': '#003d80',
279
377
  '--fe-border': '#8f8f8f',
378
+ '--fe-border-soft': '#8f8f8f',
280
379
  '--fe-border-strong': '#ffffff',
281
380
  '--fe-text': '#ffffff',
282
381
  '--fe-text-muted': '#d6d6d6',
283
382
  '--fe-text-on-primary': '#001430',
284
383
  '--fe-primary': '#7ab8ff',
285
384
  '--fe-primary-hover': '#9ccaff',
385
+ '--fe-primary-soft': '#0e1a29',
386
+ '--fe-primary-ink': '#9ccaff',
286
387
  '--fe-danger': '#ff7575',
287
388
  '--fe-danger-hover': '#ff9999',
288
389
  '--fe-shadow': '0 0 0 1px #ffffff, 0 12px 32px rgba(0, 0, 0, 0.65)',
@@ -299,12 +400,15 @@ export const THEMES: ThemeDef[] = [
299
400
  '--fe-bg-hover': '#e4e4e7',
300
401
  '--fe-bg-selected': '#d6d7db',
301
402
  '--fe-border': '#dfdfe2',
403
+ '--fe-border-soft': '#e9e9eb',
302
404
  '--fe-border-strong': '#b9b9c0',
303
405
  '--fe-text': '#26272b',
304
406
  '--fe-text-muted': '#585a63',
305
407
  '--fe-text-on-primary': '#ffffff',
306
408
  '--fe-primary': '#4b4b54',
307
409
  '--fe-primary-hover': '#38383f',
410
+ '--fe-primary-soft': '#e4e4e6',
411
+ '--fe-primary-ink': '#38383f',
308
412
  '--fe-danger': '#c62828',
309
413
  '--fe-danger-hover': '#a51f1f',
310
414
  },
@@ -314,12 +418,15 @@ export const THEMES: ThemeDef[] = [
314
418
  '--fe-bg-hover': '#26262b',
315
419
  '--fe-bg-selected': '#35353c',
316
420
  '--fe-border': '#2d2d33',
421
+ '--fe-border-soft': '#242429',
317
422
  '--fe-border-strong': '#4a4a52',
318
423
  '--fe-text': '#e6e6e9',
319
424
  '--fe-text-muted': '#a4a4ad',
320
425
  '--fe-text-on-primary': '#17171b',
321
426
  '--fe-primary': '#b0b0ba',
322
427
  '--fe-primary-hover': '#c4c4cd',
428
+ '--fe-primary-soft': '#242428',
429
+ '--fe-primary-ink': '#c4c4cd',
323
430
  '--fe-danger': '#f87171',
324
431
  '--fe-danger-hover': '#ef4444',
325
432
  },
@@ -334,12 +441,15 @@ export const THEMES: ThemeDef[] = [
334
441
  '--fe-bg-hover': '#d5ead9',
335
442
  '--fe-bg-selected': '#bce3c6',
336
443
  '--fe-border': '#cbe2d1',
444
+ '--fe-border-soft': '#dbebdf',
337
445
  '--fe-border-strong': '#94c2a0',
338
446
  '--fe-text': '#0c2913',
339
447
  '--fe-text-muted': '#31573c',
340
448
  '--fe-text-on-primary': '#ffffff',
341
449
  '--fe-primary': '#116b33',
342
450
  '--fe-primary-hover': '#0c5427',
451
+ '--fe-primary-soft': '#dbe9dd',
452
+ '--fe-primary-ink': '#0c5427',
343
453
  '--fe-danger': '#c62828',
344
454
  '--fe-danger-hover': '#a51f1f',
345
455
  '--fe-font': 'ui-monospace, "SF Mono", Consolas, Menlo, monospace',
@@ -350,105 +460,20 @@ export const THEMES: ThemeDef[] = [
350
460
  '--fe-bg-hover': '#112418',
351
461
  '--fe-bg-selected': '#1a3a26',
352
462
  '--fe-border': '#1c3a2a',
463
+ '--fe-border-soft': '#142b1e',
353
464
  '--fe-border-strong': '#316144',
354
465
  '--fe-text': '#4fdd8b',
355
466
  '--fe-text-muted': '#38a668',
356
467
  '--fe-text-on-primary': '#03180a',
357
468
  '--fe-primary': '#25c95e',
358
469
  '--fe-primary-hover': '#4fdd8b',
470
+ '--fe-primary-soft': '#0c2412',
471
+ '--fe-primary-ink': '#4fdd8b',
359
472
  '--fe-danger': '#ff6b62',
360
473
  '--fe-danger-hover': '#ff8d86',
361
474
  '--fe-font': 'ui-monospace, "SF Mono", Consolas, Menlo, monospace',
362
475
  },
363
476
  },
364
- {
365
- /**
366
- * Drive — the palette of the end-user shell alfatm built on top of filex
367
- * and put on a demo stand for review (GitHub #14). The drive profile's
368
- * layout came out of their mockups; this is the colour and shape half of
369
- * the same look, mapped onto our own tokens so every surface the explorer
370
- * has — not just the drive shell — can wear it.
371
- *
372
- * Measured from their demo rather than eyeballed: 43 custom properties,
373
- * of which the ones with a home here are the palette, the radii (6/8/10)
374
- * and the per-file-type accents. Inter leads the font stack because that
375
- * is what the demo uses; nothing is downloaded for it, so a machine
376
- * without Inter installed falls back to the same system face as every
377
- * other theme.
378
- */
379
- id: 'drive',
380
- nameKey: 'theme.name.drive',
381
- light: {
382
- '--fe-bg': '#ffffff',
383
- '--fe-bg-elev': '#f7f8fb',
384
- '--fe-bg-hover': '#f3f4f6',
385
- '--fe-bg-selected': '#eef3ff',
386
- '--fe-border': '#e5e7eb',
387
- '--fe-border-strong': '#d1d5db',
388
- '--fe-text': '#1f2937',
389
- '--fe-text-muted': '#6b7280',
390
- '--fe-text-on-primary': '#ffffff',
391
- '--fe-primary': '#2f6ceb',
392
- '--fe-primary-hover': '#2559c9',
393
- '--fe-danger': '#dc2626',
394
- '--fe-danger-hover': '#b91c1c',
395
- '--fe-font': 'Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
396
- '--fe-radius-sm': '6px',
397
- '--fe-radius': '8px',
398
- '--fe-radius-md': '10px',
399
- '--fe-radius-lg': '12px',
400
- '--fe-shadow': '0 20px 60px rgba(17, 24, 39, 0.18)',
401
- '--fe-shadow-sm': '0 8px 24px rgba(17, 24, 39, 0.12)',
402
- '--fe-icon-folder': '#f4b400',
403
- '--fe-icon-image': '#2f6ceb',
404
- '--fe-icon-video': '#7c3aed',
405
- '--fe-icon-audio': '#0d9488',
406
- '--fe-icon-pdf': '#dc2626',
407
- '--fe-icon-doc': '#2f6ceb',
408
- '--fe-icon-sheet': '#16a34a',
409
- '--fe-icon-slides': '#ea580c',
410
- '--fe-icon-archive': '#6b7280',
411
- '--fe-icon-code': '#2f6ceb',
412
- '--fe-icon-text': '#374151',
413
- '--fe-icon-unknown': '#6b7280',
414
- },
415
- dark: {
416
- '--fe-bg': '#15171c',
417
- '--fe-bg-elev': '#1a1d23',
418
- '--fe-bg-hover': '#1f232a',
419
- '--fe-bg-selected': '#1c2740',
420
- '--fe-border': '#2e333c',
421
- '--fe-border-strong': '#3d444f',
422
- '--fe-text': '#e6e8ec',
423
- '--fe-text-muted': '#888f9b',
424
- /* Dark ink on a light-blue button: white on #5b8cff measures 3.16, and
425
- * the bar this file documents is 4.5. */
426
- '--fe-text-on-primary': '#15171c',
427
- '--fe-primary': '#5b8cff',
428
- '--fe-primary-hover': '#7ba3ff',
429
- '--fe-danger': '#f87171',
430
- '--fe-danger-hover': '#ef4444',
431
- '--fe-font': 'Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
432
- '--fe-radius-sm': '6px',
433
- '--fe-radius': '8px',
434
- '--fe-radius-md': '10px',
435
- '--fe-radius-lg': '12px',
436
- '--fe-shadow': '0 20px 60px rgba(0, 0, 0, 0.6)',
437
- '--fe-shadow-sm': '0 8px 24px rgba(0, 0, 0, 0.5)',
438
- '--fe-icon-folder': '#f4b400',
439
- '--fe-icon-image': '#4d7ff0',
440
- '--fe-icon-video': '#9061f0',
441
- '--fe-icon-audio': '#2dd4bf',
442
- '--fe-icon-pdf': '#e05252',
443
- '--fe-icon-doc': '#4d7ff0',
444
- '--fe-icon-sheet': '#2eab63',
445
- '--fe-icon-slides': '#fb923c',
446
- '--fe-icon-archive': '#79808d',
447
- '--fe-icon-code': '#4d7ff0',
448
- '--fe-icon-text': '#5b6472',
449
- '--fe-icon-unknown': '#79808d',
450
- },
451
- },
452
477
  ];
453
478
 
454
479
  export function themeById(id: string | null | undefined): ThemeDef | undefined {
@@ -469,7 +494,17 @@ const ALL_TOKEN_KEYS: string[] = Array.from(
469
494
  function readStoredThemeId(): string {
470
495
  try {
471
496
  const v = localStorage.getItem(THEME_LS_KEY);
472
- return v && themeById(v) ? v : DEFAULT_THEME_ID;
497
+ if (v && themeById(v)) return v;
498
+ // One-time carry-over from the shared key. `themeById` is what makes this
499
+ // safe: 'light' / 'dark' / 'auto' are not palette ids, so a value the app's
500
+ // mode wrote can never be mistaken for a palette here.
501
+ const legacy = localStorage.getItem(LEGACY_THEME_LS_KEY);
502
+ if (legacy && themeById(legacy)) {
503
+ localStorage.setItem(THEME_LS_KEY, legacy);
504
+ localStorage.removeItem(LEGACY_THEME_LS_KEY);
505
+ return legacy;
506
+ }
507
+ return DEFAULT_THEME_ID;
473
508
  } catch {
474
509
  return DEFAULT_THEME_ID;
475
510
  }