@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.
- package/harness/build.mjs +84 -0
- package/harness/index.html +37 -0
- package/harness/vite-preset.ts +29 -0
- package/index.html +47 -0
- package/package.json +147 -0
- package/public/config.js +5 -0
- package/public/locales/de/common.json +1 -0
- package/public/locales/de/errors.json +1 -0
- package/public/locales/de/mail.json +13 -0
- package/public/locales/de/settings.json +1 -0
- package/public/locales/en/common.json +26 -0
- package/public/locales/en/errors.json +1 -0
- package/public/locales/en/mail.json +69 -0
- package/public/locales/en/settings.json +1 -0
- package/public/locales/fr/common.json +1 -0
- package/public/locales/fr/errors.json +1 -0
- package/public/locales/fr/mail.json +13 -0
- package/public/locales/fr/settings.json +1 -0
- package/public/locales/nl/common.json +1 -0
- package/public/locales/nl/errors.json +1 -0
- package/public/locales/nl/mail.json +13 -0
- package/public/locales/nl/settings.json +1 -0
- package/src/auth/AccountMenu.tsx +58 -0
- package/src/auth/AccountSession.tsx +19 -0
- package/src/auth/BetterAuthShell.tsx +198 -0
- package/src/auth/account-menu-mode.test.ts +64 -0
- package/src/auth/account-menu-mode.ts +28 -0
- package/src/auth/amplify-config.test.ts +158 -0
- package/src/auth/amplify-config.ts +60 -0
- package/src/auth/auth-error.test.ts +109 -0
- package/src/auth/auth-error.ts +107 -0
- package/src/auth/auth-interceptor.test.ts +86 -0
- package/src/auth/auth-interceptor.ts +23 -0
- package/src/auth/auth.css +334 -0
- package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
- package/src/auth/better-auth-config.ts +85 -0
- package/src/auth/better-auth-provider.tsx +20 -0
- package/src/auth/cognito/CognitoAccount.tsx +48 -0
- package/src/auth/cognito/CognitoShell.tsx +266 -0
- package/src/auth/cognito/cognito-token.test.ts +110 -0
- package/src/auth/cognito/cognito-token.ts +47 -0
- package/src/auth/cognito/cognito.css +21 -0
- package/src/auth/cognito-provider.tsx +22 -0
- package/src/auth/combined-provider.tsx +42 -0
- package/src/auth/composition.test.ts +136 -0
- package/src/auth/provider.tsx +63 -0
- package/src/auth/sign-out-visibility.test.ts +40 -0
- package/src/auth/sign-out-visibility.ts +10 -0
- package/src/auth/sign-up-disabled.test.ts +23 -0
- package/src/components/compose/AddressField.tsx +237 -0
- package/src/components/compose/ComposeBody.tsx +36 -0
- package/src/components/compose/ComposeForm.tsx +676 -0
- package/src/components/compose/ComposeProvider.tsx +133 -0
- package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
- package/src/components/compose/FromSelector.tsx +67 -0
- package/src/components/compose/FullCompose.tsx +82 -0
- package/src/components/compose/InlineCompose.tsx +29 -0
- package/src/components/compose/MobileComposeSheet.tsx +152 -0
- package/src/components/compose/PlateEditor.tsx +86 -0
- package/src/components/compose/PlateToolbar.tsx +110 -0
- package/src/components/compose/SubjectField.tsx +19 -0
- package/src/components/compose/draft-discard-banner.test.ts +76 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/sanitize-quote-html.ts +23 -0
- package/src/components/layout/AppShellSkeleton.tsx +53 -0
- package/src/components/layout/ComposeFab.tsx +86 -0
- package/src/components/layout/Drawer.tsx +105 -0
- package/src/components/mail/AutoMovedIndicator.tsx +48 -0
- package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
- package/src/components/mail/BlockedImagesNotice.tsx +41 -0
- package/src/components/mail/BriefPane.tsx +289 -0
- package/src/components/mail/ConversationView.tsx +462 -0
- package/src/components/mail/DailyBrief.tsx +451 -0
- package/src/components/mail/DraftsView.tsx +279 -0
- package/src/components/mail/FlaggedList.tsx +170 -0
- package/src/components/mail/FlaggedPane.tsx +217 -0
- package/src/components/mail/IntelligencePane.test.ts +123 -0
- package/src/components/mail/IntelligencePane.tsx +441 -0
- package/src/components/mail/MailListHeader.tsx +187 -0
- package/src/components/mail/MailNav.tsx +70 -0
- package/src/components/mail/MailSidebarAdapter.tsx +351 -0
- package/src/components/mail/MailViewChrome.tsx +95 -0
- package/src/components/mail/MailboxPane.tsx +1219 -0
- package/src/components/mail/MessageActionMenu.tsx +368 -0
- package/src/components/mail/MessageBody.tsx +207 -0
- package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
- package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
- package/src/components/mail/MessageCard.tsx +401 -0
- package/src/components/mail/MessageDetail.tsx +153 -0
- package/src/components/mail/MessageList.tsx +804 -0
- package/src/components/mail/MessageListItem.tsx +267 -0
- package/src/components/mail/MessageToolbar.render.test.ts +57 -0
- package/src/components/mail/MessageToolbar.tsx +166 -0
- package/src/components/mail/MobileMessageBar.tsx +142 -0
- package/src/components/mail/MoveToTrigger.tsx +250 -0
- package/src/components/mail/NavMenuButton.tsx +25 -0
- package/src/components/mail/OutboxPane.tsx +531 -0
- package/src/components/mail/PullToRefresh.tsx +24 -0
- package/src/components/mail/RawMessageView.tsx +84 -0
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
- package/src/components/mail/SelectionToolbar.tsx +139 -0
- package/src/components/mail/SpamRescue.render.test.ts +35 -0
- package/src/components/mail/SpamRescue.tsx +138 -0
- package/src/components/mail/SwipeableMessageRow.tsx +148 -0
- package/src/components/mail/message-body-error-banner-content.ts +87 -0
- package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
- package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
- package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
- package/src/components/mail/organize/OrganizePanel.tsx +344 -0
- package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
- package/src/components/settings/AccountFormPanel.test.ts +72 -0
- package/src/components/settings/AccountFormPanel.tsx +942 -0
- package/src/components/settings/DangerZone.tsx +30 -0
- package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
- package/src/components/settings/DeleteAccountDialog.tsx +224 -0
- package/src/components/settings/FiltersList.render.test.ts +71 -0
- package/src/components/settings/FiltersList.tsx +93 -0
- package/src/components/settings/account-form-helpers.test.ts +172 -0
- package/src/components/settings/account-form-helpers.ts +105 -0
- package/src/components/ui/AppVersion.tsx +50 -0
- package/src/components/ui/BugReportButton.tsx +45 -0
- package/src/components/ui/ConfirmDialog.tsx +125 -0
- package/src/components/ui/DropdownMenu.tsx +105 -0
- package/src/components/ui/EmptyState.tsx +11 -0
- package/src/components/ui/ErrorBanner.tsx +88 -0
- package/src/components/ui/ErrorBannerProvider.tsx +77 -0
- package/src/components/ui/ErrorBannerStack.tsx +34 -0
- package/src/components/ui/ErrorState.tsx +80 -0
- package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
- package/src/components/ui/FatalErrorOverlay.tsx +151 -0
- package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
- package/src/components/ui/SlidePanel.tsx +72 -0
- package/src/components/ui/error-banners.test.ts +207 -0
- package/src/components/ui/error-banners.ts +111 -0
- package/src/hooks/queries/keys.ts +38 -0
- package/src/hooks/useArchiveMailbox.ts +104 -0
- package/src/hooks/useAutoMovedBadge.ts +77 -0
- package/src/hooks/useCurrentMailboxName.ts +99 -0
- package/src/hooks/useDebouncedValue.ts +12 -0
- package/src/hooks/useDeleteMessages.test.ts +103 -0
- package/src/hooks/useDeleteMessages.ts +226 -0
- package/src/hooks/useFilters.ts +112 -0
- package/src/hooks/useIntelligenceData.test.ts +269 -0
- package/src/hooks/useIntelligenceData.ts +284 -0
- package/src/hooks/useIsDark.ts +27 -0
- package/src/hooks/useKeyboardNavigation.ts +192 -0
- package/src/hooks/useLayoutTier.test.ts +47 -0
- package/src/hooks/useLayoutTier.ts +44 -0
- package/src/hooks/useLongPress.test.ts +291 -0
- package/src/hooks/useLongPress.ts +87 -0
- package/src/hooks/useMailboxAccount.ts +62 -0
- package/src/hooks/useMailboxNameIndex.ts +32 -0
- package/src/hooks/useMarkAsRead.test.ts +165 -0
- package/src/hooks/useMarkAsRead.ts +319 -0
- package/src/hooks/useMediaQuery.ts +39 -0
- package/src/hooks/useMessageBodyContent.test.ts +370 -0
- package/src/hooks/useMessageBodyContent.ts +380 -0
- package/src/hooks/useMoveMessages.test.ts +63 -0
- package/src/hooks/useMoveMessages.ts +221 -0
- package/src/hooks/useOrganizeJob.ts +90 -0
- package/src/hooks/useOrganizePreview.ts +40 -0
- package/src/hooks/useRescueCandidates.test.ts +217 -0
- package/src/hooks/useRescueCandidates.ts +48 -0
- package/src/hooks/useSaveDraft.ts +118 -0
- package/src/hooks/useSelection.test.ts +75 -0
- package/src/hooks/useSelection.ts +211 -0
- package/src/hooks/useSemanticSearch.ts +94 -0
- package/src/hooks/useSignature.ts +69 -0
- package/src/hooks/useStaleAccountSync.test.ts +211 -0
- package/src/hooks/useStaleAccountSync.ts +246 -0
- package/src/hooks/useSwipeNavigation.test.ts +35 -0
- package/src/hooks/useSwipeNavigation.ts +99 -0
- package/src/hooks/useToggleStar.ts +187 -0
- package/src/hooks/useToggleTrusted.test.ts +102 -0
- package/src/hooks/useToggleTrusted.ts +161 -0
- package/src/hooks/useTriageKeyboard.ts +103 -0
- package/src/hooks/useTriggerSync.test.ts +24 -0
- package/src/hooks/useTriggerSync.ts +58 -0
- package/src/hooks/useUpdateAddressFlags.ts +125 -0
- package/src/hooks/useVisualViewport.test.ts +127 -0
- package/src/hooks/useVisualViewport.ts +51 -0
- package/src/index.css +19 -0
- package/src/lib/account-order.test.ts +84 -0
- package/src/lib/account-order.ts +13 -0
- package/src/lib/adjacent-message.test.ts +43 -0
- package/src/lib/adjacent-message.ts +20 -0
- package/src/lib/api.ts +82 -0
- package/src/lib/app-info.ts +15 -0
- package/src/lib/auto-moved.test.ts +140 -0
- package/src/lib/auto-moved.ts +68 -0
- package/src/lib/autodiscovery.test.ts +94 -0
- package/src/lib/autodiscovery.ts +354 -0
- package/src/lib/brief.test.ts +366 -0
- package/src/lib/brief.ts +180 -0
- package/src/lib/bug-report.test.ts +213 -0
- package/src/lib/bug-report.ts +176 -0
- package/src/lib/client.ts +36 -0
- package/src/lib/console-errors.test.ts +64 -0
- package/src/lib/console-errors.ts +92 -0
- package/src/lib/conversation-target.test.ts +68 -0
- package/src/lib/conversation-target.ts +59 -0
- package/src/lib/display-category.ts +20 -0
- package/src/lib/drafts.test.ts +241 -0
- package/src/lib/drafts.ts +121 -0
- package/src/lib/error-classifier.test.ts +133 -0
- package/src/lib/error-classifier.ts +96 -0
- package/src/lib/fatal-error.test.ts +97 -0
- package/src/lib/fatal-error.ts +165 -0
- package/src/lib/folder-roles.test.ts +113 -0
- package/src/lib/folder-roles.ts +112 -0
- package/src/lib/format.test.ts +115 -0
- package/src/lib/format.ts +198 -0
- package/src/lib/i18n.ts +36 -0
- package/src/lib/intelligence-pref.test.ts +57 -0
- package/src/lib/intelligence-pref.ts +25 -0
- package/src/lib/keymap-dispatch.test.ts +178 -0
- package/src/lib/keymap-dispatch.ts +187 -0
- package/src/lib/keymap.test.ts +49 -0
- package/src/lib/keymap.ts +178 -0
- package/src/lib/mail-context.ts +64 -0
- package/src/lib/mail-route.test.ts +80 -0
- package/src/lib/mail-route.ts +44 -0
- package/src/lib/message-body-source.test.ts +98 -0
- package/src/lib/message-body-source.ts +48 -0
- package/src/lib/move-targets.test.ts +195 -0
- package/src/lib/move-targets.ts +74 -0
- package/src/lib/onboarding-completion.test.ts +64 -0
- package/src/lib/onboarding-completion.ts +23 -0
- package/src/lib/organize/filter-status.test.ts +99 -0
- package/src/lib/organize/filter-status.ts +86 -0
- package/src/lib/organize/organize-copy.test.ts +93 -0
- package/src/lib/organize/organize-copy.ts +61 -0
- package/src/lib/organize/organize-model.test.ts +104 -0
- package/src/lib/organize/organize-model.ts +105 -0
- package/src/lib/organize/organize-poll.test.ts +37 -0
- package/src/lib/organize/organize-poll.ts +28 -0
- package/src/lib/outbox-status.test.ts +64 -0
- package/src/lib/outbox-status.ts +75 -0
- package/src/lib/plate-serializer.ts +164 -0
- package/src/lib/provider-presets.test.ts +96 -0
- package/src/lib/provider-presets.ts +81 -0
- package/src/lib/query-error-handler.test.ts +144 -0
- package/src/lib/query-error-handler.ts +34 -0
- package/src/lib/query-escalation.integration.test.ts +138 -0
- package/src/lib/recent-searches.test.ts +53 -0
- package/src/lib/recent-searches.ts +34 -0
- package/src/lib/rescue-candidates.test.ts +94 -0
- package/src/lib/rescue-candidates.ts +35 -0
- package/src/lib/rescue-telemetry.test.ts +75 -0
- package/src/lib/rescue-telemetry.ts +55 -0
- package/src/lib/rum-adapter.test.ts +299 -0
- package/src/lib/rum-adapter.ts +107 -0
- package/src/lib/saved-searches.test.ts +80 -0
- package/src/lib/saved-searches.ts +52 -0
- package/src/lib/search-pending.test.ts +61 -0
- package/src/lib/search-pending.ts +24 -0
- package/src/lib/search-query.test.ts +35 -0
- package/src/lib/search-query.ts +11 -0
- package/src/lib/search-result.test.ts +160 -0
- package/src/lib/search-result.ts +102 -0
- package/src/lib/search-schemas.ts +34 -0
- package/src/lib/search-token-index.test.ts +112 -0
- package/src/lib/search-token-index.ts +56 -0
- package/src/lib/search-tokens.test.ts +218 -0
- package/src/lib/search-tokens.ts +133 -0
- package/src/lib/telemetry-context.tsx +9 -0
- package/src/lib/telemetry.ts +17 -0
- package/src/lib/theme-preference.ts +46 -0
- package/src/lib/theme.ts +33 -0
- package/src/lib/utils.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/routeTree.gen.ts +392 -0
- package/src/router.test.ts +81 -0
- package/src/router.tsx +31 -0
- package/src/routes/__root.tsx +69 -0
- package/src/routes/index.tsx +7 -0
- package/src/routes/mail/$mailboxId.tsx +27 -0
- package/src/routes/mail/flagged.tsx +37 -0
- package/src/routes/mail/index.tsx +41 -0
- package/src/routes/mail/outbox.tsx +13 -0
- package/src/routes/mail.tsx +413 -0
- package/src/routes/onboarding.tsx +35 -0
- package/src/routes/settings/-accounts.test.ts +65 -0
- package/src/routes/settings/accounts.tsx +542 -0
- package/src/routes/settings/advanced.tsx +55 -0
- package/src/routes/settings/appearance.tsx +140 -0
- package/src/routes/settings/filters.tsx +141 -0
- package/src/routes/settings/folders.tsx +216 -0
- package/src/routes/settings/index.tsx +5 -0
- package/src/routes/settings/senders.tsx +466 -0
- package/src/routes/settings/suggested-vips.tsx +10 -0
- package/src/routes/settings.tsx +66 -0
- package/src/runtime-config.ts +75 -0
- package/src/shell/index.tsx +98 -0
- package/src/types/index.ts +73 -0
- package/src/vite-env.d.ts +4 -0
- 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
|
+
}
|