feat: P5-21 mention color, P5-22 font selector, P5-27 notification presets; update docs
- 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:
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user