Skip to content

Commit

Permalink
feat(core): new theme editor poc (#7810)
Browse files Browse the repository at this point in the history
  • Loading branch information
CatsJuice committed Aug 12, 2024
1 parent 75e02bb commit 6228b27
Show file tree
Hide file tree
Showing 30 changed files with 1,025 additions and 7 deletions.
1 change: 1 addition & 0 deletions packages/common/env/src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const runtimeFlagsSchema = z.object({
enableExperimentalFeature: z.boolean(),
enableInfoModal: z.boolean(),
enableOrganize: z.boolean(),
enableThemeEditor: z.boolean(),
});

export type RuntimeConfig = z.infer<typeof runtimeFlagsSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useAppSettingHelper } from '../../../../../hooks/affine/use-app-setting
import { LanguageMenu } from '../../../language-menu';
import { DateFormatSetting } from './date-format-setting';
import { settingWrapper } from './style.css';
import { ThemeEditorSetting } from './theme-editor-setting';

export const ThemeSettings = () => {
const t = useI18n();
Expand Down Expand Up @@ -172,6 +173,7 @@ export const AppearanceSettings = () => {
/>
</SettingRow>
) : null}
{runtimeConfig.enableThemeEditor ? <ThemeEditorSetting /> : null}
</SettingWrapper>
{runtimeConfig.enableNewSettingUnstableApi ? (
<SettingWrapper title={t['com.affine.appearanceSettings.date.title']()}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Button } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { ThemeEditorService } from '@affine/core/modules/theme-editor';
import { popupWindow } from '@affine/core/utils';
import { apis } from '@affine/electron-api';
import { DeleteIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useCallback } from 'react';

export const ThemeEditorSetting = () => {
const themeEditor = useService(ThemeEditorService);
const modified = useLiveData(themeEditor.modified$);

const open = useCallback(() => {
if (environment.isDesktop) {
apis?.ui.openThemeEditor().catch(console.error);
} else {
popupWindow('/theme-editor');
}
}, []);

return (
<SettingRow
name="Customize Theme"
desc="Edit all AFFiNE theme variables here"
>
<div style={{ display: 'flex', gap: 16 }}>
{modified ? (
<Button
style={{
color: cssVar('errorColor'),
borderColor: cssVar('errorColor'),
}}
prefixStyle={{
color: cssVar('errorColor'),
}}
onClick={() => themeEditor.reset()}
variant="secondary"
prefix={<DeleteIcon />}
>
Reset all
</Button>
) : null}
<Button onClick={open}>Open Theme Editor</Button>
</div>
</SettingRow>
);
};
2 changes: 2 additions & 0 deletions packages/frontend/core/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { configureQuickSearchModule } from './quicksearch';
import { configureShareDocsModule } from './share-doc';
import { configureTagModule } from './tag';
import { configureTelemetryModule } from './telemetry';
import { configureThemeEditorModule } from './theme-editor';

export function configureCommonModules(framework: Framework) {
configureInfraModules(framework);
Expand All @@ -37,4 +38,5 @@ export function configureCommonModules(framework: Framework) {
configureOrganizeModule(framework);
configureFavoriteModule(framework);
configureExplorerModule(framework);
configureThemeEditorModule(framework);
}
11 changes: 11 additions & 0 deletions packages/frontend/core/src/modules/theme-editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type Framework, GlobalState } from '@toeverything/infra';

import { ThemeEditorService } from './services/theme-editor';

export { CustomThemeModifier, useCustomTheme } from './views/custom-theme';
export { ThemeEditor } from './views/theme-editor';
export { ThemeEditorService };

export function configureThemeEditorModule(framework: Framework) {
framework.service(ThemeEditorService, [GlobalState]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { GlobalState } from '@toeverything/infra';
import { LiveData, Service } from '@toeverything/infra';
import { map } from 'rxjs';

import type { CustomTheme } from '../types';

export class ThemeEditorService extends Service {
constructor(public readonly globalState: GlobalState) {
super();
}

private readonly _key = 'custom-theme';

customTheme$ = LiveData.from<CustomTheme | undefined>(
this.globalState.watch<CustomTheme>(this._key).pipe(
map(value => {
if (!value) return { light: {}, dark: {} };
if (!value.light) value.light = {};
if (!value.dark) value.dark = {};
const removeEmpty = (obj: Record<string, string>) =>
Object.fromEntries(Object.entries(obj).filter(([, v]) => v));
return {
light: removeEmpty(value.light),
dark: removeEmpty(value.dark),
};
})
),
{ light: {}, dark: {} }
);

modified$ = LiveData.computed(get => {
const theme = get(this.customTheme$);
const isEmptyObj = (obj: Record<string, string>) =>
Object.keys(obj).length === 0;
return theme && !(isEmptyObj(theme.light) && isEmptyObj(theme.dark));
});

reset() {
this.globalState.set(this._key, { light: {}, dark: {} });
}

setCustomTheme(theme: CustomTheme) {
this.globalState.set(this._key, theme);
}

updateCustomTheme(mode: 'light' | 'dark', key: string, value?: string) {
const prev: CustomTheme = this.globalState.get(this._key) ?? {
light: {},
dark: {},
};
const next = {
...prev,
[mode]: {
...prev[mode],
[key]: value,
},
};

if (!value) {
delete next[mode][key];
}

this.globalState.set(this._key, next);
}
}
4 changes: 4 additions & 0 deletions packages/frontend/core/src/modules/theme-editor/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type CustomTheme = {
light: Record<string, string>;
dark: Record<string, string>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { IconButton, Input, Menu, MenuItem } from '@affine/component';
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import { useCallback, useState } from 'react';

import * as styles from '../theme-editor.css';
import { SimpleColorPicker } from './simple-color-picker';

export const ColorCell = ({
value,
custom,
onValueChange,
}: {
value: string;
custom?: string;
onValueChange?: (color?: string) => void;
}) => {
const [inputValue, setInputValue] = useState(value);

const onInput = useCallback(
(color: string) => {
onValueChange?.(color);
setInputValue(color);
},
[onValueChange]
);
return (
<div className={styles.colorCell}>
<div>
<div data-override={!!custom} className={styles.colorCellRow}>
<div
className={styles.colorCellColor}
style={{ backgroundColor: value }}
/>
<div className={styles.colorCellValue}>{value}</div>
</div>

<div data-empty={!custom} data-custom className={styles.colorCellRow}>
<div
className={styles.colorCellColor}
style={{ backgroundColor: custom }}
/>
<div className={styles.colorCellValue}>{custom}</div>
</div>
</div>

<Menu
contentOptions={{ style: { background: cssVar('white') } }}
items={
<ul style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<SimpleColorPicker
value={inputValue}
setValue={onInput}
className={styles.colorCellInput}
/>
<Input
value={inputValue}
onChange={onInput}
placeholder="Input color"
/>
{custom ? (
<MenuItem type="danger" onClick={() => onValueChange?.()}>
Recover
</MenuItem>
) : null}
</ul>
}
>
<IconButton size="14" icon={<MoreHorizontalIcon />} />
</Menu>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Empty } from '@affine/component';

export const ThemeEmpty = () => {
return (
<div
style={{ width: 0, flex: 1, display: 'flex', justifyContent: 'center' }}
>
<Empty description="Select a variable to edit" />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { style } from '@vanilla-extract/css';

export const wrapper = style({
position: 'relative',
borderRadius: 8,
overflow: 'hidden',
border: `1px solid rgba(125,125,125, 0.3)`,
cursor: 'pointer',
});

export const input = style({
position: 'absolute',
pointerEvents: 'none',
width: '100%',
height: '100%',
top: 0,
left: 0,
opacity: 0,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import clsx from 'clsx';
import { type HTMLAttributes, useId } from 'react';

import * as styles from './simple-color-picker.css';

export const SimpleColorPicker = ({
value,
setValue,
className,
...attrs
}: HTMLAttributes<HTMLDivElement> & {
value: string;
setValue: (value: string) => void;
}) => {
const id = useId();
return (
<label htmlFor={id}>
<div
style={{ backgroundColor: value }}
className={clsx(styles.wrapper, className)}
{...attrs}
>
<input
className={styles.input}
type="color"
name={id}
id={id}
value={value}
onChange={e => setValue(e.target.value)}
/>
</div>
</label>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';

export const row = style({
background: 'rgba(125,125,125,0.1)',
borderRadius: 4,
fontSize: cssVar('fontXs'),
padding: '4px 8px',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Input } from '@affine/component';
import { useCallback, useState } from 'react';

import * as styles from './string-cell.css';

export const StringCell = ({
value,
custom,
onValueChange,
}: {
value: string;
custom?: string;
onValueChange?: (color?: string) => void;
}) => {
const [inputValue, setInputValue] = useState(custom ?? '');

const onInput = useCallback(
(color: string) => {
onValueChange?.(color || undefined);
setInputValue(color);
},
[onValueChange]
);

return (
<div style={{ display: 'flex', gap: 8, flexDirection: 'column' }}>
<div className={styles.row}>{value}</div>
<Input
placeholder="Input value to override"
style={{ width: '100%' }}
value={inputValue}
onChange={onInput}
/>
</div>
);
};
Loading

0 comments on commit 6228b27

Please sign in to comment.