@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
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,32 +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
- '--fe-primary': '#3b82f6',
87
- '--fe-primary-hover': '#2563eb',
156
+ '--fe-primary': '#2f6ceb',
157
+ '--fe-primary-hover': '#2559c9',
158
+ '--fe-primary-soft': '#e6ecfa',
159
+ '--fe-primary-ink': '#2559c9',
88
160
  '--fe-danger': '#dc2626',
89
161
  '--fe-danger-hover': '#b91c1c',
90
162
  },
91
163
  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',
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
174
+ * this file claims. Mirrors styles/variables.css. */
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',
103
180
  '--fe-danger': '#f87171',
104
181
  '--fe-danger-hover': '#ef4444',
105
182
  },
@@ -114,12 +191,15 @@ export const THEMES: ThemeDef[] = [
114
191
  '--fe-bg-hover': '#e2e9f7',
115
192
  '--fe-bg-selected': '#d2ddf4',
116
193
  '--fe-border': '#d6deee',
194
+ '--fe-border-soft': '#e3e8f4',
117
195
  '--fe-border-strong': '#aebfda',
118
196
  '--fe-text': '#151f38',
119
197
  '--fe-text-muted': '#49587a',
120
198
  '--fe-text-on-primary': '#ffffff',
121
199
  '--fe-primary': '#2c4a9e',
122
200
  '--fe-primary-hover': '#213a80',
201
+ '--fe-primary-soft': '#e0e6f4',
202
+ '--fe-primary-ink': '#213a80',
123
203
  '--fe-danger': '#c62828',
124
204
  '--fe-danger-hover': '#a51f1f',
125
205
  },
@@ -129,12 +209,15 @@ export const THEMES: ThemeDef[] = [
129
209
  '--fe-bg-hover': '#1a2547',
130
210
  '--fe-bg-selected': '#243665',
131
211
  '--fe-border': '#26335a',
212
+ '--fe-border-soft': '#1d2748',
132
213
  '--fe-border-strong': '#3c4e84',
133
214
  '--fe-text': '#dfe6f7',
134
215
  '--fe-text-muted': '#96a5cc',
135
216
  '--fe-text-on-primary': '#050b26',
136
217
  '--fe-primary': '#6d8dfc',
137
218
  '--fe-primary-hover': '#5273f2',
219
+ '--fe-primary-soft': '#182243',
220
+ '--fe-primary-ink': '#6d8dfc',
138
221
  '--fe-danger': '#f87171',
139
222
  '--fe-danger-hover': '#ef4444',
140
223
  },
@@ -149,12 +232,15 @@ export const THEMES: ThemeDef[] = [
149
232
  '--fe-bg-hover': '#dcecdf',
150
233
  '--fe-bg-selected': '#c8e2ce',
151
234
  '--fe-border': '#d2e2d4',
235
+ '--fe-border-soft': '#e0ece1',
152
236
  '--fe-border-strong': '#a2c3a8',
153
237
  '--fe-text': '#182b1d',
154
238
  '--fe-text-muted': '#42604a',
155
239
  '--fe-text-on-primary': '#ffffff',
156
240
  '--fe-primary': '#2b7a41',
157
241
  '--fe-primary-hover': '#226334',
242
+ '--fe-primary-soft': '#dbe9dc',
243
+ '--fe-primary-ink': '#226334',
158
244
  '--fe-danger': '#c62828',
159
245
  '--fe-danger-hover': '#a51f1f',
160
246
  },
@@ -164,12 +250,15 @@ export const THEMES: ThemeDef[] = [
164
250
  '--fe-bg-hover': '#1b2d23',
165
251
  '--fe-bg-selected': '#264532',
166
252
  '--fe-border': '#28402f',
253
+ '--fe-border-soft': '#1f3225',
167
254
  '--fe-border-strong': '#3e5f49',
168
255
  '--fe-text': '#dcebe0',
169
256
  '--fe-text-muted': '#93b09b',
170
257
  '--fe-text-on-primary': '#04180b',
171
258
  '--fe-primary': '#54c17a',
172
259
  '--fe-primary-hover': '#3fae66',
260
+ '--fe-primary-soft': '#16291d',
261
+ '--fe-primary-ink': '#54c17a',
173
262
  '--fe-danger': '#f87171',
174
263
  '--fe-danger-hover': '#ef4444',
175
264
  },
@@ -184,12 +273,15 @@ export const THEMES: ThemeDef[] = [
184
273
  '--fe-bg-hover': '#f0e3c8',
185
274
  '--fe-bg-selected': '#ead6ab',
186
275
  '--fe-border': '#e6dabf',
276
+ '--fe-border-soft': '#efe6d3',
187
277
  '--fe-border-strong': '#c6b184',
188
278
  '--fe-text': '#33270f',
189
279
  '--fe-text-muted': '#655631',
190
280
  '--fe-text-on-primary': '#ffffff',
191
281
  '--fe-primary': '#9a4b00',
192
282
  '--fe-primary-hover': '#7c3c00',
283
+ '--fe-primary-soft': '#f3e5d7',
284
+ '--fe-primary-ink': '#7c3c00',
193
285
  '--fe-danger': '#c62828',
194
286
  '--fe-danger-hover': '#a51f1f',
195
287
  },
@@ -199,12 +291,15 @@ export const THEMES: ThemeDef[] = [
199
291
  '--fe-bg-hover': '#2f2312',
200
292
  '--fe-bg-selected': '#44331b',
201
293
  '--fe-border': '#3b2e17',
294
+ '--fe-border-soft': '#302512',
202
295
  '--fe-border-strong': '#5b4a29',
203
296
  '--fe-text': '#f2e7d4',
204
297
  '--fe-text-muted': '#bda887',
205
298
  '--fe-text-on-primary': '#2a1c02',
206
299
  '--fe-primary': '#f5a524',
207
300
  '--fe-primary-hover': '#ffb84d',
301
+ '--fe-primary-soft': '#31230c',
302
+ '--fe-primary-ink': '#ffb84d',
208
303
  '--fe-danger': '#f87171',
209
304
  '--fe-danger-hover': '#ef4444',
210
305
  },
@@ -219,12 +314,15 @@ export const THEMES: ThemeDef[] = [
219
314
  '--fe-bg-hover': '#e8ddf5',
220
315
  '--fe-bg-selected': '#dccaf0',
221
316
  '--fe-border': '#e1d6ee',
317
+ '--fe-border-soft': '#ebe3f4',
222
318
  '--fe-border-strong': '#bda6d9',
223
319
  '--fe-text': '#241a33',
224
320
  '--fe-text-muted': '#584871',
225
321
  '--fe-text-on-primary': '#ffffff',
226
322
  '--fe-primary': '#7231e0',
227
323
  '--fe-primary-hover': '#5f21c4',
324
+ '--fe-primary-soft': '#e9e3fc',
325
+ '--fe-primary-ink': '#5f21c4',
228
326
  '--fe-danger': '#c62828',
229
327
  '--fe-danger-hover': '#a51f1f',
230
328
  },
@@ -234,12 +332,15 @@ export const THEMES: ThemeDef[] = [
234
332
  '--fe-bg-hover': '#261d37',
235
333
  '--fe-bg-selected': '#37294f',
236
334
  '--fe-border': '#302546',
335
+ '--fe-border-soft': '#261e38',
237
336
  '--fe-border-strong': '#4b3a6d',
238
337
  '--fe-text': '#e9e2f5',
239
338
  '--fe-text-muted': '#a999c5',
240
339
  '--fe-text-on-primary': '#180d33',
241
340
  '--fe-primary': '#b197fa',
242
341
  '--fe-primary-hover': '#9c7cf4',
342
+ '--fe-primary-soft': '#272038',
343
+ '--fe-primary-ink': '#b197fa',
243
344
  '--fe-danger': '#f87171',
244
345
  '--fe-danger-hover': '#ef4444',
245
346
  },
@@ -254,12 +355,15 @@ export const THEMES: ThemeDef[] = [
254
355
  '--fe-bg-hover': '#e0e0e0',
255
356
  '--fe-bg-selected': '#c9dcff',
256
357
  '--fe-border': '#5c5c5c',
358
+ '--fe-border-soft': '#5c5c5c',
257
359
  '--fe-border-strong': '#000000',
258
360
  '--fe-text': '#000000',
259
361
  '--fe-text-muted': '#3d3d3d',
260
362
  '--fe-text-on-primary': '#ffffff',
261
363
  '--fe-primary': '#003d99',
262
364
  '--fe-primary-hover': '#002a6b',
365
+ '--fe-primary-soft': '#e7edf7',
366
+ '--fe-primary-ink': '#002a6b',
263
367
  '--fe-danger': '#a80000',
264
368
  '--fe-danger-hover': '#7d0000',
265
369
  '--fe-shadow': '0 0 0 1px #000000, 0 10px 32px rgba(0, 0, 0, 0.25)',
@@ -271,12 +375,15 @@ export const THEMES: ThemeDef[] = [
271
375
  '--fe-bg-hover': '#212121',
272
376
  '--fe-bg-selected': '#003d80',
273
377
  '--fe-border': '#8f8f8f',
378
+ '--fe-border-soft': '#8f8f8f',
274
379
  '--fe-border-strong': '#ffffff',
275
380
  '--fe-text': '#ffffff',
276
381
  '--fe-text-muted': '#d6d6d6',
277
382
  '--fe-text-on-primary': '#001430',
278
383
  '--fe-primary': '#7ab8ff',
279
384
  '--fe-primary-hover': '#9ccaff',
385
+ '--fe-primary-soft': '#0e1a29',
386
+ '--fe-primary-ink': '#9ccaff',
280
387
  '--fe-danger': '#ff7575',
281
388
  '--fe-danger-hover': '#ff9999',
282
389
  '--fe-shadow': '0 0 0 1px #ffffff, 0 12px 32px rgba(0, 0, 0, 0.65)',
@@ -293,12 +400,15 @@ export const THEMES: ThemeDef[] = [
293
400
  '--fe-bg-hover': '#e4e4e7',
294
401
  '--fe-bg-selected': '#d6d7db',
295
402
  '--fe-border': '#dfdfe2',
403
+ '--fe-border-soft': '#e9e9eb',
296
404
  '--fe-border-strong': '#b9b9c0',
297
405
  '--fe-text': '#26272b',
298
406
  '--fe-text-muted': '#585a63',
299
407
  '--fe-text-on-primary': '#ffffff',
300
408
  '--fe-primary': '#4b4b54',
301
409
  '--fe-primary-hover': '#38383f',
410
+ '--fe-primary-soft': '#e4e4e6',
411
+ '--fe-primary-ink': '#38383f',
302
412
  '--fe-danger': '#c62828',
303
413
  '--fe-danger-hover': '#a51f1f',
304
414
  },
@@ -308,12 +418,15 @@ export const THEMES: ThemeDef[] = [
308
418
  '--fe-bg-hover': '#26262b',
309
419
  '--fe-bg-selected': '#35353c',
310
420
  '--fe-border': '#2d2d33',
421
+ '--fe-border-soft': '#242429',
311
422
  '--fe-border-strong': '#4a4a52',
312
423
  '--fe-text': '#e6e6e9',
313
424
  '--fe-text-muted': '#a4a4ad',
314
425
  '--fe-text-on-primary': '#17171b',
315
426
  '--fe-primary': '#b0b0ba',
316
427
  '--fe-primary-hover': '#c4c4cd',
428
+ '--fe-primary-soft': '#242428',
429
+ '--fe-primary-ink': '#c4c4cd',
317
430
  '--fe-danger': '#f87171',
318
431
  '--fe-danger-hover': '#ef4444',
319
432
  },
@@ -328,12 +441,15 @@ export const THEMES: ThemeDef[] = [
328
441
  '--fe-bg-hover': '#d5ead9',
329
442
  '--fe-bg-selected': '#bce3c6',
330
443
  '--fe-border': '#cbe2d1',
444
+ '--fe-border-soft': '#dbebdf',
331
445
  '--fe-border-strong': '#94c2a0',
332
446
  '--fe-text': '#0c2913',
333
447
  '--fe-text-muted': '#31573c',
334
448
  '--fe-text-on-primary': '#ffffff',
335
449
  '--fe-primary': '#116b33',
336
450
  '--fe-primary-hover': '#0c5427',
451
+ '--fe-primary-soft': '#dbe9dd',
452
+ '--fe-primary-ink': '#0c5427',
337
453
  '--fe-danger': '#c62828',
338
454
  '--fe-danger-hover': '#a51f1f',
339
455
  '--fe-font': 'ui-monospace, "SF Mono", Consolas, Menlo, monospace',
@@ -344,12 +460,15 @@ export const THEMES: ThemeDef[] = [
344
460
  '--fe-bg-hover': '#112418',
345
461
  '--fe-bg-selected': '#1a3a26',
346
462
  '--fe-border': '#1c3a2a',
463
+ '--fe-border-soft': '#142b1e',
347
464
  '--fe-border-strong': '#316144',
348
465
  '--fe-text': '#4fdd8b',
349
466
  '--fe-text-muted': '#38a668',
350
467
  '--fe-text-on-primary': '#03180a',
351
468
  '--fe-primary': '#25c95e',
352
469
  '--fe-primary-hover': '#4fdd8b',
470
+ '--fe-primary-soft': '#0c2412',
471
+ '--fe-primary-ink': '#4fdd8b',
353
472
  '--fe-danger': '#ff6b62',
354
473
  '--fe-danger-hover': '#ff8d86',
355
474
  '--fe-font': 'ui-monospace, "SF Mono", Consolas, Menlo, monospace',
@@ -375,7 +494,17 @@ const ALL_TOKEN_KEYS: string[] = Array.from(
375
494
  function readStoredThemeId(): string {
376
495
  try {
377
496
  const v = localStorage.getItem(THEME_LS_KEY);
378
- 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;
379
508
  } catch {
380
509
  return DEFAULT_THEME_ID;
381
510
  }