Compare commits

...

8 Commits

Author SHA1 Message Date
EYHN
73bbb6c9d2 improve font 2024-08-19 23:29:40 +08:00
Jimmfly
76bc48dd9c feat(core): add custom font family setting 2024-08-19 23:00:43 +08:00
Jimmfly
d28a6300d9 feat(core): add SystemFontFamilyService 2024-08-19 23:00:43 +08:00
Jimmfly
9a4cb5d69a feat(electron): add font list handlers 2024-08-19 23:00:43 +08:00
Jimmfly
3a220da1b6 feat(core): init editor setting ui 2024-08-19 23:00:43 +08:00
Jimmfly
499f986261 feat(component): add custom props to Slider 2024-08-19 23:00:42 +08:00
Jimmfly
87767df0fd refactor(component): impl with @radix-ui/react-slider 2024-08-19 23:00:42 +08:00
Jimmfly
bc77d7a648 feat(component): add slider ui component 2024-08-19 23:00:42 +08:00
37 changed files with 1733 additions and 49 deletions

View File

@@ -32,6 +32,7 @@ export const runtimeFlagsSchema = z.object({
enableInfoModal: z.boolean(),
enableOrganize: z.boolean(),
enableThemeEditor: z.boolean(),
enableEditorSettings: z.boolean(),
});
export type RuntimeConfig = z.infer<typeof runtimeFlagsSchema>;

View File

@@ -22,6 +22,7 @@ export type AppSetting = {
fullWidthLayout: boolean;
windowFrameStyle: 'frameless' | 'NativeTitleBar';
fontStyle: FontFamily;
customFontFamily: string;
dateFormat: DateFormats;
startWeekOnMonday: boolean;
enableBlurBackground: boolean;
@@ -45,12 +46,13 @@ export const dateFormatOptions: DateFormats[] = [
'dd MMMM YYYY',
];
export type FontFamily = 'Sans' | 'Serif' | 'Mono';
export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom';
export const fontStyleOptions = [
{ key: 'Sans', value: 'var(--affine-font-sans-family)' },
{ key: 'Serif', value: 'var(--affine-font-serif-family)' },
{ key: 'Mono', value: 'var(--affine-font-mono-family)' },
{ key: 'Custom', value: 'var(--affine-font-sans-family)' },
] satisfies {
key: FontFamily;
value: string;
@@ -61,6 +63,7 @@ const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
fullWidthLayout: false,
windowFrameStyle: 'frameless',
fontStyle: 'Sans',
customFontFamily: '',
dateFormat: dateFormatOptions[0],
startWeekOnMonday: false,
enableBlurBackground: true,

View File

@@ -43,6 +43,7 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toolbar": "^1.0.4",

View File

@@ -22,6 +22,7 @@ export * from './ui/popover';
export * from './ui/radio';
export * from './ui/scrollbar';
export * from './ui/skeleton';
export * from './ui/slider';
export * from './ui/switch';
export * from './ui/table';
export * from './ui/tabs';

View File

@@ -0,0 +1,57 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const trackStyle = style({
width: '100%',
height: '1px',
position: 'relative',
display: 'flex',
alignItems: 'center',
padding: '12px 0',
cursor: 'pointer',
});
export const fakeTrackStyle = style({
width: '100%',
height: '1px',
backgroundColor: cssVarV2('layer/insideBorder/border'),
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
});
export const filledTrackStyle = style({
height: '100%',
backgroundColor: cssVarV2('icon/primary'),
borderRadius: '1px',
position: 'absolute',
top: '0',
left: '0',
});
export const thumbStyle = style({
width: '8px',
height: '8px',
backgroundColor: cssVarV2('icon/primary'),
borderRadius: '50%',
position: 'absolute',
top: '50%',
transform: 'translate(-50%, -50%)',
cursor: 'pointer',
});
export const nodeStyle = style({
width: '4px',
height: '4px',
border: '2px solid transparent',
backgroundColor: cssVarV2('layer/insideBorder/border'),
borderRadius: '50%',
position: 'absolute',
top: '50%',
cursor: 'pointer',
transform: 'translate(-50%, -50%)',
selectors: {
'&[data-active="true"]': {
backgroundColor: cssVarV2('icon/primary'),
},
},
});

View File

@@ -0,0 +1 @@
export * from './slider';

View File

@@ -0,0 +1,24 @@
import type { Meta, StoryFn } from '@storybook/react';
import { useState } from 'react';
import type { SliderProps } from './index';
import { Slider } from './index';
export default {
title: 'UI/Slider',
component: Slider,
} satisfies Meta<typeof Slider>;
const Template: StoryFn<SliderProps> = args => {
const [value, setValue] = useState<number[]>([0]);
return <Slider value={value} onValueChange={setValue} {...args} />;
};
export const Default: StoryFn<SliderProps> = Template.bind(undefined);
Default.args = {
min: 0,
max: 10,
width: 500,
step: 1,
nodes: [0, 5, 10],
};

View File

@@ -0,0 +1,75 @@
import * as Sliders from '@radix-ui/react-slider';
import { useRef } from 'react';
import * as styles from './index.css';
export interface SliderProps extends Sliders.SliderProps {
width?: number;
containerStyle?: React.CSSProperties;
rootStyle?: React.CSSProperties;
trackStyle?: React.CSSProperties;
rangeStyle?: React.CSSProperties;
thumbStyle?: React.CSSProperties;
noteStyle?: React.CSSProperties;
nodes?: number[]; // The values where the nodes should be placed
}
export const Slider = ({
value,
min = 0,
max = 10,
step,
width = 250,
nodes,
containerStyle,
rootStyle,
trackStyle,
rangeStyle,
thumbStyle,
noteStyle,
...props
}: SliderProps) => {
const sliderRef = useRef<HTMLDivElement>(null);
return (
<div style={{ ...containerStyle, width: width ? `${width}px` : undefined }}>
<Sliders.Root
value={value}
min={min}
max={max}
step={step}
style={rootStyle}
{...props}
>
<Sliders.Track className={styles.trackStyle} ref={sliderRef}>
<div className={styles.fakeTrackStyle} style={trackStyle}>
<Sliders.Range
className={styles.filledTrackStyle}
style={rangeStyle}
/>
</div>
{!!nodes &&
nodes.map((nodeValue, index) => (
<div
key={index}
className={styles.nodeStyle}
data-active={value && value[0] >= nodeValue}
style={{
left: `${((nodeValue - (min !== undefined ? min : 0)) / (max !== undefined ? max - (min !== undefined ? min : 0) : 1)) * 100}%`,
transform:
index === 0
? 'translateY(-50%)'
: index === nodes.length - 1
? 'translateY(-50%) translateX(-100%)'
: undefined,
...noteStyle,
}}
/>
))}
<Sliders.Thumb className={styles.thumbStyle} style={thumbStyle} />
</Sliders.Track>
</Sliders.Root>
</div>
);
};

View File

@@ -7,7 +7,7 @@ import {
} from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import type { AppSetting } from '@toeverything/infra';
import { fontStyleOptions, windowFrameStyleOptions } from '@toeverything/infra';
import { windowFrameStyleOptions } from '@toeverything/infra';
import { useTheme } from 'next-themes';
import { useCallback, useMemo } from 'react';
@@ -58,45 +58,6 @@ export const ThemeSettings = () => {
);
};
const FontFamilySettings = () => {
const t = useI18n();
const { appSettings, updateSettings } = useAppSettingHelper();
const radioItems = useMemo(() => {
return fontStyleOptions.map(({ key, value }) => {
const label =
key === 'Mono'
? t[`com.affine.appearanceSettings.fontStyle.mono`]()
: key === 'Sans'
? t['com.affine.appearanceSettings.fontStyle.sans']()
: key === 'Serif'
? t['com.affine.appearanceSettings.fontStyle.serif']()
: '';
return {
value: key,
label,
testId: 'system-font-style-trigger',
style: { fontFamily: value },
} satisfies RadioItem;
});
}, [t]);
return (
<RadioGroup
items={radioItems}
value={appSettings.fontStyle}
width={250}
className={settingWrapper}
onChange={useCallback(
(value: AppSetting['fontStyle']) => {
updateSettings('fontStyle', value);
},
[updateSettings]
)}
/>
);
};
export const AppearanceSettings = () => {
const t = useI18n();
@@ -116,12 +77,6 @@ export const AppearanceSettings = () => {
>
<ThemeSettings />
</SettingRow>
<SettingRow
name={t['com.affine.appearanceSettings.font.title']()}
desc={t['com.affine.appearanceSettings.font.description']()}
>
<FontFamilySettings />
</SettingRow>
<SettingRow
name={t['com.affine.appearanceSettings.language.title']()}
desc={t['com.affine.appearanceSettings.language.description']()}

View File

@@ -0,0 +1,183 @@
import {
Menu,
MenuItem,
MenuTrigger,
RadioGroup,
type RadioItem,
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { useMemo, useState } from 'react';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
export const ConnecterSettings = () => {
const t = useI18n();
const [connecterStyle, setConnecterStyle] = useState<'general' | 'scribbled'>(
'general'
);
const [connectorShape, setConnectorShape] = useState<
'elbowed' | 'curve' | 'straight'
>('elbowed');
const [borderStyle, setBorderStyle] = useState<'solid' | 'dash'>('solid');
const [borderThickness, setBorderThickness] = useState<number[]>([3]);
const connecterStyleItems = useMemo<RadioItem[]>(
() => [
{
value: 'general',
label: t['com.affine.settings.editorSettings.edgeless.style.general'](),
},
{
value: 'scribbled',
label:
t['com.affine.settings.editorSettings.edgeless.style.scribbled'](),
},
],
[t]
);
const connecterShapeItems = useMemo<RadioItem[]>(
() => [
{
value: 'elbowed',
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.elbowed'
](),
},
{
value: 'curve',
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.curve'
](),
},
{
value: 'straight',
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.straight'
](),
},
],
[t]
);
const borderStyleItems = useMemo<RadioItem[]>(
() => [
{
value: 'solid',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
},
{
value: 'dash',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
},
],
[t]
);
return (
<>
<EdgelessSnapshot
title={t['com.affine.settings.editorSettings.edgeless.connecter']()}
option={['mock-option']}
type="mock-type"
/>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.color'
]()}
desc={''}
>
<Menu items={<MenuItem>Grey</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Grey
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.style']()}
desc={''}
>
<RadioGroup
items={connecterStyleItems}
value={connecterStyle}
width={250}
className={settingWrapper}
onChange={setConnecterStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape'
]()}
desc={''}
>
<RadioGroup
items={connecterShapeItems}
value={connectorShape}
width={250}
className={settingWrapper}
onChange={setConnectorShape}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.border-style'
]()}
desc={''}
>
<RadioGroup
items={borderStyleItems}
value={borderStyle}
width={250}
className={settingWrapper}
onChange={setBorderStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.border-thickness'
]()}
desc={''}
>
<Slider
value={borderThickness}
onValueChange={setBorderThickness}
min={1}
max={5}
step={1}
nodes={[1, 2, 3, 4, 5]}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.start-endpoint'
]()}
desc={''}
>
<Menu items={<MenuItem>None</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
None
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.end-endpoint'
]()}
desc={''}
>
<Menu items={<MenuItem>Arrow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Arrow
</MenuTrigger>
</Menu>
</SettingRow>
</>
);
};

View File

@@ -0,0 +1,23 @@
import { SettingWrapper } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { ConnecterSettings } from './connecter';
import { MindMapSettings } from './mind-map';
import { NoteSettings } from './note';
import { PenSettings } from './pen';
import { ShapeSettings } from './shape';
import { TextSettings } from './text';
export const Edgeless = () => {
const t = useI18n();
return (
<SettingWrapper title={t['com.affine.settings.editorSettings.edgeless']()}>
<NoteSettings />
<TextSettings />
<ShapeSettings />
<ConnecterSettings />
<PenSettings />
<MindMapSettings />
</SettingWrapper>
);
};

View File

@@ -0,0 +1 @@
export * from './edgeless';

View File

@@ -0,0 +1,79 @@
import {
Menu,
MenuItem,
MenuTrigger,
RadioGroup,
type RadioItem,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { useMemo, useState } from 'react';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
export const MindMapSettings = () => {
const t = useI18n();
const [layoutValue, setLayoutValue] = useState<'left' | 'radial' | 'right'>(
'right'
);
const layoutValueItems = useMemo<RadioItem[]>(
() => [
{
value: 'left',
label:
t[
'com.affine.settings.editorSettings.edgeless.mind-map.layout.left'
](),
},
{
value: 'radial',
label:
t[
'com.affine.settings.editorSettings.edgeless.mind-map.layout.radial'
](),
},
{
value: 'right',
label:
t[
'com.affine.settings.editorSettings.edgeless.mind-map.layout.right'
](),
},
],
[t]
);
return (
<>
<EdgelessSnapshot
title={t['com.affine.settings.editorSettings.edgeless.mind-map']()}
option={['mock-option']}
type="mock-type"
/>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.style']()}
desc={''}
>
<Menu items={<MenuItem>Style 1</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Style 1
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.mind-map.layout'
]()}
desc={''}
>
<RadioGroup
items={layoutValueItems}
value={layoutValue}
width={250}
className={settingWrapper}
onChange={setLayoutValue}
/>
</SettingRow>
</>
);
};

View File

@@ -0,0 +1,111 @@
import {
Menu,
MenuItem,
MenuTrigger,
RadioGroup,
type RadioItem,
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { useMemo, useState } from 'react';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
export const NoteSettings = () => {
const t = useI18n();
const [borderStyle, setBorderStyle] = useState<'solid' | 'dash' | 'none'>(
'solid'
);
const [borderThickness, setBorderThickness] = useState<number[]>([3]);
const borderStyleItems = useMemo<RadioItem[]>(
() => [
{
value: 'solid',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
},
{
value: 'dash',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
},
{
value: 'none',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.none'](),
},
],
[t]
);
return (
<>
<EdgelessSnapshot
title={t['com.affine.settings.editorSettings.edgeless.note']()}
option={['mock-option']}
type="mock-type"
/>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.note.background'
]()}
desc={''}
>
<Menu items={<MenuItem>Yellow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Yellow
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.note.corners']()}
desc={''}
>
<Menu items={<MenuItem>Small</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Small
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.note.shadow']()}
desc={''}
>
<Menu items={<MenuItem>Box shadow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Box shadow
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.note.border']()}
desc={''}
>
<RadioGroup
items={borderStyleItems}
value={borderStyle}
width={250}
className={settingWrapper}
onChange={setBorderStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.note.border-thickness'
]()}
desc={''}
>
<Slider
value={borderThickness}
onValueChange={setBorderThickness}
min={1}
max={5}
step={1}
nodes={[1, 2, 3, 4, 5]}
/>
</SettingRow>
</>
);
};

View File

@@ -0,0 +1,44 @@
import { Menu, MenuItem, MenuTrigger, Slider } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { useState } from 'react';
import { menuTrigger } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
export const PenSettings = () => {
const t = useI18n();
const [borderThickness, setBorderThickness] = useState<number[]>([3]);
return (
<>
<EdgelessSnapshot
title={t['com.affine.settings.editorSettings.edgeless.pen']()}
option={['mock-option']}
type="mock-type"
/>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.pen.color']()}
desc={''}
>
<Menu items={<MenuItem>Yellow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Yellow
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.pen.thickness']()}
desc={''}
>
<Slider
value={borderThickness}
onValueChange={setBorderThickness}
min={1}
max={5}
step={1}
nodes={[1, 2, 3, 4, 5]}
/>
</SettingRow>
</>
);
};

View File

@@ -0,0 +1,276 @@
import {
Menu,
MenuItem,
MenuTrigger,
RadioGroup,
type RadioItem,
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { useMemo, useState } from 'react';
import { menuTrigger, settingWrapper, shapeIndicator } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
type Shape =
| 'square'
| 'ellipse'
| 'diamond'
| 'triangle'
| 'rounded-rectangle';
export const ShapeSettings = () => {
const t = useI18n();
const [currentShape, setCurrentShape] = useState<Shape>('square');
const [shapeStyle, setShapeStyle] = useState<'general' | 'scribbled'>(
'general'
);
const [borderStyle, setBorderStyle] = useState<'solid' | 'dash' | 'none'>(
'solid'
);
const [borderThickness, setBorderThickness] = useState<number[]>([3]);
const [textAlignment, setTextAlignment] = useState<
'left' | 'center' | 'right'
>('left');
const shapeStyleItems = useMemo<RadioItem[]>(
() => [
{
value: 'general',
label: t['com.affine.settings.editorSettings.edgeless.style.general'](),
},
{
value: 'scribbled',
label:
t['com.affine.settings.editorSettings.edgeless.style.scribbled'](),
},
],
[t]
);
const borderStyleItems = useMemo<RadioItem[]>(
() => [
{
value: 'solid',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
},
{
value: 'dash',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
},
{
value: 'none',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.none'](),
},
],
[t]
);
const alignItems = useMemo<RadioItem[]>(
() => [
{
value: 'left',
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.left'
](),
},
{
value: 'center',
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.center'
](),
},
{
value: 'right',
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.right'
](),
},
],
[t]
);
const shapes = useMemo<RadioItem[]>(
() => [
{
value: 'square',
label: t['com.affine.settings.editorSettings.edgeless.shape.square'](),
},
{
value: 'ellipse',
label: t['com.affine.settings.editorSettings.edgeless.shape.ellipse'](),
},
{
value: 'diamond',
label: t['com.affine.settings.editorSettings.edgeless.shape.diamond'](),
},
{
value: 'triangle',
label:
t['com.affine.settings.editorSettings.edgeless.shape.triangle'](),
},
{
value: 'rounded-rectangle',
label:
t[
'com.affine.settings.editorSettings.edgeless.shape.rounded-rectangle'
](),
},
],
[t]
);
return (
<>
<EdgelessSnapshot
title={t['com.affine.settings.editorSettings.edgeless.shape']()}
option={['mock-option']}
type="mock-type"
/>
<RadioGroup
padding={0}
gap={4}
itemHeight={28}
borderRadius={8}
value={currentShape}
items={shapes}
onChange={setCurrentShape}
style={{ background: 'transparent', marginBottom: '16px' }}
indicatorClassName={shapeIndicator}
/>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.style']()}
desc={''}
>
<RadioGroup
items={shapeStyleItems}
value={shapeStyle}
width={250}
className={settingWrapper}
onChange={setShapeStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.fill-color'
]()}
desc={''}
>
<Menu items={<MenuItem>Yellow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Yellow
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.border-color'
]()}
desc={''}
>
<Menu items={<MenuItem>Yellow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Yellow
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.border-style'
]()}
desc={''}
>
<RadioGroup
items={borderStyleItems}
value={borderStyle}
width={250}
className={settingWrapper}
onChange={setBorderStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.border-thickness'
]()}
desc={''}
>
<Slider
value={borderThickness}
onValueChange={setBorderThickness}
min={1}
max={5}
step={1}
nodes={[1, 2, 3, 4, 5]}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.text-color'
]()}
desc={''}
>
<Menu items={<MenuItem>Yellow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Yellow
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.shape.font']()}
desc={''}
>
<Menu items={<MenuItem>Inter</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Inter
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.font-size'
]()}
desc={''}
>
<Menu items={<MenuItem>15px</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
15px
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.font-style'
]()}
desc={''}
>
<Menu items={<MenuItem>Regular</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Regular
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.text-alignment'
]()}
desc={''}
>
<RadioGroup
items={alignItems}
value={textAlignment}
width={250}
className={settingWrapper}
onChange={setTextAlignment}
/>
</SettingRow>
</>
);
};

View File

@@ -0,0 +1,24 @@
import { snapshot, snapshotContainer, snapshotTitle } from '../style.css';
export const EdgelessSnapshot = ({
title,
option,
type,
}: {
title: string;
option: string[];
type: string;
}) => {
return (
<div className={snapshotContainer}>
<div className={snapshotTitle}>{title}</div>
<div
className={snapshot}
data-editor-option={option}
data-editor-type={type}
>
Mock Preview
</div>
</div>
);
};

View File

@@ -0,0 +1,112 @@
import {
Menu,
MenuItem,
MenuTrigger,
RadioGroup,
type RadioItem,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { useMemo, useState } from 'react';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
export const TextSettings = () => {
const t = useI18n();
const [textAlignment, setTextAlignment] = useState<
'left' | 'center' | 'right'
>('left');
const alignItems = useMemo<RadioItem[]>(
() => [
{
value: 'left',
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.left'
](),
},
{
value: 'center',
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.center'
](),
},
{
value: 'right',
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.right'
](),
},
],
[t]
);
return (
<>
<EdgelessSnapshot
title={t['com.affine.settings.editorSettings.edgeless.text']()}
option={['mock-option']}
type="mock-type"
/>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.text.color']()}
desc={''}
>
<Menu items={<MenuItem>Blue</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Blue
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.text.font']()}
desc={''}
>
<Menu items={<MenuItem>Inter</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Inter
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.text.font-size']()}
desc={''}
>
<Menu items={<MenuItem>15px</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
15px
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.text.font-weight'
]()}
desc={''}
>
<Menu items={<MenuItem>Regular</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Regular
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.text.alignment']()}
desc={''}
>
<RadioGroup
items={alignItems}
value={textAlignment}
width={250}
className={settingWrapper}
onChange={setTextAlignment}
/>
</SettingRow>
</>
);
};

View File

@@ -0,0 +1,333 @@
import {
Loading,
Menu,
MenuItem,
MenuSeparator,
MenuTrigger,
RadioGroup,
type RadioItem,
Scrollable,
Switch,
} from '@affine/component';
import {
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { SystemFontFamilyService } from '@affine/core/modules/system-font-family/services/system-font-family';
import { useI18n } from '@affine/i18n';
import {
type AppSetting,
type DocMode,
type FontFamily,
fontStyleOptions,
useLiveData,
useService,
} from '@toeverything/infra';
import {
type ChangeEvent,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { menu, menuTrigger, searchInput, settingWrapper } from './style.css';
const FontFamilySettings = () => {
const t = useI18n();
const { appSettings, updateSettings } = useAppSettingHelper();
const getLabel = useCallback(
(fontKey: FontFamily) => {
switch (fontKey) {
case 'Sans':
return t['com.affine.appearanceSettings.fontStyle.sans']();
case 'Serif':
return t['com.affine.appearanceSettings.fontStyle.serif']();
case 'Mono':
return t[`com.affine.appearanceSettings.fontStyle.mono`]();
case 'Custom':
return t['com.affine.settings.editorSettings.edgeless.custom']();
default:
return '';
}
},
[t]
);
const radioItems = useMemo(() => {
return fontStyleOptions
.map(({ key, value }) => {
if (key === 'Custom' && !environment.isDesktop) {
return null;
}
const label = getLabel(key);
let fontFamily = value;
if (key === 'Custom' && appSettings.customFontFamily) {
fontFamily = `${appSettings.customFontFamily}, ${value}`;
}
return {
value: key,
label,
testId: 'system-font-style-trigger',
style: {
fontFamily,
},
} satisfies RadioItem;
})
.filter(item => item !== null);
}, [appSettings.customFontFamily, getLabel]);
return (
<RadioGroup
items={radioItems}
value={appSettings.fontStyle}
width={250}
className={settingWrapper}
onChange={useCallback(
(value: AppSetting['fontStyle']) => {
updateSettings('fontStyle', value);
},
[updateSettings]
)}
/>
);
};
const getFontFamily = (font: string) => `${font}, ${fontStyleOptions[0].value}`;
const FontMenuItems = ({ onSelect }: { onSelect: (font: string) => void }) => {
const systemFontFamily = useService(SystemFontFamilyService).systemFontFamily;
useEffect(() => {
systemFontFamily.loadFontList();
systemFontFamily.clearSearch();
}, [systemFontFamily]);
const isLoading = useLiveData(systemFontFamily.isLoading$);
const result = useLiveData(systemFontFamily.result$);
const searchText = useLiveData(systemFontFamily.searchText$);
const onInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
systemFontFamily.search(e.target.value);
},
[systemFontFamily]
);
const onInputKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation(); // avoid typeahead search built-in in the menu
},
[]
);
return (
<div>
<input
value={searchText ?? ''}
onChange={onInputChange}
onKeyDown={onInputKeyDown}
autoFocus
className={searchInput}
placeholder="Type here ..."
/>
<MenuSeparator />
{isLoading ? (
<Loading />
) : (
<Scrollable.Root style={{ height: '200px' }}>
<Scrollable.Viewport>
{result.length > 0 ? (
result.map(font => (
<FontMenuItem key={font} font={font} onSelect={onSelect} />
))
) : (
<div>not found</div>
)}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
)}
</div>
);
};
const FontMenuItem = ({
font,
onSelect,
}: {
font: string;
onSelect: (font: string) => void;
}) => {
const handleFontSelect = useCallback(() => onSelect(font), [font, onSelect]);
const fontFamily = getFontFamily(font);
return (
<MenuItem key={font} onSelect={handleFontSelect} style={{ fontFamily }}>
{font}
</MenuItem>
);
};
const CustomFontFamilySettings = () => {
const t = useI18n();
const { appSettings, updateSettings } = useAppSettingHelper();
const fontFamily = getFontFamily(appSettings.customFontFamily);
const onCustomFontFamilyChange = useCallback(
(fontFamily: string) => {
updateSettings('customFontFamily', fontFamily);
},
[updateSettings]
);
if (appSettings.fontStyle !== 'Custom' || !environment.isDesktop) {
return null;
}
return (
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.font-family.custom.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.font-family.custom.description'
]()}
>
<Menu
items={<FontMenuItems onSelect={onCustomFontFamilyChange} />}
contentOptions={{
align: 'end',
}}
>
<MenuTrigger className={menuTrigger} style={{ fontFamily }}>
{appSettings.customFontFamily || 'Select a font'}
</MenuTrigger>
</Menu>
</SettingRow>
);
};
const NewDocDefaultModeSettings = () => {
const t = useI18n();
const [value, setValue] = useState<DocMode>('page');
const radioItems = useMemo<RadioItem[]>(
() => [
{
value: 'page',
label: t['Page'](),
testId: 'page-mode-trigger',
},
{
value: 'edgeless',
label: t['Edgeless'](),
testId: 'edgeless-mode-trigger',
},
],
[t]
);
return (
<RadioGroup
items={radioItems}
value={value}
width={250}
className={settingWrapper}
onChange={setValue}
/>
);
};
export const General = () => {
const t = useI18n();
return (
<SettingWrapper title={t['com.affine.settings.editorSettings.general']()}>
<SettingRow
name={t['com.affine.settings.editorSettings.general.ai.title']()}
desc={t['com.affine.settings.editorSettings.general.ai.description']()}
>
<Switch />
</SettingRow>
<SettingRow
name={t['com.affine.appearanceSettings.font.title']()}
desc={t['com.affine.appearanceSettings.font.description']()}
>
<FontFamilySettings />
</SettingRow>
<CustomFontFamilySettings />
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.font-family.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.font-family.description'
]()}
>
<Menu items={<MenuItem>inter</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
inter
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.general.font-size.title']()}
desc={t[
'com.affine.settings.editorSettings.general.font-size.description'
]()}
>
<Menu
contentOptions={{
className: menu,
}}
items={<MenuItem>15</MenuItem>}
>
<MenuTrigger className={menuTrigger} disabled>
15
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.default-new-doc.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.default-new-doc.description'
]()}
>
<NewDocDefaultModeSettings />
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.default-code-block.language.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.default-code-block.language.description'
]()}
>
<Menu
contentOptions={{
className: menu,
}}
items={<MenuItem>Plain Text</MenuItem>}
>
<MenuTrigger className={menuTrigger} disabled>
Plain Text
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.default-code-block.wrap.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.default-code-block.wrap.description'
]()}
>
<Switch />
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.spell-check.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.spell-check.description'
]()}
>
<Switch />
</SettingRow>
</SettingWrapper>
);
};

View File

@@ -0,0 +1,24 @@
import { SettingHeader } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { Edgeless } from './edgeless';
import { General } from './general';
import { Page } from './page';
import { Preferences } from './preferences';
export const EditorSettings = () => {
const t = useI18n();
return (
<>
<SettingHeader
title={t['com.affine.settings.editorSettings.title']()}
subtitle={t['com.affine.settings.editorSettings.subtitle']()}
/>
<General />
<Page />
<Edgeless />
<Preferences />
</>
);
};

View File

@@ -0,0 +1,38 @@
import { Switch } from '@affine/component';
import {
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useI18n } from '@affine/i18n';
export const Page = () => {
const t = useI18n();
const { appSettings, updateSettings } = useAppSettingHelper();
return (
<SettingWrapper title={t['com.affine.settings.editorSettings.page']()}>
<SettingRow
name={t['com.affine.settings.editorSettings.page.full-width.title']()}
desc={t[
'com.affine.settings.editorSettings.page.full-width.description'
]()}
>
<Switch
data-testid="full-width-layout-trigger"
checked={appSettings.fullWidthLayout}
onChange={checked => updateSettings('fullWidthLayout', checked)}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.page.display-doc-info.title'
]()}
desc={t[
'com.affine.settings.editorSettings.page.display-doc-info.description'
]()}
>
<Switch />
</SettingRow>
</SettingWrapper>
);
};

View File

@@ -0,0 +1,36 @@
import { Button } from '@affine/component';
import {
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
export const Preferences = () => {
const t = useI18n();
return (
<SettingWrapper
title={t['com.affine.settings.editorSettings.preferences']()}
>
<SettingRow
name={t[
'com.affine.settings.editorSettings.preferences.export.title'
]()}
desc={t[
'com.affine.settings.editorSettings.preferences.export.description'
]()}
>
<Button>Export</Button>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.preferences.import.title'
]()}
desc={t[
'com.affine.settings.editorSettings.preferences.import.description'
]()}
>
<Button>Import</Button>
</SettingRow>
</SettingWrapper>
);
};

View File

@@ -0,0 +1,66 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const settingWrapper = style({
flexGrow: 1,
display: 'flex',
justifyContent: 'flex-end',
minWidth: '150px',
maxWidth: '250px',
});
export const menu = style({
background: 'white',
width: '250px',
maxHeight: '30vh',
overflowY: 'auto',
});
export const menuTrigger = style({
textTransform: 'capitalize',
fontWeight: 600,
width: '250px',
});
export const snapshotContainer = style({
display: 'flex',
flexDirection: 'column',
marginBottom: '24px',
});
export const snapshotTitle = style({
marginBottom: '8px',
fontSize: cssVar('fontSm'),
fontWeight: 500,
color: cssVarV2('text/secondary'),
});
export const snapshot = style({
width: '100%',
height: '180px',
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
borderRadius: '4px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const shapeIndicator = style({
boxShadow: 'none',
backgroundColor: cssVarV2('layer/background/tertiary'),
});
export const searchInput = style({
flexGrow: 1,
padding: '10px 0',
margin: '-10px 0',
border: 'none',
outline: 'none',
fontSize: cssVar('fontSm'),
fontFamily: 'inherit',
color: 'inherit',
backgroundColor: 'transparent',
'::placeholder': {
color: cssVarV2('text/placeholder'),
},
});

View File

@@ -2,6 +2,7 @@ import { UserFeatureService } from '@affine/core/modules/cloud/services/user-fea
import { useI18n } from '@affine/i18n';
import {
AppearanceIcon,
BlocksuiteIcon,
ExperimentIcon,
InformationIcon,
KeyboardIcon,
@@ -15,6 +16,7 @@ import type { GeneralSettingKey } from '../types';
import { AboutAffine } from './about';
import { AppearanceSettings } from './appearance';
import { BillingSettings } from './billing';
import { EditorSettings } from './editor';
import { ExperimentalFeatures } from './experimental-features';
import { PaymentIcon, UpgradeIcon } from './icons';
import { AFFiNEPricingPlans } from './plans';
@@ -65,6 +67,15 @@ export const useGeneralSettingList = (): GeneralSettingList => {
testId: 'about-panel-trigger',
},
];
if (runtimeConfig.enableEditorSettings) {
// add editor settings to second position
settings.splice(1, 0, {
key: 'editor',
title: t['com.affine.settings.editorSettings.title'](),
icon: BlocksuiteIcon,
testId: 'editor-panel-trigger',
});
}
if (hasPaymentFeature) {
settings.splice(3, 0, {
@@ -103,6 +114,8 @@ export const GeneralSetting = ({ generalKey }: GeneralSettingProps) => {
switch (generalKey) {
case 'shortcuts':
return <Shortcuts />;
case 'editor':
return <EditorSettings />;
case 'appearance':
return <AppearanceSettings />;
case 'about':

View File

@@ -5,6 +5,7 @@ export const GeneralSettingKeys = [
'plans',
'billing',
'experimental-features',
'editor',
] as const;
export const WorkspaceSubTabs = ['preference', 'properties'] as const;

View File

@@ -57,8 +57,13 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
option => option.key === appSettings.fontStyle
);
assertExists(fontStyle);
return fontStyle.value;
}, [appSettings.fontStyle]);
const customFontFamily = appSettings.customFontFamily;
return customFontFamily && fontStyle.key === 'Custom'
? `${customFontFamily}, ${fontStyle.value}`
: fontStyle.value;
}, [appSettings.customFontFamily, appSettings.fontStyle]);
const blockId = useRouterHash();

View File

@@ -16,6 +16,7 @@ import { configurePermissionsModule } from './permissions';
import { configureWorkspacePropertiesModule } from './properties';
import { configureQuickSearchModule } from './quicksearch';
import { configureShareDocsModule } from './share-doc';
import { configureSystemFontFamilyModule } from './system-font-family';
import { configureTagModule } from './tag';
import { configureTelemetryModule } from './telemetry';
import { configureThemeEditorModule } from './theme-editor';
@@ -41,4 +42,5 @@ export function configureCommonModules(framework: Framework) {
configureExplorerModule(framework);
configureThemeEditorModule(framework);
configureEditorModule(framework);
configureSystemFontFamilyModule(framework);
}

View File

@@ -0,0 +1,61 @@
import { apis } from '@affine/electron-api';
import {
effect,
Entity,
fromPromise,
LiveData,
mapInto,
onComplete,
onStart,
} from '@toeverything/infra';
import { exhaustMap } from 'rxjs';
export class SystemFontFamily extends Entity {
constructor() {
super();
}
readonly searchText$ = new LiveData<string | null>(null);
readonly isLoading$ = new LiveData<boolean>(false);
readonly fontList$ = new LiveData<string[]>([]);
readonly result$ = LiveData.computed(get => {
const fontList = get(this.fontList$);
const searchText = get(this.searchText$);
if (!searchText) {
return fontList;
}
const filteredFonts = fontList.filter(font =>
font.toLowerCase().includes(searchText.toLowerCase())
);
return filteredFonts;
}).throttleTime(500);
loadFontList = effect(
exhaustMap(() => {
return fromPromise(async () => {
if (!apis?.fontList) {
return [];
}
return apis.fontList.getSystemFonts();
}).pipe(
mapInto(this.fontList$),
// TODO: catchErrorInto(this.error$),
onStart(() => {
this.isLoading$.next(true);
}),
onComplete(() => {
this.isLoading$.next(false);
})
);
})
);
search(searchText: string) {
this.searchText$.next(searchText);
}
clearSearch() {
this.searchText$.next(null);
}
}

View File

@@ -0,0 +1,8 @@
import type { Framework } from '@toeverything/infra';
import { SystemFontFamily } from './entities/system-font-family';
import { SystemFontFamilyService } from './services/system-font-family';
export function configureSystemFontFamilyModule(framework: Framework) {
framework.service(SystemFontFamilyService).entity(SystemFontFamily);
}

View File

@@ -0,0 +1,8 @@
import { Service } from '@toeverything/infra';
import { SystemFontFamily } from '../entities/system-font-family';
export class SystemFontFamilyService extends Service {
public readonly systemFontFamily =
this.framework.createEntity(SystemFontFamily);
}

View File

@@ -80,6 +80,7 @@
"dependencies": {
"async-call-rpc": "^6.4.2",
"electron-updater": "^6.2.1",
"font-list": "^1.5.1",
"link-preview-js": "^3.0.5",
"yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch"
},

View File

@@ -0,0 +1,15 @@
import { getFonts } from 'font-list';
import type { NamespaceHandlers } from '../type';
export const fontListHandlers = {
getSystemFonts: async () => {
try {
const fonts = await getFonts();
return fonts;
} catch (error) {
console.error('Failed to get system fonts:', error);
return [];
}
},
} satisfies NamespaceHandlers;

View File

@@ -0,0 +1 @@
export * from './handlers';

View File

@@ -5,6 +5,7 @@ import { clipboardHandlers } from './clipboard';
import { configStorageHandlers } from './config-storage';
import { exportHandlers } from './export';
import { findInPageHandlers } from './find-in-page';
import { fontListHandlers } from './font-list';
import { getLogFilePath, logger, revealLogFile } from './logger';
import { sharedStorageHandlers } from './shared-storage';
import { uiHandlers } from './ui/handlers';
@@ -29,6 +30,7 @@ export const allHandlers = {
configStorage: configStorageHandlers,
findInPage: findInPageHandlers,
sharedStorage: sharedStorageHandlers,
fontList: fontListHandlers,
};
export const registerHandlers = () => {

View File

@@ -1216,6 +1216,94 @@
"com.affine.settings.appearance.window-frame-description": "Customise appearance of Windows Client.",
"com.affine.settings.auto-check-description": "If enabled, it will automatically check for new versions at regular intervals.",
"com.affine.settings.auto-download-description": "If enabled, new versions will be automatically downloaded to the current device.",
"com.affine.settings.editorSettings": "Editor",
"com.affine.settings.editorSettings.title": "Editor Settings",
"com.affine.settings.editorSettings.subtitle": "Configure your own editor.",
"com.affine.settings.editorSettings.general": "General",
"com.affine.settings.editorSettings.general.ai.title": "AFFiNE AI",
"com.affine.settings.editorSettings.general.ai.description": "Enable the powerful AI assistant, AFFiNE AI.",
"com.affine.settings.editorSettings.general.font-style.title": "Font style",
"com.affine.settings.editorSettings.general.font-style.description": "Choose your editors font style.",
"com.affine.settings.editorSettings.general.font-family.title": "Font family",
"com.affine.settings.editorSettings.general.font-family.description": "Choose your editors font family.",
"com.affine.settings.editorSettings.general.font-family.custom.title": "Custom font family",
"com.affine.settings.editorSettings.general.font-family.custom.description": "Customize your text experience.",
"com.affine.settings.editorSettings.general.font-size.title": "Font size",
"com.affine.settings.editorSettings.general.font-size.description": "Choose your editors base font size.",
"com.affine.settings.editorSettings.general.default-new-doc.title": "New doc default mode",
"com.affine.settings.editorSettings.general.default-new-doc.description": "Default mode for new doc.",
"com.affine.settings.editorSettings.general.default-code-block.language.title": "Code blocks default language",
"com.affine.settings.editorSettings.general.default-code-block.language.description": "Set a default programming language.",
"com.affine.settings.editorSettings.general.default-code-block.wrap.title": "Wrap code in code blocks",
"com.affine.settings.editorSettings.general.default-code-block.wrap.description": "Encapsulate code snippets for better readability.",
"com.affine.settings.editorSettings.general.spell-check.title": "Spell check",
"com.affine.settings.editorSettings.general.spell-check.description": "Automatically detect and correct spelling errors.",
"com.affine.settings.editorSettings.page": "Page",
"com.affine.settings.editorSettings.page.full-width.title": "Full width layout",
"com.affine.settings.editorSettings.page.full-width.description": "Maximise display of content within a page.",
"com.affine.settings.editorSettings.page.display-doc-info.title": "Display doc info",
"com.affine.settings.editorSettings.page.display-doc-info.description": "Display document information on the doc.",
"com.affine.settings.editorSettings.edgeless": "Edgeless",
"com.affine.settings.editorSettings.edgeless.style": "Style",
"com.affine.settings.editorSettings.edgeless.style.general": "General",
"com.affine.settings.editorSettings.edgeless.style.scribbled": "Scribbled",
"com.affine.settings.editorSettings.edgeless.custom": "Custom",
"com.affine.settings.editorSettings.edgeless.note": "Note",
"com.affine.settings.editorSettings.edgeless.note.background": "Background",
"com.affine.settings.editorSettings.edgeless.note.corners": "Corners",
"com.affine.settings.editorSettings.edgeless.note.shadow": "Shadow style",
"com.affine.settings.editorSettings.edgeless.note.border": "Border style",
"com.affine.settings.editorSettings.edgeless.note.border.solid": "Solid",
"com.affine.settings.editorSettings.edgeless.note.border.dash": "Dash",
"com.affine.settings.editorSettings.edgeless.note.border.none": "None",
"com.affine.settings.editorSettings.edgeless.note.border-thickness": "Border thickness",
"com.affine.settings.editorSettings.edgeless.text": "Text",
"com.affine.settings.editorSettings.edgeless.text.color": "Text color",
"com.affine.settings.editorSettings.edgeless.text.font": "Font",
"com.affine.settings.editorSettings.edgeless.text.font-size": "Font size",
"com.affine.settings.editorSettings.edgeless.text.font-weight": "Font weight",
"com.affine.settings.editorSettings.edgeless.text.alignment": "Alignment",
"com.affine.settings.editorSettings.edgeless.text.alignment.left": "Left",
"com.affine.settings.editorSettings.edgeless.text.alignment.center": "Center",
"com.affine.settings.editorSettings.edgeless.text.alignment.right": "Right",
"com.affine.settings.editorSettings.edgeless.shape": "Shape",
"com.affine.settings.editorSettings.edgeless.shape.square": "Square",
"com.affine.settings.editorSettings.edgeless.shape.ellipse": "Ellipse",
"com.affine.settings.editorSettings.edgeless.shape.diamond": "Diamond",
"com.affine.settings.editorSettings.edgeless.shape.triangle": "Triangle",
"com.affine.settings.editorSettings.edgeless.shape.rounded-rectangle": "Rounded Rectangle",
"com.affine.settings.editorSettings.edgeless.shape.fill-color": "Fill color",
"com.affine.settings.editorSettings.edgeless.shape.border-color": "Border color",
"com.affine.settings.editorSettings.edgeless.shape.border-style": "Border style",
"com.affine.settings.editorSettings.edgeless.shape.border-thickness": "Border thickness",
"com.affine.settings.editorSettings.edgeless.shape.text-color": "Text color",
"com.affine.settings.editorSettings.edgeless.shape.font": "Font",
"com.affine.settings.editorSettings.edgeless.shape.font-size": "Font size",
"com.affine.settings.editorSettings.edgeless.shape.font-style": "Font style",
"com.affine.settings.editorSettings.edgeless.shape.text-alignment": "Text alignment",
"com.affine.settings.editorSettings.edgeless.connecter": "Connecter",
"com.affine.settings.editorSettings.edgeless.connecter.color": "Color",
"com.affine.settings.editorSettings.edgeless.connecter.connector-shape": "Connector Shape",
"com.affine.settings.editorSettings.edgeless.connecter.connector-shape.elbowed": "Elbowed",
"com.affine.settings.editorSettings.edgeless.connecter.connector-shape.curve": "Curve",
"com.affine.settings.editorSettings.edgeless.connecter.connector-shape.straight": "Straight",
"com.affine.settings.editorSettings.edgeless.connecter.border-style": "Border style",
"com.affine.settings.editorSettings.edgeless.connecter.border-thickness": "Border thickness",
"com.affine.settings.editorSettings.edgeless.connecter.start-endpoint": "Start endpoint",
"com.affine.settings.editorSettings.edgeless.connecter.end-endpoint": "End endpoint",
"com.affine.settings.editorSettings.edgeless.pen": "Pen",
"com.affine.settings.editorSettings.edgeless.pen.color": "Color",
"com.affine.settings.editorSettings.edgeless.pen.thickness": "Thickness",
"com.affine.settings.editorSettings.edgeless.mind-map": "Mind Map",
"com.affine.settings.editorSettings.edgeless.mind-map.layout": "Layout",
"com.affine.settings.editorSettings.edgeless.mind-map.layout.left": "Left",
"com.affine.settings.editorSettings.edgeless.mind-map.layout.radial": "Radial",
"com.affine.settings.editorSettings.edgeless.mind-map.layout.right": "Right",
"com.affine.settings.editorSettings.preferences": "Preferences",
"com.affine.settings.editorSettings.preferences.export.title": "Export Settings",
"com.affine.settings.editorSettings.preferences.export.description": "You can export the entire preferences data for backup, and the exported data can be re-imported.",
"com.affine.settings.editorSettings.preferences.import.title": "Import Settings",
"com.affine.settings.editorSettings.preferences.import.description": "You can import previously exported preferences data for restoration.",
"com.affine.settings.email": "Email",
"com.affine.settings.email.action": "Change email",
"com.affine.settings.email.action.change": "Change email",

View File

@@ -28,6 +28,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enableNewSettingUnstableApi: false,
enableEnhanceShareMode: false,
enableThemeEditor: false,
enableEditorSettings: false,
};
},
get beta() {
@@ -56,6 +57,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enableInfoModal: true,
enableOrganize: true,
enableThemeEditor: true,
enableEditorSettings: true,
};
},
};

View File

@@ -310,6 +310,7 @@ __metadata:
"@radix-ui/react-popover": "npm:^1.0.7"
"@radix-ui/react-radio-group": "npm:^1.1.3"
"@radix-ui/react-scroll-area": "npm:^1.0.5"
"@radix-ui/react-slider": "npm:^1.2.0"
"@radix-ui/react-tabs": "npm:^1.1.0"
"@radix-ui/react-toast": "npm:^1.1.5"
"@radix-ui/react-toolbar": "npm:^1.0.4"
@@ -557,6 +558,7 @@ __metadata:
electron-updater: "npm:^6.2.1"
electron-window-state: "npm:^5.0.3"
esbuild: "npm:^0.23.0"
font-list: "npm:^1.5.1"
fs-extra: "npm:^11.2.0"
glob: "npm:^11.0.0"
jotai: "npm:^2.8.0"
@@ -21796,6 +21798,13 @@ __metadata:
languageName: node
linkType: hard
"font-list@npm:^1.5.1":
version: 1.5.1
resolution: "font-list@npm:1.5.1"
checksum: 10/e1500017b63e0a554348143f69d64d4f61dba8bd002456e161840e79454b67feea4bbd33b5fb1648f6f8d0415307616da14751f9289ceaabcbcd62414e0f3ff8
languageName: node
linkType: hard
"foreground-child@npm:^2.0.0":
version: 2.0.0
resolution: "foreground-child@npm:2.0.0"