@remit/web-client 0.0.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 (298) hide show
  1. package/harness/build.mjs +84 -0
  2. package/harness/index.html +37 -0
  3. package/harness/vite-preset.ts +29 -0
  4. package/index.html +47 -0
  5. package/package.json +147 -0
  6. package/public/config.js +5 -0
  7. package/public/locales/de/common.json +1 -0
  8. package/public/locales/de/errors.json +1 -0
  9. package/public/locales/de/mail.json +13 -0
  10. package/public/locales/de/settings.json +1 -0
  11. package/public/locales/en/common.json +26 -0
  12. package/public/locales/en/errors.json +1 -0
  13. package/public/locales/en/mail.json +69 -0
  14. package/public/locales/en/settings.json +1 -0
  15. package/public/locales/fr/common.json +1 -0
  16. package/public/locales/fr/errors.json +1 -0
  17. package/public/locales/fr/mail.json +13 -0
  18. package/public/locales/fr/settings.json +1 -0
  19. package/public/locales/nl/common.json +1 -0
  20. package/public/locales/nl/errors.json +1 -0
  21. package/public/locales/nl/mail.json +13 -0
  22. package/public/locales/nl/settings.json +1 -0
  23. package/src/auth/AccountMenu.tsx +58 -0
  24. package/src/auth/AccountSession.tsx +19 -0
  25. package/src/auth/BetterAuthShell.tsx +198 -0
  26. package/src/auth/account-menu-mode.test.ts +64 -0
  27. package/src/auth/account-menu-mode.ts +28 -0
  28. package/src/auth/amplify-config.test.ts +158 -0
  29. package/src/auth/amplify-config.ts +60 -0
  30. package/src/auth/auth-error.test.ts +109 -0
  31. package/src/auth/auth-error.ts +107 -0
  32. package/src/auth/auth-interceptor.test.ts +86 -0
  33. package/src/auth/auth-interceptor.ts +23 -0
  34. package/src/auth/auth.css +334 -0
  35. package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
  36. package/src/auth/better-auth-config.ts +85 -0
  37. package/src/auth/better-auth-provider.tsx +20 -0
  38. package/src/auth/cognito/CognitoAccount.tsx +48 -0
  39. package/src/auth/cognito/CognitoShell.tsx +266 -0
  40. package/src/auth/cognito/cognito-token.test.ts +110 -0
  41. package/src/auth/cognito/cognito-token.ts +47 -0
  42. package/src/auth/cognito/cognito.css +21 -0
  43. package/src/auth/cognito-provider.tsx +22 -0
  44. package/src/auth/combined-provider.tsx +42 -0
  45. package/src/auth/composition.test.ts +136 -0
  46. package/src/auth/provider.tsx +63 -0
  47. package/src/auth/sign-out-visibility.test.ts +40 -0
  48. package/src/auth/sign-out-visibility.ts +10 -0
  49. package/src/auth/sign-up-disabled.test.ts +23 -0
  50. package/src/components/compose/AddressField.tsx +237 -0
  51. package/src/components/compose/ComposeBody.tsx +36 -0
  52. package/src/components/compose/ComposeForm.tsx +676 -0
  53. package/src/components/compose/ComposeProvider.tsx +133 -0
  54. package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
  55. package/src/components/compose/FromSelector.tsx +67 -0
  56. package/src/components/compose/FullCompose.tsx +82 -0
  57. package/src/components/compose/InlineCompose.tsx +29 -0
  58. package/src/components/compose/MobileComposeSheet.tsx +152 -0
  59. package/src/components/compose/PlateEditor.tsx +86 -0
  60. package/src/components/compose/PlateToolbar.tsx +110 -0
  61. package/src/components/compose/SubjectField.tsx +19 -0
  62. package/src/components/compose/draft-discard-banner.test.ts +76 -0
  63. package/src/components/compose/index.ts +6 -0
  64. package/src/components/compose/sanitize-quote-html.ts +23 -0
  65. package/src/components/layout/AppShellSkeleton.tsx +53 -0
  66. package/src/components/layout/ComposeFab.tsx +86 -0
  67. package/src/components/layout/Drawer.tsx +105 -0
  68. package/src/components/mail/AutoMovedIndicator.tsx +48 -0
  69. package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
  70. package/src/components/mail/BlockedImagesNotice.tsx +41 -0
  71. package/src/components/mail/BriefPane.tsx +289 -0
  72. package/src/components/mail/ConversationView.tsx +462 -0
  73. package/src/components/mail/DailyBrief.tsx +451 -0
  74. package/src/components/mail/DraftsView.tsx +279 -0
  75. package/src/components/mail/FlaggedList.tsx +170 -0
  76. package/src/components/mail/FlaggedPane.tsx +217 -0
  77. package/src/components/mail/IntelligencePane.test.ts +123 -0
  78. package/src/components/mail/IntelligencePane.tsx +441 -0
  79. package/src/components/mail/MailListHeader.tsx +187 -0
  80. package/src/components/mail/MailNav.tsx +70 -0
  81. package/src/components/mail/MailSidebarAdapter.tsx +351 -0
  82. package/src/components/mail/MailViewChrome.tsx +95 -0
  83. package/src/components/mail/MailboxPane.tsx +1219 -0
  84. package/src/components/mail/MessageActionMenu.tsx +368 -0
  85. package/src/components/mail/MessageBody.tsx +207 -0
  86. package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
  87. package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
  88. package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
  89. package/src/components/mail/MessageCard.tsx +401 -0
  90. package/src/components/mail/MessageDetail.tsx +153 -0
  91. package/src/components/mail/MessageList.tsx +804 -0
  92. package/src/components/mail/MessageListItem.tsx +267 -0
  93. package/src/components/mail/MessageToolbar.render.test.ts +57 -0
  94. package/src/components/mail/MessageToolbar.tsx +166 -0
  95. package/src/components/mail/MobileMessageBar.tsx +142 -0
  96. package/src/components/mail/MoveToTrigger.tsx +250 -0
  97. package/src/components/mail/NavMenuButton.tsx +25 -0
  98. package/src/components/mail/OutboxPane.tsx +531 -0
  99. package/src/components/mail/PullToRefresh.tsx +24 -0
  100. package/src/components/mail/RawMessageView.tsx +84 -0
  101. package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
  102. package/src/components/mail/SelectionToolbar.tsx +139 -0
  103. package/src/components/mail/SpamRescue.render.test.ts +35 -0
  104. package/src/components/mail/SpamRescue.tsx +138 -0
  105. package/src/components/mail/SwipeableMessageRow.tsx +148 -0
  106. package/src/components/mail/message-body-error-banner-content.ts +87 -0
  107. package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
  108. package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
  109. package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
  110. package/src/components/mail/organize/OrganizePanel.tsx +344 -0
  111. package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
  112. package/src/components/settings/AccountFormPanel.test.ts +72 -0
  113. package/src/components/settings/AccountFormPanel.tsx +942 -0
  114. package/src/components/settings/DangerZone.tsx +30 -0
  115. package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
  116. package/src/components/settings/DeleteAccountDialog.tsx +224 -0
  117. package/src/components/settings/FiltersList.render.test.ts +71 -0
  118. package/src/components/settings/FiltersList.tsx +93 -0
  119. package/src/components/settings/account-form-helpers.test.ts +172 -0
  120. package/src/components/settings/account-form-helpers.ts +105 -0
  121. package/src/components/ui/AppVersion.tsx +50 -0
  122. package/src/components/ui/BugReportButton.tsx +45 -0
  123. package/src/components/ui/ConfirmDialog.tsx +125 -0
  124. package/src/components/ui/DropdownMenu.tsx +105 -0
  125. package/src/components/ui/EmptyState.tsx +11 -0
  126. package/src/components/ui/ErrorBanner.tsx +88 -0
  127. package/src/components/ui/ErrorBannerProvider.tsx +77 -0
  128. package/src/components/ui/ErrorBannerStack.tsx +34 -0
  129. package/src/components/ui/ErrorState.tsx +80 -0
  130. package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
  131. package/src/components/ui/FatalErrorOverlay.tsx +151 -0
  132. package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
  133. package/src/components/ui/SlidePanel.tsx +72 -0
  134. package/src/components/ui/error-banners.test.ts +207 -0
  135. package/src/components/ui/error-banners.ts +111 -0
  136. package/src/hooks/queries/keys.ts +38 -0
  137. package/src/hooks/useArchiveMailbox.ts +104 -0
  138. package/src/hooks/useAutoMovedBadge.ts +77 -0
  139. package/src/hooks/useCurrentMailboxName.ts +99 -0
  140. package/src/hooks/useDebouncedValue.ts +12 -0
  141. package/src/hooks/useDeleteMessages.test.ts +103 -0
  142. package/src/hooks/useDeleteMessages.ts +226 -0
  143. package/src/hooks/useFilters.ts +112 -0
  144. package/src/hooks/useIntelligenceData.test.ts +269 -0
  145. package/src/hooks/useIntelligenceData.ts +284 -0
  146. package/src/hooks/useIsDark.ts +27 -0
  147. package/src/hooks/useKeyboardNavigation.ts +192 -0
  148. package/src/hooks/useLayoutTier.test.ts +47 -0
  149. package/src/hooks/useLayoutTier.ts +44 -0
  150. package/src/hooks/useLongPress.test.ts +291 -0
  151. package/src/hooks/useLongPress.ts +87 -0
  152. package/src/hooks/useMailboxAccount.ts +62 -0
  153. package/src/hooks/useMailboxNameIndex.ts +32 -0
  154. package/src/hooks/useMarkAsRead.test.ts +165 -0
  155. package/src/hooks/useMarkAsRead.ts +319 -0
  156. package/src/hooks/useMediaQuery.ts +39 -0
  157. package/src/hooks/useMessageBodyContent.test.ts +370 -0
  158. package/src/hooks/useMessageBodyContent.ts +380 -0
  159. package/src/hooks/useMoveMessages.test.ts +63 -0
  160. package/src/hooks/useMoveMessages.ts +221 -0
  161. package/src/hooks/useOrganizeJob.ts +90 -0
  162. package/src/hooks/useOrganizePreview.ts +40 -0
  163. package/src/hooks/useRescueCandidates.test.ts +217 -0
  164. package/src/hooks/useRescueCandidates.ts +48 -0
  165. package/src/hooks/useSaveDraft.ts +118 -0
  166. package/src/hooks/useSelection.test.ts +75 -0
  167. package/src/hooks/useSelection.ts +211 -0
  168. package/src/hooks/useSemanticSearch.ts +94 -0
  169. package/src/hooks/useSignature.ts +69 -0
  170. package/src/hooks/useStaleAccountSync.test.ts +211 -0
  171. package/src/hooks/useStaleAccountSync.ts +246 -0
  172. package/src/hooks/useSwipeNavigation.test.ts +35 -0
  173. package/src/hooks/useSwipeNavigation.ts +99 -0
  174. package/src/hooks/useToggleStar.ts +187 -0
  175. package/src/hooks/useToggleTrusted.test.ts +102 -0
  176. package/src/hooks/useToggleTrusted.ts +161 -0
  177. package/src/hooks/useTriageKeyboard.ts +103 -0
  178. package/src/hooks/useTriggerSync.test.ts +24 -0
  179. package/src/hooks/useTriggerSync.ts +58 -0
  180. package/src/hooks/useUpdateAddressFlags.ts +125 -0
  181. package/src/hooks/useVisualViewport.test.ts +127 -0
  182. package/src/hooks/useVisualViewport.ts +51 -0
  183. package/src/index.css +19 -0
  184. package/src/lib/account-order.test.ts +84 -0
  185. package/src/lib/account-order.ts +13 -0
  186. package/src/lib/adjacent-message.test.ts +43 -0
  187. package/src/lib/adjacent-message.ts +20 -0
  188. package/src/lib/api.ts +82 -0
  189. package/src/lib/app-info.ts +15 -0
  190. package/src/lib/auto-moved.test.ts +140 -0
  191. package/src/lib/auto-moved.ts +68 -0
  192. package/src/lib/autodiscovery.test.ts +94 -0
  193. package/src/lib/autodiscovery.ts +354 -0
  194. package/src/lib/brief.test.ts +366 -0
  195. package/src/lib/brief.ts +180 -0
  196. package/src/lib/bug-report.test.ts +213 -0
  197. package/src/lib/bug-report.ts +176 -0
  198. package/src/lib/client.ts +36 -0
  199. package/src/lib/console-errors.test.ts +64 -0
  200. package/src/lib/console-errors.ts +92 -0
  201. package/src/lib/conversation-target.test.ts +68 -0
  202. package/src/lib/conversation-target.ts +59 -0
  203. package/src/lib/display-category.ts +20 -0
  204. package/src/lib/drafts.test.ts +241 -0
  205. package/src/lib/drafts.ts +121 -0
  206. package/src/lib/error-classifier.test.ts +133 -0
  207. package/src/lib/error-classifier.ts +96 -0
  208. package/src/lib/fatal-error.test.ts +97 -0
  209. package/src/lib/fatal-error.ts +165 -0
  210. package/src/lib/folder-roles.test.ts +113 -0
  211. package/src/lib/folder-roles.ts +112 -0
  212. package/src/lib/format.test.ts +115 -0
  213. package/src/lib/format.ts +198 -0
  214. package/src/lib/i18n.ts +36 -0
  215. package/src/lib/intelligence-pref.test.ts +57 -0
  216. package/src/lib/intelligence-pref.ts +25 -0
  217. package/src/lib/keymap-dispatch.test.ts +178 -0
  218. package/src/lib/keymap-dispatch.ts +187 -0
  219. package/src/lib/keymap.test.ts +49 -0
  220. package/src/lib/keymap.ts +178 -0
  221. package/src/lib/mail-context.ts +64 -0
  222. package/src/lib/mail-route.test.ts +80 -0
  223. package/src/lib/mail-route.ts +44 -0
  224. package/src/lib/message-body-source.test.ts +98 -0
  225. package/src/lib/message-body-source.ts +48 -0
  226. package/src/lib/move-targets.test.ts +195 -0
  227. package/src/lib/move-targets.ts +74 -0
  228. package/src/lib/onboarding-completion.test.ts +64 -0
  229. package/src/lib/onboarding-completion.ts +23 -0
  230. package/src/lib/organize/filter-status.test.ts +99 -0
  231. package/src/lib/organize/filter-status.ts +86 -0
  232. package/src/lib/organize/organize-copy.test.ts +93 -0
  233. package/src/lib/organize/organize-copy.ts +61 -0
  234. package/src/lib/organize/organize-model.test.ts +104 -0
  235. package/src/lib/organize/organize-model.ts +105 -0
  236. package/src/lib/organize/organize-poll.test.ts +37 -0
  237. package/src/lib/organize/organize-poll.ts +28 -0
  238. package/src/lib/outbox-status.test.ts +64 -0
  239. package/src/lib/outbox-status.ts +75 -0
  240. package/src/lib/plate-serializer.ts +164 -0
  241. package/src/lib/provider-presets.test.ts +96 -0
  242. package/src/lib/provider-presets.ts +81 -0
  243. package/src/lib/query-error-handler.test.ts +144 -0
  244. package/src/lib/query-error-handler.ts +34 -0
  245. package/src/lib/query-escalation.integration.test.ts +138 -0
  246. package/src/lib/recent-searches.test.ts +53 -0
  247. package/src/lib/recent-searches.ts +34 -0
  248. package/src/lib/rescue-candidates.test.ts +94 -0
  249. package/src/lib/rescue-candidates.ts +35 -0
  250. package/src/lib/rescue-telemetry.test.ts +75 -0
  251. package/src/lib/rescue-telemetry.ts +55 -0
  252. package/src/lib/rum-adapter.test.ts +299 -0
  253. package/src/lib/rum-adapter.ts +107 -0
  254. package/src/lib/saved-searches.test.ts +80 -0
  255. package/src/lib/saved-searches.ts +52 -0
  256. package/src/lib/search-pending.test.ts +61 -0
  257. package/src/lib/search-pending.ts +24 -0
  258. package/src/lib/search-query.test.ts +35 -0
  259. package/src/lib/search-query.ts +11 -0
  260. package/src/lib/search-result.test.ts +160 -0
  261. package/src/lib/search-result.ts +102 -0
  262. package/src/lib/search-schemas.ts +34 -0
  263. package/src/lib/search-token-index.test.ts +112 -0
  264. package/src/lib/search-token-index.ts +56 -0
  265. package/src/lib/search-tokens.test.ts +218 -0
  266. package/src/lib/search-tokens.ts +133 -0
  267. package/src/lib/telemetry-context.tsx +9 -0
  268. package/src/lib/telemetry.ts +17 -0
  269. package/src/lib/theme-preference.ts +46 -0
  270. package/src/lib/theme.ts +33 -0
  271. package/src/lib/utils.ts +4 -0
  272. package/src/main.tsx +10 -0
  273. package/src/routeTree.gen.ts +392 -0
  274. package/src/router.test.ts +81 -0
  275. package/src/router.tsx +31 -0
  276. package/src/routes/__root.tsx +69 -0
  277. package/src/routes/index.tsx +7 -0
  278. package/src/routes/mail/$mailboxId.tsx +27 -0
  279. package/src/routes/mail/flagged.tsx +37 -0
  280. package/src/routes/mail/index.tsx +41 -0
  281. package/src/routes/mail/outbox.tsx +13 -0
  282. package/src/routes/mail.tsx +413 -0
  283. package/src/routes/onboarding.tsx +35 -0
  284. package/src/routes/settings/-accounts.test.ts +65 -0
  285. package/src/routes/settings/accounts.tsx +542 -0
  286. package/src/routes/settings/advanced.tsx +55 -0
  287. package/src/routes/settings/appearance.tsx +140 -0
  288. package/src/routes/settings/filters.tsx +141 -0
  289. package/src/routes/settings/folders.tsx +216 -0
  290. package/src/routes/settings/index.tsx +5 -0
  291. package/src/routes/settings/senders.tsx +466 -0
  292. package/src/routes/settings/suggested-vips.tsx +10 -0
  293. package/src/routes/settings.tsx +66 -0
  294. package/src/runtime-config.ts +75 -0
  295. package/src/shell/index.tsx +98 -0
  296. package/src/types/index.ts +73 -0
  297. package/src/vite-env.d.ts +4 -0
  298. package/vite.base.ts +43 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Appearance settings — density and theme.
3
+ * Instant-apply, no save button. Both settings persist in localStorage.
4
+ *
5
+ * Theme is driven by theme-preference.ts (setThemePreference /
6
+ * getThemePreference) so the same preference that boots from theme.ts
7
+ * is the one the picker shows and changes.
8
+ */
9
+ import { SegmentedControl, SettingsShell } from "@remit/ui";
10
+ import { createFileRoute, useNavigate } from "@tanstack/react-router";
11
+ import { useState } from "react";
12
+ import {
13
+ getThemePreference,
14
+ setThemePreference,
15
+ type ThemePreference,
16
+ } from "@/lib/theme-preference";
17
+ import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
18
+
19
+ export const Route = createFileRoute("/settings/appearance")({
20
+ component: AppearanceSettings,
21
+ });
22
+
23
+ /* ------------------------------------------------------------------ */
24
+ /* Density helpers (local-only until user-preference storage exists) */
25
+ /* ------------------------------------------------------------------ */
26
+
27
+ const DENSITY_KEY = "remit.density";
28
+ type Density = "comfortable" | "compact";
29
+
30
+ function readDensity(): Density {
31
+ if (typeof localStorage === "undefined") return "comfortable";
32
+ return (localStorage.getItem(DENSITY_KEY) as Density | null) ?? "comfortable";
33
+ }
34
+
35
+ /* ------------------------------------------------------------------ */
36
+ /* Help rail copy */
37
+ /* ------------------------------------------------------------------ */
38
+
39
+ const appearanceHelp = (
40
+ <div className="space-y-3">
41
+ <p>
42
+ <strong className="text-fg">Density</strong> controls how much information
43
+ fits on screen. Compact is great on smaller displays; Comfortable gives
44
+ each item more breathing room.
45
+ </p>
46
+ <p>
47
+ <strong className="text-fg">Theme</strong> switches between light, dark,
48
+ and system-preference modes instantly. The change takes effect across the
49
+ whole app immediately.
50
+ </p>
51
+ <p className="text-2xs text-fg-subtle">
52
+ Preferences are stored locally in this browser. Server-side sync is coming
53
+ soon.
54
+ </p>
55
+ </div>
56
+ );
57
+
58
+ /* ------------------------------------------------------------------ */
59
+ /* Page component */
60
+ /* ------------------------------------------------------------------ */
61
+
62
+ function AppearanceSettings() {
63
+ const navigate = useNavigate();
64
+ const [helpOpen, setHelpOpen] = useState(true);
65
+
66
+ const [density, setDensity] = useState<Density>(readDensity);
67
+ // Read the current stored preference so the picker reflects the boot-time value
68
+ const [theme, setTheme] = useState<ThemePreference>(getThemePreference);
69
+
70
+ const handleDensity = (value: Density) => {
71
+ setDensity(value);
72
+ // Density is read by the mail list from the same localStorage key
73
+ if (typeof localStorage !== "undefined") {
74
+ localStorage.setItem(DENSITY_KEY, value);
75
+ }
76
+ };
77
+
78
+ const handleTheme = (value: ThemePreference) => {
79
+ setTheme(value);
80
+ setThemePreference(value); // persists + applies immediately
81
+ };
82
+
83
+ const handleSelectNav = (id: string) => {
84
+ const path = SETTINGS_ID_TO_PATH[id];
85
+ if (path) void navigate({ to: path });
86
+ };
87
+
88
+ return (
89
+ <SettingsShell
90
+ items={SETTINGS_NAV_ITEMS}
91
+ activeId="appearance"
92
+ title="Appearance"
93
+ description="Display density and colour theme — instant-apply."
94
+ help={appearanceHelp}
95
+ helpOpen={helpOpen}
96
+ onToggleHelp={() => setHelpOpen((v) => !v)}
97
+ onSelect={handleSelectNav}
98
+ onBackToMail={() => void navigate({ to: "/mail" })}
99
+ >
100
+ <div className="space-y-5">
101
+ {/* Density */}
102
+ <div className="space-y-2">
103
+ <p className="text-sm font-medium text-fg">Density</p>
104
+ <SegmentedControl
105
+ name="density"
106
+ aria-label="Density"
107
+ value={density}
108
+ onChange={handleDensity}
109
+ options={[
110
+ { value: "comfortable", label: "Comfortable" },
111
+ { value: "compact", label: "Compact" },
112
+ ]}
113
+ />
114
+ <p className="text-xs text-fg-subtle">
115
+ Controls the spacing of message rows in the mail list.
116
+ </p>
117
+ </div>
118
+
119
+ {/* Theme */}
120
+ <div className="space-y-2">
121
+ <p className="text-sm font-medium text-fg">Theme</p>
122
+ <SegmentedControl
123
+ name="theme"
124
+ aria-label="Theme"
125
+ value={theme}
126
+ onChange={handleTheme}
127
+ options={[
128
+ { value: "system", label: "System" },
129
+ { value: "light", label: "Light" },
130
+ { value: "dark", label: "Dark" },
131
+ ]}
132
+ />
133
+ <p className="text-xs text-fg-subtle">
134
+ Applies immediately. System default follows your OS preference.
135
+ </p>
136
+ </div>
137
+ </div>
138
+ </SettingsShell>
139
+ );
140
+ }
@@ -0,0 +1,141 @@
1
+ import {
2
+ configOperationsGetConfigOptions,
3
+ mailboxOperationsListMailboxesOptions,
4
+ } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
5
+ import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
6
+ import { SettingsShell } from "@remit/ui";
7
+ import { useQuery } from "@tanstack/react-query";
8
+ import { createFileRoute, useNavigate } from "@tanstack/react-router";
9
+ import { useCallback, useState } from "react";
10
+ import { FiltersList } from "@/components/settings/FiltersList";
11
+ import { ErrorState } from "@/components/ui/ErrorState";
12
+ import { useDeleteFilter, useFilterList } from "@/hooks/useFilters";
13
+ import { getMailboxDisplayName } from "@/lib/folder-roles";
14
+ import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
15
+
16
+ export const Route = createFileRoute("/settings/filters")({
17
+ component: FiltersSettings,
18
+ });
19
+
20
+ const filtersHelp = (
21
+ <div className="space-y-3">
22
+ <p>
23
+ Filters are the standing rules Organize creates. A{" "}
24
+ <strong>Standing</strong> filter keeps working on new mail; a{" "}
25
+ <strong>Temporary</strong> one stops on its own at the date you picked.
26
+ </p>
27
+ <p>
28
+ An expired filter isn't deleted the instant it lapses — it stays here,
29
+ marked Expired, so you can see what it did and when it stopped.
30
+ </p>
31
+ </div>
32
+ );
33
+
34
+ function AccountFilters({ account }: { account: RemitImapAccountResponse }) {
35
+ const accountId = account.accountId;
36
+ const { filters, isPending, isError, error, refetch } =
37
+ useFilterList(accountId);
38
+ const { deleteFilter, deletingFilterId } = useDeleteFilter(accountId);
39
+
40
+ const { data: mailboxesData } = useQuery({
41
+ ...mailboxOperationsListMailboxesOptions({ path: { accountId } }),
42
+ staleTime: Infinity,
43
+ });
44
+
45
+ const mailboxName = useCallback(
46
+ (mailboxId: string): string | undefined => {
47
+ const mailbox = mailboxesData?.items.find(
48
+ (item) => item.mailboxId === mailboxId,
49
+ );
50
+ return mailbox ? getMailboxDisplayName(mailbox.fullPath) : undefined;
51
+ },
52
+ [mailboxesData?.items],
53
+ );
54
+
55
+ return (
56
+ <section className="space-y-2">
57
+ <h2 className="text-sm font-semibold text-fg">{account.email}</h2>
58
+ {isPending ? (
59
+ // biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label on loading skeleton provides useful context for assistive tech
60
+ <div
61
+ className="h-16 animate-pulse rounded-md border border-line bg-surface"
62
+ aria-busy="true"
63
+ aria-label={`Loading filters for ${account.email}`}
64
+ />
65
+ ) : isError ? (
66
+ <ErrorState
67
+ variant="inline"
68
+ title={`Couldn't load filters for ${account.email}`}
69
+ error={error}
70
+ onRetry={() => {
71
+ refetch();
72
+ }}
73
+ />
74
+ ) : (
75
+ <FiltersList
76
+ filters={filters}
77
+ mailboxName={mailboxName}
78
+ onDelete={deleteFilter}
79
+ deletingFilterId={deletingFilterId}
80
+ />
81
+ )}
82
+ </section>
83
+ );
84
+ }
85
+
86
+ function FiltersSettings() {
87
+ const navigate = useNavigate();
88
+ const [helpOpen, setHelpOpen] = useState(true);
89
+
90
+ const {
91
+ data: config,
92
+ isPending,
93
+ isError,
94
+ error,
95
+ refetch,
96
+ } = useQuery(configOperationsGetConfigOptions());
97
+
98
+ const handleSelectNav = (id: string) => {
99
+ const path = SETTINGS_ID_TO_PATH[id];
100
+ if (path) void navigate({ to: path });
101
+ };
102
+
103
+ return (
104
+ <SettingsShell
105
+ items={SETTINGS_NAV_ITEMS}
106
+ activeId="filters"
107
+ title="Filters"
108
+ description="Standing and temporary rules created by Organize, per account."
109
+ help={filtersHelp}
110
+ helpOpen={helpOpen}
111
+ onToggleHelp={() => setHelpOpen((v) => !v)}
112
+ onSelect={handleSelectNav}
113
+ onBackToMail={() => void navigate({ to: "/mail" })}
114
+ >
115
+ {isPending ? (
116
+ // biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label on loading skeleton provides useful context for assistive tech
117
+ <div
118
+ className="h-24 animate-pulse rounded-sm border border-line bg-surface"
119
+ aria-busy="true"
120
+ aria-label="Loading accounts"
121
+ />
122
+ ) : isError ? (
123
+ <ErrorState
124
+ title="Couldn't load accounts"
125
+ error={error}
126
+ onRetry={() => {
127
+ refetch();
128
+ }}
129
+ />
130
+ ) : config.accounts.length === 0 ? (
131
+ <p className="py-12 text-sm text-fg-muted">No accounts configured.</p>
132
+ ) : (
133
+ <div className="space-y-8">
134
+ {config.accounts.map((account) => (
135
+ <AccountFilters key={account.accountId} account={account} />
136
+ ))}
137
+ </div>
138
+ )}
139
+ </SettingsShell>
140
+ );
141
+ }
@@ -0,0 +1,216 @@
1
+ import {
2
+ configOperationsGetConfigOptions,
3
+ configOperationsGetConfigQueryKey,
4
+ folderRoleOperationsAppointFolderRoleMutation,
5
+ mailboxDetailOperationsRenameMailboxMutation,
6
+ mailboxOperationsListMailboxesOptions,
7
+ mailboxOperationsListMailboxesQueryKey,
8
+ } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
9
+ import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
10
+ import {
11
+ Banner,
12
+ type CandidateFolder,
13
+ type FolderRole,
14
+ RoleAppointmentList,
15
+ SettingsShell,
16
+ } from "@remit/ui";
17
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
18
+ import { createFileRoute, useNavigate } from "@tanstack/react-router";
19
+ import { useState } from "react";
20
+ import { ErrorState } from "@/components/ui/ErrorState";
21
+ import {
22
+ CANONICAL_TO_NAV_ROLE,
23
+ NAV_ROLE_TO_CANONICAL,
24
+ } from "@/lib/folder-roles";
25
+ import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
26
+
27
+ export const Route = createFileRoute("/settings/folders")({
28
+ component: FoldersSettings,
29
+ });
30
+
31
+ const foldersHelp = (
32
+ <div className="space-y-3">
33
+ <p>
34
+ Each canonical role — Inbox, Drafts, Sent, Archive, Spam, Trash — points
35
+ at one of your account's real folders. Pick the one that actually holds
36
+ the mail; the message counts tell real folders from empty look-alikes.
37
+ </p>
38
+ <p>
39
+ Appointing a folder to a role here doesn't touch any other role, and
40
+ doesn't move or rename anything on the server — it just tells Remit which
41
+ folder to treat as e.g. "Drafts" everywhere (sidebar, unread badges, the
42
+ compose flow).
43
+ </p>
44
+ <p>
45
+ <strong className="text-fg">Display name</strong> renames the appointed
46
+ folder for the sidebar. Leave it blank to use the role's canonical name.
47
+ </p>
48
+ </div>
49
+ );
50
+
51
+ /** One account's folder roles, fed to the kit list. Owns its own queries + mutations. */
52
+ function AccountFolderRoles({
53
+ account,
54
+ }: {
55
+ account: RemitImapAccountResponse;
56
+ }) {
57
+ const queryClient = useQueryClient();
58
+ const accountId = account.accountId;
59
+
60
+ const { data, isPending, isError, error, refetch } = useQuery(
61
+ mailboxOperationsListMailboxesOptions({ path: { accountId } }),
62
+ );
63
+
64
+ const appointMutation = useMutation({
65
+ ...folderRoleOperationsAppointFolderRoleMutation(),
66
+ onSuccess: () => {
67
+ queryClient.invalidateQueries({
68
+ queryKey: configOperationsGetConfigQueryKey(),
69
+ });
70
+ },
71
+ });
72
+
73
+ const renameMutation = useMutation({
74
+ ...mailboxDetailOperationsRenameMailboxMutation(),
75
+ onSuccess: () => {
76
+ queryClient.invalidateQueries({
77
+ queryKey: mailboxOperationsListMailboxesQueryKey({
78
+ path: { accountId },
79
+ }),
80
+ });
81
+ },
82
+ });
83
+
84
+ const handleAppoint = (role: FolderRole, mailboxId: string | null) => {
85
+ appointMutation.mutate({
86
+ path: { accountId, role: NAV_ROLE_TO_CANONICAL[role] },
87
+ body: { mailboxId },
88
+ });
89
+ };
90
+
91
+ const handleRename = (mailboxId: string, name: string) => {
92
+ const trimmed = name.trim();
93
+ renameMutation.mutate({
94
+ path: { accountId, mailboxId },
95
+ body: { displayNameOverride: trimmed === "" ? null : trimmed },
96
+ });
97
+ };
98
+
99
+ if (isPending) {
100
+ return (
101
+ // biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label on loading skeleton provides useful context for assistive tech
102
+ <div
103
+ className="h-24 animate-pulse rounded-sm border border-line bg-surface"
104
+ aria-busy="true"
105
+ aria-label={`Loading folders for ${account.email}`}
106
+ />
107
+ );
108
+ }
109
+
110
+ if (isError) {
111
+ return (
112
+ <ErrorState
113
+ variant="inline"
114
+ title={`Couldn't load folders for ${account.email}`}
115
+ error={error}
116
+ onRetry={() => {
117
+ refetch();
118
+ }}
119
+ />
120
+ );
121
+ }
122
+
123
+ const folders: CandidateFolder[] = data.items.map((mailbox) => ({
124
+ mailboxId: mailbox.mailboxId,
125
+ providerPath: mailbox.fullPath,
126
+ messageCount: mailbox.messageCount,
127
+ }));
128
+
129
+ const appointments: Record<string, string | null> = {};
130
+ for (const appointment of account.folderAppointments) {
131
+ const role = CANONICAL_TO_NAV_ROLE[appointment.role];
132
+ if (role) appointments[role] = appointment.mailboxId ?? null;
133
+ }
134
+
135
+ const displayNames: Record<string, string> = {};
136
+ for (const mailbox of data.items) {
137
+ if (mailbox.displayNameOverride) {
138
+ displayNames[mailbox.mailboxId] = mailbox.displayNameOverride;
139
+ }
140
+ }
141
+
142
+ return (
143
+ <div className="space-y-2">
144
+ {(appointMutation.isError || renameMutation.isError) && (
145
+ <Banner tone="danger" variant="soft">
146
+ Couldn't save that change. Please try again.
147
+ </Banner>
148
+ )}
149
+ <RoleAppointmentList
150
+ accountEmail={account.email}
151
+ folders={folders}
152
+ appointments={appointments}
153
+ displayNames={displayNames}
154
+ onAppoint={handleAppoint}
155
+ onRename={handleRename}
156
+ />
157
+ </div>
158
+ );
159
+ }
160
+
161
+ function FoldersSettings() {
162
+ const navigate = useNavigate();
163
+ const [helpOpen, setHelpOpen] = useState(true);
164
+
165
+ const {
166
+ data: config,
167
+ isPending,
168
+ isError,
169
+ error,
170
+ refetch,
171
+ } = useQuery(configOperationsGetConfigOptions());
172
+
173
+ const handleSelectNav = (id: string) => {
174
+ const path = SETTINGS_ID_TO_PATH[id];
175
+ if (path) void navigate({ to: path });
176
+ };
177
+
178
+ return (
179
+ <SettingsShell
180
+ items={SETTINGS_NAV_ITEMS}
181
+ activeId="folders"
182
+ title="Folder roles"
183
+ description="Appoint which real folder fills each canonical role, per account."
184
+ help={foldersHelp}
185
+ helpOpen={helpOpen}
186
+ onToggleHelp={() => setHelpOpen((v) => !v)}
187
+ onSelect={handleSelectNav}
188
+ onBackToMail={() => void navigate({ to: "/mail" })}
189
+ >
190
+ {isPending ? (
191
+ // biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label on loading skeleton provides useful context for assistive tech
192
+ <div
193
+ className="h-24 animate-pulse rounded-sm border border-line bg-surface"
194
+ aria-busy="true"
195
+ aria-label="Loading accounts"
196
+ />
197
+ ) : isError ? (
198
+ <ErrorState
199
+ title="Couldn't load accounts"
200
+ error={error}
201
+ onRetry={() => {
202
+ refetch();
203
+ }}
204
+ />
205
+ ) : config.accounts.length === 0 ? (
206
+ <p className="py-12 text-sm text-fg-muted">No accounts configured.</p>
207
+ ) : (
208
+ <div className="space-y-8">
209
+ {config.accounts.map((account) => (
210
+ <AccountFolderRoles key={account.accountId} account={account} />
211
+ ))}
212
+ </div>
213
+ )}
214
+ </SettingsShell>
215
+ );
216
+ }
@@ -0,0 +1,5 @@
1
+ import { createFileRoute, Navigate } from "@tanstack/react-router";
2
+
3
+ export const Route = createFileRoute("/settings/")({
4
+ component: () => <Navigate to="/settings/accounts" />,
5
+ });