-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): new theme editor poc (#7810)
- Loading branch information
Showing
30 changed files
with
1,025 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
...e/src/components/affine/setting-modal/general-setting/appearance/theme-editor-setting.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | ||
} |
65 changes: 65 additions & 0 deletions
65
packages/frontend/core/src/modules/theme-editor/services/theme-editor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
}; |
73 changes: 73 additions & 0 deletions
73
packages/frontend/core/src/modules/theme-editor/views/components/color-cell.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
11 changes: 11 additions & 0 deletions
11
packages/frontend/core/src/modules/theme-editor/views/components/empty.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
19 changes: 19 additions & 0 deletions
19
packages/frontend/core/src/modules/theme-editor/views/components/simple-color-picker.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); |
34 changes: 34 additions & 0 deletions
34
packages/frontend/core/src/modules/theme-editor/views/components/simple-color-picker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
9 changes: 9 additions & 0 deletions
9
packages/frontend/core/src/modules/theme-editor/views/components/string-cell.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}); |
36 changes: 36 additions & 0 deletions
36
packages/frontend/core/src/modules/theme-editor/views/components/string-cell.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.