Backgrounds: theme-aware patterns and visual preview grid
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": [
|
||||
"matrix.lotusguild.org"
|
||||
],
|
||||
"allowCustomHomeservers": false,
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
"spaces": [],
|
||||
"rooms": [],
|
||||
"servers": []
|
||||
},
|
||||
"hashRouter": {
|
||||
"enabled": false,
|
||||
"basename": "/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import { ChatBackground } from '../../state/settings';
|
||||
|
||||
export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: 'blueprint', label: 'Blueprint' },
|
||||
{ value: 'carbon', label: 'Carbon' },
|
||||
{ value: 'stars', label: 'Stars' },
|
||||
{ value: 'topographic', label: 'Topographic' },
|
||||
{ value: 'herringbone', label: 'Herringbone' },
|
||||
{ value: 'crosshatch', label: 'Crosshatch' },
|
||||
];
|
||||
|
||||
const DARK: Record<ChatBackground, CSSProperties> = {
|
||||
none: {},
|
||||
|
||||
blueprint: {
|
||||
backgroundColor: '#0a1628',
|
||||
backgroundImage: [
|
||||
'linear-gradient(rgba(100,149,237,0.14) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(100,149,237,0.14) 1px, transparent 1px)',
|
||||
'linear-gradient(rgba(100,149,237,0.05) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(100,149,237,0.05) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '80px 80px, 80px 80px, 16px 16px, 16px 16px',
|
||||
},
|
||||
|
||||
carbon: {
|
||||
backgroundColor: '#0e0e0e',
|
||||
backgroundImage: [
|
||||
'repeating-linear-gradient(45deg, rgba(255,255,255,0.035) 0, rgba(255,255,255,0.035) 2px, transparent 0, transparent 50%)',
|
||||
'repeating-linear-gradient(135deg, rgba(255,255,255,0.035) 0, rgba(255,255,255,0.035) 2px, transparent 0, transparent 50%)',
|
||||
].join(','),
|
||||
backgroundSize: '8px 8px',
|
||||
},
|
||||
|
||||
stars: {
|
||||
backgroundColor: '#050510',
|
||||
backgroundImage: [
|
||||
'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
|
||||
'radial-gradient(circle, rgba(255,255,255,0.55) 1px, transparent 1px)',
|
||||
'radial-gradient(circle, rgba(200,200,255,0.3) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '130px 130px, 190px 190px, 260px 260px',
|
||||
backgroundPosition: '0 0, 65px 32px, 32px 97px',
|
||||
},
|
||||
|
||||
topographic: {
|
||||
backgroundColor: '#0f0f17',
|
||||
backgroundImage: [
|
||||
'repeating-radial-gradient(circle at 20% 20%, transparent 0, transparent 30px, rgba(152,0,0,0.07) 31px, transparent 32px)',
|
||||
'repeating-radial-gradient(circle at 80% 80%, transparent 0, transparent 25px, rgba(100,100,200,0.06) 26px, transparent 27px)',
|
||||
'repeating-radial-gradient(circle at 50% 10%, transparent 0, transparent 45px, rgba(152,0,0,0.04) 46px, transparent 47px)',
|
||||
].join(','),
|
||||
},
|
||||
|
||||
herringbone: {
|
||||
backgroundColor: '#111118',
|
||||
backgroundImage: [
|
||||
'repeating-linear-gradient(60deg, rgba(180,160,210,0.08) 0, rgba(180,160,210,0.08) 1px, transparent 0, transparent 50%)',
|
||||
'repeating-linear-gradient(120deg, rgba(180,160,210,0.08) 0, rgba(180,160,210,0.08) 1px, transparent 0, transparent 50%)',
|
||||
].join(','),
|
||||
backgroundSize: '20px 36px',
|
||||
},
|
||||
|
||||
crosshatch: {
|
||||
backgroundColor: '#0f0f0f',
|
||||
backgroundImage: [
|
||||
'linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px)',
|
||||
'linear-gradient(rgba(255,255,255,0.022) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(255,255,255,0.022) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
|
||||
},
|
||||
};
|
||||
|
||||
const LIGHT: Record<ChatBackground, CSSProperties> = {
|
||||
none: {},
|
||||
|
||||
blueprint: {
|
||||
backgroundColor: '#eef3ff',
|
||||
backgroundImage: [
|
||||
'linear-gradient(rgba(50,100,220,0.16) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(50,100,220,0.16) 1px, transparent 1px)',
|
||||
'linear-gradient(rgba(50,100,220,0.06) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(50,100,220,0.06) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '80px 80px, 80px 80px, 16px 16px, 16px 16px',
|
||||
},
|
||||
|
||||
carbon: {
|
||||
backgroundColor: '#efefef',
|
||||
backgroundImage: [
|
||||
'repeating-linear-gradient(45deg, rgba(0,0,0,0.04) 0, rgba(0,0,0,0.04) 2px, transparent 0, transparent 50%)',
|
||||
'repeating-linear-gradient(135deg, rgba(0,0,0,0.04) 0, rgba(0,0,0,0.04) 2px, transparent 0, transparent 50%)',
|
||||
].join(','),
|
||||
backgroundSize: '8px 8px',
|
||||
},
|
||||
|
||||
// Stars is intentionally always dark — it's a night-sky theme
|
||||
stars: {
|
||||
backgroundColor: '#050510',
|
||||
backgroundImage: [
|
||||
'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
|
||||
'radial-gradient(circle, rgba(255,255,255,0.55) 1px, transparent 1px)',
|
||||
'radial-gradient(circle, rgba(200,200,255,0.3) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '130px 130px, 190px 190px, 260px 260px',
|
||||
backgroundPosition: '0 0, 65px 32px, 32px 97px',
|
||||
},
|
||||
|
||||
topographic: {
|
||||
backgroundColor: '#faf8f5',
|
||||
backgroundImage: [
|
||||
'repeating-radial-gradient(circle at 20% 20%, transparent 0, transparent 30px, rgba(100,60,60,0.09) 31px, transparent 32px)',
|
||||
'repeating-radial-gradient(circle at 80% 80%, transparent 0, transparent 25px, rgba(60,60,130,0.07) 26px, transparent 27px)',
|
||||
'repeating-radial-gradient(circle at 50% 10%, transparent 0, transparent 45px, rgba(100,60,60,0.05) 46px, transparent 47px)',
|
||||
].join(','),
|
||||
},
|
||||
|
||||
herringbone: {
|
||||
backgroundColor: '#f9f9f9',
|
||||
backgroundImage: [
|
||||
'repeating-linear-gradient(60deg, rgba(80,70,110,0.09) 0, rgba(80,70,110,0.09) 1px, transparent 0, transparent 50%)',
|
||||
'repeating-linear-gradient(120deg, rgba(80,70,110,0.09) 0, rgba(80,70,110,0.09) 1px, transparent 0, transparent 50%)',
|
||||
].join(','),
|
||||
backgroundSize: '20px 36px',
|
||||
},
|
||||
|
||||
crosshatch: {
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundImage: [
|
||||
'linear-gradient(rgba(0,0,0,0.07) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(0,0,0,0.07) 1px, transparent 1px)',
|
||||
'linear-gradient(rgba(0,0,0,0.025) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(0,0,0,0.025) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
|
||||
},
|
||||
};
|
||||
|
||||
export const getChatBg = (bg: ChatBackground, isDark: boolean): CSSProperties =>
|
||||
isDark ? DARK[bg] : LIGHT[bg];
|
||||
@@ -16,7 +16,9 @@ import { RoomInput } from './RoomInput';
|
||||
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
|
||||
import { Page } from '../../components/page';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom, ChatBackground } from '../../state/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useTheme, ThemeKind } from '../../hooks/useTheme';
|
||||
import { getChatBg } from '../lotus/chatBackground';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
import { editableActiveElement } from '../../utils/dom';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
@@ -57,80 +59,13 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||
};
|
||||
|
||||
|
||||
const CHAT_BG: Record<ChatBackground, React.CSSProperties> = {
|
||||
none: {},
|
||||
|
||||
// Dark navy with light-blue grid lines — technical/clean
|
||||
blueprint: {
|
||||
backgroundColor: '#0a1628',
|
||||
backgroundImage: [
|
||||
'linear-gradient(rgba(100,149,237,0.14) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(100,149,237,0.14) 1px, transparent 1px)',
|
||||
'linear-gradient(rgba(100,149,237,0.05) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(100,149,237,0.05) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '80px 80px, 80px 80px, 16px 16px, 16px 16px',
|
||||
},
|
||||
|
||||
// Near-black diagonal weave — carbon fiber look
|
||||
carbon: {
|
||||
backgroundColor: '#0e0e0e',
|
||||
backgroundImage: [
|
||||
'repeating-linear-gradient(45deg, rgba(255,255,255,0.03) 0, rgba(255,255,255,0.03) 2px, transparent 0, transparent 50%)',
|
||||
'repeating-linear-gradient(135deg, rgba(255,255,255,0.03) 0, rgba(255,255,255,0.03) 2px, transparent 0, transparent 50%)',
|
||||
].join(','),
|
||||
backgroundSize: '8px 8px',
|
||||
},
|
||||
|
||||
// Deep space with three offset star layers
|
||||
stars: {
|
||||
backgroundColor: '#050510',
|
||||
backgroundImage: [
|
||||
'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
|
||||
'radial-gradient(circle, rgba(255,255,255,0.55) 1px, transparent 1px)',
|
||||
'radial-gradient(circle, rgba(200,200,255,0.3) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '130px 130px, 190px 190px, 260px 260px',
|
||||
backgroundPosition: '0 0, 65px 32px, 32px 97px',
|
||||
},
|
||||
|
||||
// Concentric rings from three focal points — map contour effect
|
||||
topographic: {
|
||||
backgroundColor: '#0f0f17',
|
||||
backgroundImage: [
|
||||
'repeating-radial-gradient(circle at 20% 20%, transparent 0, transparent 30px, rgba(152,0,0,0.07) 31px, transparent 32px)',
|
||||
'repeating-radial-gradient(circle at 80% 80%, transparent 0, transparent 25px, rgba(100,100,200,0.06) 26px, transparent 27px)',
|
||||
'repeating-radial-gradient(circle at 50% 10%, transparent 0, transparent 45px, rgba(152,0,0,0.04) 46px, transparent 47px)',
|
||||
].join(','),
|
||||
},
|
||||
|
||||
// 60°/120° diagonal stripe pair — classic herringbone weave
|
||||
herringbone: {
|
||||
backgroundColor: '#111118',
|
||||
backgroundImage: [
|
||||
'repeating-linear-gradient(60deg, rgba(180,160,210,0.07) 0, rgba(180,160,210,0.07) 1px, transparent 0, transparent 50%)',
|
||||
'repeating-linear-gradient(120deg, rgba(180,160,210,0.07) 0, rgba(180,160,210,0.07) 1px, transparent 0, transparent 50%)',
|
||||
].join(','),
|
||||
backgroundSize: '20px 36px',
|
||||
},
|
||||
|
||||
// Fine white grid with larger subdivision — graph paper
|
||||
crosshatch: {
|
||||
backgroundColor: '#0f0f0f',
|
||||
backgroundImage: [
|
||||
'linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px)',
|
||||
'linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px)',
|
||||
'linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px)',
|
||||
].join(','),
|
||||
backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
|
||||
},
|
||||
};
|
||||
|
||||
export function RoomView({ eventId }: { eventId?: string }) {
|
||||
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const theme = useTheme();
|
||||
const isDark = theme.kind === ThemeKind.Dark;
|
||||
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
|
||||
@@ -165,7 +100,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Page ref={roomViewRef} style={CHAT_BG[chatBackground]}>
|
||||
<Page ref={roomViewRef} style={getChatBg(chatBackground, isDark)}>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<RoomTimeline
|
||||
key={roomId}
|
||||
|
||||
@@ -32,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { ChatBackground, DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
@@ -42,10 +42,12 @@ import {
|
||||
Theme,
|
||||
ThemeKind,
|
||||
useSystemThemeKind,
|
||||
useTheme,
|
||||
useThemeNames,
|
||||
useThemes,
|
||||
} from '../../../hooks/useTheme';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { BG_OPTIONS, getChatBg } from '../../lotus/chatBackground';
|
||||
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
|
||||
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
|
||||
import { useDateFormatItems } from '../../../hooks/useDateFormat';
|
||||
@@ -352,12 +354,14 @@ function Appearance() {
|
||||
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="200">
|
||||
<SettingTile
|
||||
title="Chat Background"
|
||||
description="Pattern applied behind the message timeline."
|
||||
after={<SelectChatBackground />}
|
||||
/>
|
||||
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
|
||||
<ChatBgGrid />
|
||||
</Box>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
@@ -759,81 +763,43 @@ function Editor() {
|
||||
}
|
||||
|
||||
|
||||
const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: 'blueprint', label: 'Blueprint' },
|
||||
{ value: 'carbon', label: 'Carbon' },
|
||||
{ value: 'stars', label: 'Stars' },
|
||||
{ value: 'topographic', label: 'Topographic' },
|
||||
{ value: 'herringbone', label: 'Herringbone' },
|
||||
{ value: 'crosshatch', label: 'Crosshatch' },
|
||||
];
|
||||
|
||||
function SelectChatBackground() {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
function ChatBgGrid() {
|
||||
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleSelect = (value: ChatBackground) => {
|
||||
setChatBackground(value);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
const theme = useTheme();
|
||||
const isDark = theme.kind === ThemeKind.Dark;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
outlined
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
after={<Icon size="300" src={Icons.ChevronBottom} />}
|
||||
onClick={handleMenu}
|
||||
>
|
||||
<Text size="T300">
|
||||
{BG_OPTIONS.find((o) => o.value === chatBackground)?.label ?? 'None'}
|
||||
</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
<Box wrap="Wrap" gap="200">
|
||||
{BG_OPTIONS.map((opt) => (
|
||||
<Box key={opt.value} direction="Column" gap="100" style={{ alignItems: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={opt.label}
|
||||
aria-pressed={chatBackground === opt.value}
|
||||
onClick={() => setChatBackground(opt.value as ChatBackground)}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: toRem(76),
|
||||
height: toRem(50),
|
||||
borderRadius: toRem(8),
|
||||
cursor: 'pointer',
|
||||
border: chatBackground === opt.value
|
||||
? '2px solid #980000'
|
||||
: '2px solid rgba(128,128,128,0.25)',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
...getChatBg(opt.value as ChatBackground, isDark),
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="T200"
|
||||
style={chatBackground === opt.value ? { color: '#980000' } : undefined}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{BG_OPTIONS.map((opt) => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
size="300"
|
||||
variant={chatBackground === opt.value ? 'Primary' : 'Surface'}
|
||||
radii="300"
|
||||
onClick={() => handleSelect(opt.value)}
|
||||
>
|
||||
<Text size="T300">{opt.label}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user