Theme designer updates

This commit is contained in:
Sean Edmunds
2026-01-29 14:20:29 -06:00
parent 4e31378214
commit fa905011f5
7 changed files with 440 additions and 110 deletions

View File

@@ -9,6 +9,9 @@ const appTheme = atom<any>({
primaryColor: '#222222', primaryColor: '#222222',
accentColor: '#5c5c5c', accentColor: '#5c5c5c',
borderColor: '#5c5c5c', borderColor: '#5c5c5c',
textShadowsEnabled: false,
appShadowsEnabled: false,
themeFont: null,
}); });
export { appTheme }; export { appTheme };

View File

@@ -95,6 +95,7 @@ export function useCommonHooks() {
nickname: 'Boss', nickname: 'Boss',
email: 'triggerUser@default.com', email: 'triggerUser@default.com',
physicalAddress: '1510 Polk St, Houston, TX 77002', physicalAddress: '1510 Polk St, Houston, TX 77002',
defaultFont: 'Jost',
}, },
}); });
const [currentLocation, setCurrentLocation] = useLocalStorage<Location>({ const [currentLocation, setCurrentLocation] = useLocalStorage<Location>({

View File

@@ -179,6 +179,8 @@ export default function Search({
open(`${providerURL}${subtext}`, '_self'); open(`${providerURL}${subtext}`, '_self');
}); });
} else if (providerLabel === 'Show weather') { } else if (providerLabel === 'Show weather') {
setSearchValue('');
closeDropdown();
openWeatherModal(); openWeatherModal();
} else if (providerLabel.startsWith('Launch')) { } else if (providerLabel.startsWith('Launch')) {
const url = applications?.find((app: Application) => const url = applications?.find((app: Application) =>

View File

@@ -1,11 +1,13 @@
import { import {
ActionIcon, ActionIcon,
Box,
Button, Button,
Checkbox, Checkbox,
ColorPicker, ColorPicker,
ColorSwatch, ColorSwatch,
Group, Group,
Image, Image,
Overlay,
ScrollArea, ScrollArea,
Stack, Stack,
Text, Text,
@@ -14,9 +16,11 @@ import {
UnstyledButton, UnstyledButton,
} from '@mantine/core'; } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import FontPicker from 'react-fontpicker-ts'; import FontPicker from 'react-fontpicker-ts';
import { FaClipboard, FaFont, FaGoogle } from 'react-icons/fa';
import { FaShuffle } from 'react-icons/fa6'; import { FaShuffle } from 'react-icons/fa6';
import { FiImage, FiSave, FiShuffle, FiX } from 'react-icons/fi'; import { FiImage, FiSave, FiShuffle, FiX } from 'react-icons/fi';
import { import {
@@ -27,7 +31,7 @@ import {
textInputClasses, textInputClasses,
toolTipClasses, toolTipClasses,
} from 'styles'; } from 'styles';
import { BackgroundProperties, type Theme } from 'types'; import { type Theme, ThemeProperties } from 'types';
import { ThemeModalContent } from './ThemeModalContent'; import { ThemeModalContent } from './ThemeModalContent';
export function ThemeEditor({ export function ThemeEditor({
@@ -61,31 +65,40 @@ export function ThemeEditor({
}) { }) {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(-1); const [hovered, setHovered] = useState(-1);
const { isMobile, theme, getObjectDiff, textShadowStyle } = useCommonHooks(); const { currentUser, isMobile, theme, getObjectDiff, textShadowStyle } =
useCommonHooks();
const viewportRef = useRef<HTMLDivElement>(null); const viewportRef = useRef<HTMLDivElement>(null);
const [colorId, setColorId] = useState<string>(''); const [colorId, setColorId] = useState<string>('');
const [colorValue, setColorValue] = useState('#000000'); const [colorValue, setColorValue] = useState('#000000');
const [font, setFont] = useState<Theme['themeFont']>(); const [font, setFont] = useState<Theme['themeFont']>();
const [customFontEnabled, setCustomFontEnabled] = useState<boolean>(false);
const re = /(?:\.([^.]+))?$/; const re = /(?:\.([^.]+))?$/;
const [backgroundPicker, { open: openBackgroundPicker, close: closeBackgroundPicker }] = const [backgroundPicker, { open: openBackgroundPicker, close: closeBackgroundPicker }] =
useDisclosure(false); useDisclosure(false);
const filtered = backgroundList.filter((item: string) => const filtered = backgroundList.filter((item: string) =>
item.toLowerCase().includes(query.toLowerCase()), item.toLowerCase().includes(query.toLowerCase()),
); );
useEffect(() => { useEffect(() => {
if (font) { if (font) {
setCustomFontEnabled(true);
setSelectedTheme({ setSelectedTheme({
...selectedTheme, ...selectedTheme,
themeFont: font, themeFont: font,
}); });
} else { } else {
setCustomFontEnabled(false);
setSelectedTheme({ setSelectedTheme({
...selectedTheme, ...selectedTheme,
themeFont: null, themeFont: null,
}); });
} }
}, [font]); }, [font]);
useEffect(() => {
if (selectedTheme) {
setCustomFontEnabled(selectedTheme.themeFont !== null);
setFont(selectedTheme.themeFont);
}
}, [selectedTheme]);
const items = filtered.map((item: string, index: number) => { const items = filtered.map((item: string, index: number) => {
return ( return (
<UnstyledButton <UnstyledButton
@@ -171,6 +184,7 @@ export function ThemeEditor({
} }
return color; return color;
}; };
return ( return (
<> <>
<Group p={'2rem'} w={'100%'} justify='space-between'> <Group p={'2rem'} w={'100%'} justify='space-between'>
@@ -289,10 +303,10 @@ export function ThemeEditor({
<Stack <Stack
w={isMobile ? '100%' : '28%'} w={isMobile ? '100%' : '28%'}
h={'450px'} h={'450px'}
p={'1rem'}
style={{ style={{
borderRadius: '1rem', borderRadius: '1rem',
backgroundColor: 'var(--mantine-color-accent-0)', backgroundColor: 'var(--mantine-color-accent-0)',
overflow: 'clip',
}} }}
> >
{pickerOpen && ( {pickerOpen && (
@@ -305,8 +319,7 @@ export function ThemeEditor({
textShadow: theme.textShadowsEnabled ? textShadowStyle : undefined, textShadow: theme.textShadowsEnabled ? textShadowStyle : undefined,
}} }}
> >
Change{' '} Change {ThemeProperties[colorId as keyof typeof ThemeProperties]}
{BackgroundProperties[colorId as keyof typeof BackgroundProperties]}
</Text> </Text>
{colorId === 'searchColor' && ( {colorId === 'searchColor' && (
<ColorPicker <ColorPicker
@@ -492,79 +505,200 @@ export function ThemeEditor({
)} )}
{!pickerOpen && !backgroundPicker && ( {!pickerOpen && !backgroundPicker && (
<Stack h={'100%'} justify='space-between' align='center' gap={4}> <Stack h={'100%'} justify='space-between' align='center' gap={4}>
<Stack w={'100%'} align='center' gap={4}> <Stack w={'100%'} align='center' gap={0} h={'min-content'}>
<Checkbox <Group gap={2} w={'100%'} justify='space-between' align='center'>
label='Text Shadows' <Checkbox.Card
checked={selectedTheme.textShadowsEnabled === 1} w={'calc(50% - 1px)'}
c={'var(--mantine-color-background-0)'} checked={selectedTheme.textShadowsEnabled === 1}
onChange={(e) => { c={'var(--mantine-color-background-0)'}
setSelectedTheme({ onChange={(e) => {
...selectedTheme, setSelectedTheme({
textShadowsEnabled: e.target.checked ? 1 : 0, ...selectedTheme,
}); textShadowsEnabled: e ? 1 : 0,
}} });
styles={{ }}
root: { width: '50%' }, style={{
body: { border: 'none',
width: '100%', filter: `grayscale(${selectedTheme.textShadowsEnabled === 1 ? 0 : '75%'}) brightness(${selectedTheme.textShadowsEnabled === 1 ? '1' : '1.5'})`,
display: 'flex', transition: 'all 1s ease',
justifyContent: 'space-between',
},
}}
/>
<Checkbox
label='App Shadows'
checked={selectedTheme.appShadowsEnabled === 1}
c={'var(--mantine-color-background-0)'}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
appShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
<Group w={'100%'} justify='center'>
<FontPicker
style={{ width: '75%' }}
autoLoad
defaultValue={
selectedTheme.themeFont === null ? 'Jost' : selectedTheme.themeFont
}
value={(font: string) => setFont(font)}
/>
<ActionIcon
onClick={() => {
setFont(null);
}} }}
> >
<FiX /> <Tooltip
</ActionIcon> label='Text Shadows'
className={toolTipClasses.default}
classNames={toolTipClasses}
>
<Group
w={'100%'}
justify='center'
style={{
backgroundColor: selectedTheme.backgroundColor,
}}
>
<Text
classNames={textClasses}
className={textClasses.sectionTitle}
style={{
color: selectedTheme.accentColor,
textShadow: `2px 2px ${selectedTheme.primaryColor}`,
}}
>
Shadows
</Text>
</Group>
</Tooltip>
</Checkbox.Card>
<Checkbox.Card
w={'calc(50% - 1px)'}
checked={selectedTheme.appShadowsEnabled === 1}
c={'var(--mantine-color-background-0)'}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
appShadowsEnabled: e ? 1 : 0,
});
}}
style={{
border: 'none',
filter: `grayscale(${selectedTheme.appShadowsEnabled === 1 ? 0 : '75%'}) brightness(${selectedTheme.appShadowsEnabled === 1 ? '1' : '1.5'})`,
transition: 'all 1s ease',
}}
>
<Tooltip
label='Icon Shadows'
className={toolTipClasses.default}
classNames={toolTipClasses}
>
<Group
w={'100%'}
justify='center'
align='center'
style={{
backgroundColor: selectedTheme.backgroundColor,
overflow: 'clip',
}}
>
<FaGoogle
style={{
marginTop: '12px',
marginBottom: '7px',
color: selectedTheme.primaryColor,
height: 32,
width: 32,
filter: `drop-shadow(2px 2px color-mix(in srgb, ${selectedTheme.accentColor}, transparent 30%))`,
}}
/>
</Group>
</Tooltip>
</Checkbox.Card>
</Group>
<Group w={'100%'} gap={0} justify='center'>
<Tooltip
label={`Custom Font ${customFontEnabled ? 'Enabled' : 'Disabled'}`}
className={toolTipClasses.default}
classNames={toolTipClasses}
>
<ActionIcon
color={'red'}
size={32}
onClick={() => {
setCustomFontEnabled(!customFontEnabled);
setFont(null);
}}
style={{
borderRadius: '0px',
filter: `grayscale(${customFontEnabled ? 0 : '75%'})`,
}}
>
<FaFont />
</ActionIcon>
</Tooltip>
<Box
w={'calc(100% - 64px)'}
onClick={(e) => {
if (!customFontEnabled) {
e.preventDefault();
e.stopPropagation();
}
}}
pos={'relative'}
>
{!customFontEnabled && <Overlay pos={'absolute'} />}
<FontPicker
autoLoad
defaultValue={
selectedTheme.themeFont === null
? 'Jost'
: selectedTheme.themeFont
}
value={(font: string) => {
if (customFontEnabled) {
setFont(font);
} else {
setFont(null);
}
}}
/>
</Box>
<Tooltip
label='Reset to use default'
className={toolTipClasses.default}
classNames={toolTipClasses}
>
<ActionIcon
disabled={!customFontEnabled}
color={'red'}
size={32}
onClick={() => {
setFont(null);
setCustomFontEnabled(false);
}}
style={{
borderRadius: '0px',
}}
>
<FiX />
</ActionIcon>
</Tooltip>
</Group> </Group>
</Stack> </Stack>
{JSON.stringify(defaultTheme) !== JSON.stringify(selectedTheme) && ( {JSON.stringify(defaultTheme) !== JSON.stringify(selectedTheme) && (
<> <Stack
<Text c={'var(--mantine-color-background-0)'} fz={'1.5rem'}> gap={0}
Changes: w={'95%'}
h={'calc(100% - 144px)'}
align='center'
style={{
backgroundColor: 'var(--mantine-color-background-0)',
borderRadius: 8,
}}
>
<Text
c={'var(--mantine-color-primary-0)'}
fz={'1.5rem'}
w={'100%'}
style={{
textAlign: 'center',
borderBottom: '1px solid var(--mantine-color-primary-0',
}}
>
Changes
</Text> </Text>
<ScrollArea.Autosize <ScrollArea.Autosize
w={'100%'} w={'100%'}
className={scrollAreaClasses.default} className={scrollAreaClasses.default}
classNames={scrollAreaClasses} classNames={scrollAreaClasses}
style={{ style={{
backgroundColor: 'var(--mantine-color-background-0)',
borderRadius: 8,
padding: '1rem', padding: '1rem',
}} }}
styles={{
scrollbar: {
position: 'absolute',
right: -2,
},
}}
> >
<Stack w={'100%'} gap={0}> <Stack w={'95%'} gap={2}>
{getObjectDiff(defaultTheme, designerTheme) !== null && {getObjectDiff(defaultTheme, designerTheme) !== null &&
Object.keys(getObjectDiff(defaultTheme, designerTheme)).map( Object.keys(getObjectDiff(defaultTheme, designerTheme)).map(
(key: any) => { (key: any) => {
@@ -576,22 +710,115 @@ export function ThemeEditor({
gap={0} gap={0}
key={key} key={key}
> >
<Text key={key}> <Group w={'100%'} justify='space-between'>
{ <Text
BackgroundProperties[ key={key}
key as keyof typeof BackgroundProperties pl={2}
] style={{
} color: 'var(--mantine-color-primary-0)',
</Text> backgroundColor:
'var(--mantine-color-background-0)',
}}
>
{ThemeProperties[key as keyof typeof ThemeProperties]}
</Text>
<Tooltip
label={`Reset ${
ThemeProperties[key as keyof typeof ThemeProperties]
}`}
classNames={toolTipClasses}
className={toolTipClasses.default}
>
<ActionIcon
variant='subtle'
color='red'
onClick={() => {
setSelectedTheme({
...designerTheme,
[key]: defaultTheme[key as keyof Theme],
});
if (key.includes('Font')) {
setFont(defaultTheme.themeFont);
}
}}
>
<FiX />
</ActionIcon>
</Tooltip>
</Group>
{key === 'name' && ( {key === 'name' && (
<Group w={'100%'} justify='center'> <Group
w={'100%'}
justify='center'
bg='var(--mantine-color-accent-0)'
style={{ borderRadius: 8, overflow: 'clip' }}
h={48}
>
<Text>{defaultTheme[key as keyof Theme]}</Text> <Text>{defaultTheme[key as keyof Theme]}</Text>
<Text>→</Text> <Text>→</Text>
<Text>{designerTheme[key as keyof Theme]}</Text> <Text>{designerTheme[key as keyof Theme]}</Text>
</Group> </Group>
)} )}
{key.includes('Shadow') && (
<Group
w={'100%'}
justify='center'
bg='var(--mantine-color-accent-0)'
style={{ borderRadius: 8, overflow: 'clip' }}
h={48}
>
{defaultTheme[key as keyof Theme] === 0 ? (
<Text>Off</Text>
) : (
<Text>On</Text>
)}
<Text>→</Text>
{designerTheme[key as keyof Theme] === 0 ? (
<Text>Off</Text>
) : (
<Text>On</Text>
)}
</Group>
)}
{key.includes('Font') && (
<Group
w={'100%'}
justify='center'
bg='var(--mantine-color-accent-0)'
h={48}
style={{ borderRadius: 8, overflow: 'clip' }}
>
<Text
style={{
fontFamily:
(defaultTheme[key as keyof Theme] as string) ??
undefined,
fontSize: 18,
}}
>
{defaultTheme[key as keyof Theme] ||
`Default (${currentUser.defaultFont})`}
</Text>
<Text>→</Text>
<Text
style={{
fontFamily: selectedTheme.themeFont ?? undefined,
fontSize: 18,
}}
>
{designerTheme[key as keyof Theme] ||
`Default (${currentUser.defaultFont})`}
</Text>
</Group>
)}
{key === 'backgroundImage' && ( {key === 'backgroundImage' && (
<Group w={'100%'} justify='center' gap={4}> <Group
w={'100%'}
justify='center'
gap={4}
bg='var(--mantine-color-accent-0)'
style={{ borderRadius: 8, overflow: 'clip' }}
>
<Image <Image
w={'40%'} w={'40%'}
src={ src={
@@ -616,39 +843,133 @@ export function ThemeEditor({
</Group> </Group>
)} )}
{key.includes('Color') && ( {key.includes('Color') && (
<Group w={'100%'} justify='center'> <Group
<ColorSwatch w={'100%'}
color={defaultTheme[key as keyof Theme] as string} justify='space-between'
/> bg='var(--mantine-color-accent-0)'
style={{ borderRadius: 8, overflow: 'clip' }}
>
<Tooltip
label='Copy color code'
classNames={toolTipClasses}
className={toolTipClasses.default}
>
<ColorSwatch
h={50}
onClick={() => {
navigator.clipboard.writeText(
defaultTheme[key as keyof Theme] as string,
);
notifications.show({
title: (
<Text
fw={600}
c={'var(--mantine-color-accent-0)'}
>
Copied{' '}
{
ThemeProperties[
key as keyof typeof ThemeProperties
]
}
</Text>
),
message: (
<Text c={'var(--mantine-color-primary-0)'}>
{
defaultTheme[
key as keyof Theme
] as string
}
</Text>
),
autoClose: true,
withCloseButton: true,
icon: <FaClipboard size={24} />,
});
}}
color={defaultTheme[key as keyof Theme] as string}
style={{
cursor: 'pointer',
}}
styles={{
alphaOverlay: {
borderRadius: '0px',
},
colorOverlay: {
borderRadius: '0px',
},
shadowOverlay: {
borderRadius: '0px',
},
childrenOverlay: {
borderRadius: '0px',
},
}}
/>
</Tooltip>
<Text>→</Text> <Text>→</Text>
<ColorSwatch <Tooltip
color={designerTheme[key as keyof Theme] as string} label='Copy color code'
/> classNames={toolTipClasses}
</Group> className={toolTipClasses.default}
)} >
{key.includes('Shadow') && ( <ColorSwatch
<Group w={'100%'} justify='center'> h={50}
{defaultTheme[key as keyof Theme] === 0 ? ( onClick={() => {
<Text>Off</Text> navigator.clipboard.writeText(
) : ( defaultTheme[key as keyof Theme] as string,
<Text>On</Text> );
)} notifications.show({
<Text>→</Text> title: (
{designerTheme[key as keyof Theme] === 0 ? ( <Text
<Text>Off</Text> fw={600}
) : ( c={'var(--mantine-color-accent-0)'}
<Text>On</Text> >
)} Copied{' '}
</Group> {
)} ThemeProperties[
{key.includes('Font') && ( key as keyof typeof ThemeProperties
<Group w={'100%'} justify='center'> ]
<Text> }
{defaultTheme[key as keyof Theme] || </Text>
'Default (Jost)'} ),
</Text> message: (
<Text>→</Text> <Text c={'var(--mantine-color-primary-0)'}>
<Text>{designerTheme[key as keyof Theme]}</Text> {
designerTheme[
key as keyof Theme
] as string
}
</Text>
),
autoClose: true,
withCloseButton: true,
icon: <FaClipboard size={24} />,
});
}}
color={
designerTheme[key as keyof Theme] as string
}
style={{
cursor: 'pointer',
}}
styles={{
alphaOverlay: {
borderRadius: '0px',
},
colorOverlay: {
borderRadius: '0px',
},
shadowOverlay: {
borderRadius: '0px',
},
childrenOverlay: {
borderRadius: '0px',
},
}}
/>
</Tooltip>
</Group> </Group>
)} )}
</Stack> </Stack>
@@ -657,9 +978,9 @@ export function ThemeEditor({
)} )}
</Stack> </Stack>
</ScrollArea.Autosize> </ScrollArea.Autosize>
</> </Stack>
)} )}
<Group mih={'36px'}> <Group mih={'36px'} pb={'1.5rem'}>
<Tooltip <Tooltip
label={'Randomize Colors!'} label={'Randomize Colors!'}
c={'var(--mantine-color-primary-0)'} c={'var(--mantine-color-primary-0)'}

View File

@@ -8,7 +8,7 @@ export type { PersonalData } from './personalData';
export type { Provider } from './provider'; export type { Provider } from './provider';
export type { SearchHistoryItem, SearchShortcut } from './search'; export type { SearchHistoryItem, SearchShortcut } from './search';
export type { Theme, ThemePlaylist } from './theme'; export type { Theme, ThemePlaylist } from './theme';
export { BackgroundProperties } from './theme'; export { ThemeProperties } from './theme';
export type { User } from './user'; export type { User } from './user';
export type { export type {
CurrentWeather, CurrentWeather,

View File

@@ -20,7 +20,8 @@ export interface ThemePlaylist {
themeList: string; themeList: string;
} }
export enum BackgroundProperties { export enum ThemeProperties {
name = 'Theme Name',
backgroundImage = 'Background Image', backgroundImage = 'Background Image',
backgroundColor = 'Background Color', backgroundColor = 'Background Color',
searchColor = 'Search Color', searchColor = 'Search Color',
@@ -30,4 +31,5 @@ export enum BackgroundProperties {
randomEnabled = 'Random', randomEnabled = 'Random',
textShadowsEnabled = 'Text Shadows', textShadowsEnabled = 'Text Shadows',
appShadowsEnabled = 'App Shadows', appShadowsEnabled = 'App Shadows',
themeFont = 'Theme Font',
} }

View File

@@ -4,4 +4,5 @@ export interface User {
email: string; email: string;
physicalAddress: string; physicalAddress: string;
nickname: string; nickname: string;
defaultFont: string;
} }