feat: P5-21 mention color, P5-22 font selector, P5-27 notification presets; update docs
CI / Build & Quality Checks (push) Successful in 10m27s
Trigger Desktop Build / trigger (push) Successful in 7s

- P5-21: Custom @mention highlight color picker in Settings → Appearance.
  CSS vars with luminance-computed text color; resets cleanly to theme default.
- P5-22: Font selector (System, Inter, JetBrains Mono, Fira Code) in
  Settings → Appearance. Fira Code added to Google Fonts preload.
- P5-27: Gaming/Work/Sleep preset buttons at top of Settings → Notifications.
  Each atomically applies a group of notification settings.
- AppearanceEffects component in App.tsx applies CSS vars on settings change.
- LOTUS_BUGS.md: mark presence + manifest icon bugs as resolved.
- LOTUS_TODO.md: mark P3-6, P5-21, P5-22, P5-27 as [x].
- LOTUS_FEATURES.md: document all four completed features.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 15:39:35 -04:00
parent 891f2daf99
commit 5469740f4c
9 changed files with 257 additions and 36 deletions
@@ -333,6 +333,11 @@ function Appearance() {
'glassmorphismSidebar',
);
const [pauseAnimations, setPauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
const [mentionHighlightColor, setMentionHighlightColor] = useSetting(
settingsAtom,
'mentionHighlightColor',
);
const [fontFamily, setFontFamily] = useSetting(settingsAtom, 'fontFamily');
return (
<Box direction="Column" gap="100">
@@ -494,6 +499,69 @@ function Appearance() {
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="UI Font"
description="Font used throughout the interface."
after={
<select
value={fontFamily ?? 'inter'}
onChange={(e) =>
setFontFamily(
e.target.value as 'system' | 'inter' | 'jetbrains-mono' | 'fira-code',
)
}
style={{
background: 'var(--bg-surface)',
color: 'inherit',
border: '1px solid var(--border-interactive-normal)',
borderRadius: '6px',
padding: '4px 8px',
fontSize: '14px',
cursor: 'pointer',
}}
>
<option value="system">System Default</option>
<option value="inter">Inter (default)</option>
<option value="jetbrains-mono">JetBrains Mono</option>
<option value="fira-code">Fira Code</option>
</select>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="@Mention Highlight Color"
description="Color used to highlight messages that mention you. Leave empty to use the theme default."
after={
<Box alignItems="Center" gap="200">
<input
type="color"
value={mentionHighlightColor || '#4caf50'}
onChange={(e) => setMentionHighlightColor(e.target.value)}
style={{ width: '36px', height: '28px', cursor: 'pointer', borderRadius: '4px', border: 'none', padding: '2px' }}
/>
{mentionHighlightColor && (
<button
type="button"
onClick={() => setMentionHighlightColor('')}
style={{
background: 'none',
border: '1px solid var(--border-interactive-normal)',
borderRadius: '6px',
padding: '2px 8px',
cursor: 'pointer',
color: 'inherit',
fontSize: '12px',
}}
>
Reset
</button>
)}
</Box>
}
/>
</SequenceCard>
</Box>
);
}
@@ -1,5 +1,6 @@
import React from 'react';
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
import { useAtomValue, useSetAtom } from 'jotai';
import { Box, Text, IconButton, Icon, Icons, Scroll, config, toRem } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SystemNotification } from './SystemNotification';
import { AllMessagesNotifications } from './AllMessages';
@@ -9,6 +10,95 @@ import { PushRuleEditor } from './PushRuleEditor';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { settingsAtom, Settings } from '../../../state/settings';
const PRESETS: Array<{
label: string;
emoji: string;
description: string;
patch: Partial<Settings>;
}> = [
{
label: 'Gaming',
emoji: '🎮',
description: 'Notifications on, sounds off',
patch: {
showNotifications: true,
isNotificationSounds: false,
messageSoundId: 'none',
inviteSoundId: 'none',
quietHoursEnabled: false,
},
},
{
label: 'Work',
emoji: '💼',
description: 'All notifications and sounds on',
patch: {
showNotifications: true,
isNotificationSounds: true,
messageSoundId: 'notification',
inviteSoundId: 'invite',
quietHoursEnabled: false,
},
},
{
label: 'Sleep',
emoji: '🌙',
description: 'All notifications off',
patch: {
showNotifications: false,
isNotificationSounds: false,
quietHoursEnabled: false,
},
},
];
function NotificationPresets() {
const settings = useAtomValue(settingsAtom);
const setSettings = useSetAtom(settingsAtom);
const applyPreset = (patch: Partial<Settings>) => {
setSettings({ ...settings, ...patch });
};
return (
<Box direction="Column" gap="100">
<Text size="L400">Quick Presets</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<Box gap="300" style={{ padding: config.space.S300, flexWrap: 'wrap' }}>
{PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => applyPreset(preset.patch)}
title={preset.description}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: toRem(4),
padding: `${toRem(8)} ${toRem(16)}`,
borderRadius: config.radii.R300,
border: '1px solid var(--border-interactive-normal)',
background: 'var(--bg-surface-low)',
color: 'inherit',
cursor: 'pointer',
minWidth: toRem(80),
}}
>
<span style={{ fontSize: toRem(24) }}>{preset.emoji}</span>
<Text size="T300" style={{ fontWeight: 600 }}>{preset.label}</Text>
<Text size="T200" priority="300" style={{ textAlign: 'center', maxWidth: toRem(120) }}>
{preset.description}
</Text>
</button>
))}
</Box>
</SequenceCard>
</Box>
);
}
type NotificationsProps = {
requestClose: () => void;
@@ -34,6 +124,7 @@ export function Notifications({ requestClose }: NotificationsProps) {
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
<NotificationPresets />
<SystemNotification />
<AllMessagesNotifications />
<SpecialMessagesNotifications />
+38 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { Provider as JotaiProvider, useAtomValue } from 'jotai';
import { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProvider } from 'folds';
@@ -16,6 +16,42 @@ import { useCompositionEndTracking } from '../hooks/useComposingCheck';
import { settingsAtom } from '../state/settings';
import { LotusToastContainer } from '../features/toast/LotusToastContainer';
const FONT_MAP: Record<string, string> = {
system: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
inter: "'InterVariable', sans-serif",
'jetbrains-mono': "'JetBrains Mono', monospace",
'fira-code': "'Fira Code', monospace",
};
function AppearanceEffects() {
const settings = useAtomValue(settingsAtom);
useEffect(() => {
const color = settings.mentionHighlightColor;
if (color) {
document.body.style.setProperty('--mention-highlight-bg', color);
// compute black or white text based on hex luminance
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
document.body.style.setProperty('--mention-highlight-text', lum > 0.5 ? '#000' : '#fff');
document.body.style.setProperty('--mention-highlight-border', color);
} else {
document.body.style.removeProperty('--mention-highlight-bg');
document.body.style.removeProperty('--mention-highlight-text');
document.body.style.removeProperty('--mention-highlight-border');
}
}, [settings.mentionHighlightColor]);
useEffect(() => {
const font = FONT_MAP[settings.fontFamily ?? 'inter'] ?? FONT_MAP.inter;
document.body.style.setProperty('--font-secondary', font);
}, [settings.fontFamily]);
return null;
}
function NightLightOverlay() {
const settings = useAtomValue(settingsAtom);
if (!settings.nightLightEnabled) return null;
@@ -94,6 +130,7 @@ function App() {
<ClientConfigProvider value={clientConfig}>
<QueryClientProvider client={queryClient}>
<JotaiProvider>
<AppearanceEffects />
<RouterProvider router={createRouter(clientConfig, screenSize)} />
<NightLightOverlay />
<LotusToastContainer />
+6
View File
@@ -125,6 +125,9 @@ export interface Settings {
pauseAnimations: boolean;
composerToolbarButtons: ComposerToolbarSettings;
mentionHighlightColor: string;
fontFamily: 'system' | 'inter' | 'jetbrains-mono' | 'fira-code';
}
const defaultSettings: Settings = {
@@ -192,6 +195,9 @@ const defaultSettings: Settings = {
pauseAnimations: false,
composerToolbarButtons: DEFAULT_COMPOSER_TOOLBAR,
mentionHighlightColor: '',
fontFamily: 'inter',
};
export const getSettings = (): Settings => {
+3 -3
View File
@@ -169,9 +169,9 @@ export const Mention = recipe({
variants: {
highlight: {
true: {
backgroundColor: color.Success.Container,
color: color.Success.OnContainer,
boxShadow: `0 0 0 ${config.borderWidth.B300} ${color.Success.ContainerLine}`,
backgroundColor: `var(--mention-highlight-bg, ${color.Success.Container as string})`,
color: `var(--mention-highlight-text, ${color.Success.OnContainer as string})`,
boxShadow: `0 0 0 ${config.borderWidth.B300} var(--mention-highlight-border, ${color.Success.ContainerLine as string})`,
},
},
focus: {