feat: add mindmap and connector settings (#8198)

### What changed?
- Add `connector` label settings.
- Add `mindmap` style settings.
- Add skeleton loading placeholder.

<div class='graphite__hidden'>
          <div>🎥 Video uploaded on Graphite:</div>
            <a href="https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/31159d74-ef62-4c7f-b1d9-cde73047cf29.mov">
              <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/sJGviKxfE3Ap685cl5bj/31159d74-ef62-4c7f-b1d9-cde73047cf29.mov">
            </a>
          </div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/31159d74-ef62-4c7f-b1d9-cde73047cf29.mov">录屏2024-09-11 16.30.17.mov</video>
This commit is contained in:
akumatus
2024-09-11 09:17:11 +00:00
parent 85aa73bcf6
commit f12655655e
8 changed files with 420 additions and 75 deletions

View File

@@ -10,10 +10,15 @@ import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { useI18n } from '@affine/i18n';
import {
ConnectorMode,
FontFamily,
FontFamilyMap,
FontStyle,
FontWeightMap,
LineColor,
LineColorMap,
PointStyle,
StrokeStyle,
TextAlign,
} from '@blocksuite/blocks';
import type { Doc } from '@blocksuite/store';
import { useFramework, useLiveData } from '@toeverything/infra';
@@ -21,7 +26,7 @@ import { useCallback, useMemo } from 'react';
import { DropdownMenu } from '../menu';
import { menuTrigger, settingWrapper } from '../style.css';
import { useColor } from '../utils';
import { sortedFontWeightEntries, useColor } from '../utils';
import { Point } from './point';
import { EdgelessSnapshot } from './snapshot';
import { getSurfaceBlock } from './utils';
@@ -31,6 +36,15 @@ enum ConnecterStyle {
Scribbled = 'scribbled',
}
enum ConnectorTextFontSize {
'16px' = '16',
'20px' = '20',
'24px' = '24',
'32px' = '32',
'40px' = '40',
'64px' = '64',
}
export const ConnectorSettings = () => {
const t = useI18n();
const framework = useFramework();
@@ -191,6 +205,150 @@ export const ConnectorSettings = () => {
});
}, [editorSetting, settings]);
const alignItems = useMemo<RadioItem[]>(
() => [
{
value: TextAlign.Left,
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.left'
](),
},
{
value: TextAlign.Center,
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.center'
](),
},
{
value: TextAlign.Right,
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.right'
](),
},
],
[t]
);
const textAlignment = settings.connector.labelStyle.textAlign;
const setTextAlignment = useCallback(
(value: TextAlign) => {
editorSetting.set('connector', {
labelStyle: {
textAlign: value,
},
});
},
[editorSetting]
);
const fontFamilyItems = useMemo(() => {
const { fontFamily } = settings.connector.labelStyle;
return Object.entries(FontFamily).map(([name, value]) => {
const handler = () => {
editorSetting.set('connector', {
labelStyle: {
fontFamily: value,
},
});
};
const isSelected = fontFamily === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const fontStyleItems = useMemo(() => {
const { fontStyle } = settings.connector.labelStyle;
return Object.entries(FontStyle).map(([name, value]) => {
const handler = () => {
editorSetting.set('connector', {
labelStyle: {
fontStyle: value,
},
});
};
const isSelected = fontStyle === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const fontWeightItems = useMemo(() => {
const { fontWeight } = settings.connector.labelStyle;
return sortedFontWeightEntries.map(([name, value]) => {
const handler = () => {
editorSetting.set('connector', {
labelStyle: {
fontWeight: value,
},
});
};
const isSelected = fontWeight === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const fontSizeItems = useMemo(() => {
const { fontSize } = settings.connector.labelStyle;
return Object.entries(ConnectorTextFontSize).map(([name, value]) => {
const handler = () => {
editorSetting.set('connector', {
labelStyle: {
fontSize: Number(value),
},
});
};
const isSelected = fontSize === Number(value);
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const textColorItems = useMemo(() => {
const { color } = settings.connector.labelStyle;
return Object.entries(LineColor).map(([name, value]) => {
const handler = () => {
editorSetting.set('connector', {
labelStyle: {
color: value,
},
});
};
const isSelected = color === value;
return (
<MenuItem
key={name}
onSelect={handler}
selected={isSelected}
prefix={<Point color={value} />}
>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const textColor = useMemo(() => {
const { color } = settings.connector.labelStyle;
return getColorFromMap(color, LineColorMap);
}, [getColorFromMap, settings]);
const getElements = useCallback((doc: Doc) => {
const surface = getSurfaceBlock(doc);
return surface?.getElementsByType('connector') || [];
@@ -309,6 +467,100 @@ export const ConnectorSettings = () => {
}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.text-color'
]()}
desc={''}
>
{textColor ? (
<DropdownMenu
items={textColorItems}
trigger={
<MenuTrigger
className={menuTrigger}
prefix={<Point color={textColor.value} />}
>
{textColor.key}
</MenuTrigger>
}
/>
) : null}
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.text.font-family'
]()}
desc={''}
>
<DropdownMenu
items={fontFamilyItems}
trigger={
<MenuTrigger className={menuTrigger}>
{FontFamilyMap[settings.connector.labelStyle.fontFamily]}
</MenuTrigger>
}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.shape.font-size'
]()}
desc={''}
>
<DropdownMenu
items={fontSizeItems}
trigger={
<MenuTrigger className={menuTrigger}>
{settings.connector.labelStyle.fontSize + 'px'}
</MenuTrigger>
}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.text.font-style'
]()}
desc={''}
>
<DropdownMenu
items={fontStyleItems}
trigger={
<MenuTrigger className={menuTrigger}>
{settings.connector.labelStyle.fontStyle}
</MenuTrigger>
}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.text.font-weight'
]()}
desc={''}
>
<DropdownMenu
items={fontWeightItems}
trigger={
<MenuTrigger className={menuTrigger}>
{FontWeightMap[settings.connector.labelStyle.fontWeight]}
</MenuTrigger>
}
/>
</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

@@ -1,14 +1,14 @@
{
"type": "page",
"meta": {
"id": "v4nxnq2Al2",
"id": "gJEFfmo4QJ",
"title": "",
"createDate": 1725459565290,
"createDate": 1726038448921,
"tags": []
},
"blocks": {
"type": "block",
"id": "eDFE5HokPO",
"id": "orzKfiHevj",
"flavour": "affine:page",
"version": 2,
"props": {
@@ -20,37 +20,58 @@
"children": [
{
"type": "block",
"id": "ZksVPLRI0K",
"id": "-48EmppaxI",
"flavour": "affine:surface",
"version": 5,
"props": {
"elements": {
"hdqh5vFnzj": {
"m_PwyyI76y": {
"index": "a0",
"seed": 263456687,
"seed": 44330892,
"frontEndpointStyle": "None",
"labelOffset": {
"distance": 0.5,
"anchor": "center"
},
"labelStyle": {
"color": "--affine-palette-line-black",
"fontSize": 16,
"fontFamily": "blocksuite:surface:Inter",
"fontWeight": "400",
"fontStyle": "normal",
"textAlign": "center"
},
"labelXYWH": [235.6484375, 65.23828125, 200, 20],
"mode": 2,
"rearEndpointStyle": "Arrow",
"rough": false,
"roughness": 1.4,
"source": {
"position": [196.0625, 145.84765625]
"position": [120.8515625, 146.44921875]
},
"stroke": "--affine-palette-line-grey",
"strokeStyle": "solid",
"strokeWidth": 2,
"target": {
"position": [418.5859375, 52.13671875]
"position": [387.4453125, 4.02734375]
},
"text": {
"affine:surface:text": true,
"delta": [
{
"insert": "label"
}
]
},
"type": "connector",
"id": "hdqh5vFnzj"
"id": "m_PwyyI76y"
}
}
},
"children": [
{
"type": "block",
"id": "TNgzGwq6Ct",
"id": "-6UhNH7qhy",
"flavour": "affine:frame",
"version": 1,
"props": {
@@ -63,10 +84,10 @@
]
},
"background": "--affine-palette-transparent",
"xywh": "[-13.38671875,-4.5625,739.3828125,192.51171875]",
"index": "a0",
"xywh": "[-12.04296875,-49.66796875,542.9765625,248.3984375]",
"index": "Zz",
"childElementIds": {
"hdqh5vFnzj": true
"m_PwyyI76y": true
}
},
"children": []

View File

@@ -69,13 +69,14 @@ export async function getDocByName(name: DocName) {
if (docMap.get(name)) {
return docMap.get(name);
}
const snapshot = (await loaders[name]()) as DocSnapshot;
const promiseDoc = initDocFromSnapshot(snapshot);
docMap.set(name, promiseDoc);
return promiseDoc;
const promise = initDoc(name);
docMap.set(name, promise);
return promise;
}
async function initDocFromSnapshot(snapshot: DocSnapshot) {
async function initDoc(name: DocName) {
const snapshot = (await loaders[name]()) as DocSnapshot;
const collection = await getCollection();
const job = new Job({
collection,

View File

@@ -1,61 +1,65 @@
{
"type": "page",
"meta": {
"id": "0P4XpxtY1T",
"title": "",
"createDate": 1725500529462,
"id": "_JUmoI_F28",
"title": "BlockSuite Playground",
"createDate": 1725610677620,
"tags": []
},
"blocks": {
"type": "block",
"id": "o_kDMq1Y2z",
"id": "FnaWN8Zm2_",
"flavour": "affine:page",
"version": 2,
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": []
"delta": [
{
"insert": "BlockSuite Playground"
}
]
}
},
"children": [
{
"type": "block",
"id": "CX3YRdnn7u",
"id": "hevWf4ccWc",
"flavour": "affine:surface",
"version": 5,
"props": {
"elements": {
"uXwcJ22j4U": {
"index": "a1",
"seed": 521352102,
"MYyOCcLTWN": {
"index": "a0",
"seed": 739933670,
"children": {
"affine:surface:ymap": true,
"json": {
"5Jk9NbvuPN": {
"BvanBL7O38": {
"index": "a0"
},
"DqF847301v": {
"QkmFfps45U": {
"index": "a0",
"parent": "5Jk9NbvuPN"
"parent": "BvanBL7O38"
},
"3cpFVUO7z_": {
"1IN8YOdsCP": {
"index": "a1",
"parent": "5Jk9NbvuPN"
"parent": "BvanBL7O38"
},
"cq1V7jb9Nw": {
"iFQ9oVR0KN": {
"index": "a2",
"parent": "5Jk9NbvuPN"
"parent": "BvanBL7O38"
}
}
},
"layoutType": 0,
"style": 1,
"type": "mindmap",
"id": "uXwcJ22j4U"
"id": "MYyOCcLTWN"
},
"5Jk9NbvuPN": {
"BvanBL7O38": {
"index": "a0",
"seed": 2040865434,
"seed": 819595867,
"color": "--affine-black",
"fillColor": "--affine-white",
"filled": true,
@@ -87,13 +91,13 @@
]
},
"textResizing": 0,
"xywh": "[219.1787109375,-137.21072387695312,144.7799530029297,52]",
"xywh": "[-214.85955810546875,-113.65847778320312,144.7799530029297,52]",
"type": "shape",
"id": "5Jk9NbvuPN"
"id": "BvanBL7O38"
},
"DqF847301v": {
"QkmFfps45U": {
"index": "a0",
"seed": 42391752,
"seed": 557026939,
"color": "--affine-black",
"fillColor": "--affine-white",
"filled": true,
@@ -125,13 +129,13 @@
]
},
"textResizing": 0,
"xywh": "[563.9586639404297,-210.21072387695312,76.83197021484375,36]",
"xywh": "[129.92039489746094,-186.65847778320312,76.83197021484375,36]",
"type": "shape",
"id": "DqF847301v"
"id": "QkmFfps45U"
},
"3cpFVUO7z_": {
"1IN8YOdsCP": {
"index": "a0",
"seed": 1821565231,
"seed": 205695803,
"color": "--affine-black",
"fillColor": "--affine-white",
"filled": true,
@@ -163,13 +167,13 @@
]
},
"textResizing": 0,
"xywh": "[563.9586639404297,-129.21072387695312,76.83197021484375,36]",
"xywh": "[129.92039489746094,-105.65847778320312,76.83197021484375,36]",
"type": "shape",
"id": "3cpFVUO7z_"
"id": "1IN8YOdsCP"
},
"cq1V7jb9Nw": {
"iFQ9oVR0KN": {
"index": "a0",
"seed": 1835053830,
"seed": 585656351,
"color": "--affine-black",
"fillColor": "--affine-white",
"filled": true,
@@ -201,16 +205,16 @@
]
},
"textResizing": 0,
"xywh": "[563.9586639404297,-48.210723876953125,76.83197021484375,36]",
"xywh": "[129.92039489746094,-24.658477783203125,76.83197021484375,36]",
"type": "shape",
"id": "cq1V7jb9Nw"
"id": "iFQ9oVR0KN"
}
}
},
"children": [
{
"type": "block",
"id": "yUWjMW5rEZ",
"id": "bVYX1z2q3T",
"flavour": "affine:frame",
"version": 1,
"props": {
@@ -223,10 +227,10 @@
]
},
"background": "--affine-palette-transparent",
"xywh": "[138.3125,-266.609375,602.640625,321.26171875]",
"index": "a0",
"xywh": "[-542.4695816040039,-292.0061340332031,798.42578125,414.50390625]",
"index": "Zz",
"childElementIds": {
"uXwcJ22j4U": true
"MYyOCcLTWN": true
}
},
"children": []

View File

@@ -5,38 +5,70 @@ import {
type RadioItem,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { useI18n } from '@affine/i18n';
import { LayoutType, MindmapStyle } from '@blocksuite/blocks';
import type { Doc } from '@blocksuite/store';
import { useCallback, useMemo, useState } from 'react';
import { useFramework, useLiveData } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { DropdownMenu } from '../menu';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
import { getSurfaceBlock } from './utils';
const MINDMAP_STYLES = [
{
value: MindmapStyle.ONE,
name: 'Style 1',
},
{
value: MindmapStyle.TWO,
name: 'Style 2',
},
{
value: MindmapStyle.THREE,
name: 'Style 3',
},
{
value: MindmapStyle.FOUR,
name: 'Style 4',
},
];
export const MindMapSettings = () => {
const t = useI18n();
const [layoutValue, setLayoutValue] = useState<'left' | 'radial' | 'right'>(
'right'
const framework = useFramework();
const { editorSetting } = framework.get(EditorSettingService);
const settings = useLiveData(editorSetting.settings$);
const { layoutType } = settings.mindmap;
const setLayoutType = useCallback(
(value: LayoutType) => {
editorSetting.set('mindmap', {
layoutType: value,
});
},
[editorSetting]
);
const layoutValueItems = useMemo<RadioItem[]>(
const layoutTypeItems = useMemo<RadioItem[]>(
() => [
{
value: 'left',
value: LayoutType.LEFT as any,
label:
t[
'com.affine.settings.editorSettings.edgeless.mind-map.layout.left'
](),
},
{
value: 'radial',
value: LayoutType.BALANCE as any,
label:
t[
'com.affine.settings.editorSettings.edgeless.mind-map.layout.radial'
](),
},
{
value: 'right',
value: LayoutType.RIGHT as any,
label:
t[
'com.affine.settings.editorSettings.edgeless.mind-map.layout.right'
@@ -46,6 +78,21 @@ export const MindMapSettings = () => {
[t]
);
const styleItems = useMemo(() => {
const { style } = settings.mindmap;
return MINDMAP_STYLES.map(({ name, value }) => {
const handler = () => {
editorSetting.set('mindmap', { style: value });
};
const isSelected = style === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const getElements = useCallback((doc: Doc) => {
const surface = getSurfaceBlock(doc);
return surface?.getElementsByType('mindmap') || [];
@@ -65,10 +112,10 @@ export const MindMapSettings = () => {
desc={''}
>
<DropdownMenu
items={<MenuItem>Style 1</MenuItem>}
items={styleItems}
trigger={
<MenuTrigger className={menuTrigger} disabled>
Style 1
<MenuTrigger className={menuTrigger}>
{`Style ${settings.mindmap.style}`}
</MenuTrigger>
}
/>
@@ -80,11 +127,11 @@ export const MindMapSettings = () => {
desc={''}
>
<RadioGroup
items={layoutValueItems}
value={layoutValue}
items={layoutTypeItems}
value={layoutType}
width={250}
className={settingWrapper}
onChange={setLayoutValue}
onChange={setLayoutType}
/>
</SettingRow>
</>

View File

@@ -47,10 +47,12 @@ import { EdgelessSnapshot } from './snapshot';
import { getSurfaceBlock } from './utils';
enum ShapeTextFontSize {
'12px' = '12',
'16px' = '16',
'20px' = '20',
'28px' = '28',
'36px' = '36',
'24px' = '24',
'32px' = '32',
'40px' = '40',
'64px' = '64',
}
const ShapeFillColorMap = createEnumMap(ShapeFillColor);
@@ -557,7 +559,7 @@ export const ShapeSettings = () => {
items={fontStyleItems}
trigger={
<MenuTrigger className={menuTrigger}>
{String(settings[`shape:${currentShape}`].fontStyle)}
{settings[`shape:${currentShape}`].fontStyle}
</MenuTrigger>
}
/>

View File

@@ -1,3 +1,4 @@
import { Skeleton } from '@affine/component';
import type { EditorSettingSchema } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import type { EditorHost } from '@blocksuite/block-std';
@@ -12,7 +13,12 @@ import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useRef } from 'react';
import { map, pairwise } from 'rxjs';
import { snapshotContainer, snapshotLabel, snapshotTitle } from '../style.css';
import {
snapshotContainer,
snapshotLabel,
snapshotSkeleton,
snapshotTitle,
} from '../style.css';
import { type DocName, getDocByName } from './docs';
import { getFrameBlock } from './utils';
@@ -136,7 +142,13 @@ export const EdgelessSnapshot = (props: Props) => {
overflow: 'hidden',
height,
}}
></div>
>
<Skeleton
className={snapshotSkeleton}
variant="rounded"
height={'100%'}
/>
</div>
{children}
</div>
);

View File

@@ -43,6 +43,12 @@ export const snapshotTitle = style({
color: cssVarV2('text/secondary'),
});
export const snapshotSkeleton = style({
position: 'absolute',
top: 0,
left: 0,
});
export const snapshot = style({
width: '100%',
height: '180px',