refactor(editor): move shape toolbar config and components to its package (#11082)
This commit is contained in:
@@ -1,383 +1,72 @@
|
|||||||
|
import { shapeToolbarConfig } from '@blocksuite/affine-gfx-shape';
|
||||||
import {
|
import {
|
||||||
EdgelessCRUDIdentifier,
|
|
||||||
normalizeShapeBound,
|
|
||||||
} from '@blocksuite/affine-block-surface';
|
|
||||||
import {
|
|
||||||
packColor,
|
|
||||||
type PickColorEvent,
|
|
||||||
} from '@blocksuite/affine-components/color-picker';
|
|
||||||
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
|
|
||||||
import {
|
|
||||||
mountShapeTextEditor,
|
|
||||||
ShapeComponentConfig,
|
|
||||||
type ShapeToolOption,
|
|
||||||
} from '@blocksuite/affine-gfx-shape';
|
|
||||||
import {
|
|
||||||
type Color,
|
|
||||||
DefaultTheme,
|
|
||||||
FontFamily,
|
|
||||||
getShapeName,
|
|
||||||
getShapeRadius,
|
|
||||||
getShapeType,
|
|
||||||
isTransparent,
|
|
||||||
LineWidth,
|
|
||||||
MindmapElementModel,
|
MindmapElementModel,
|
||||||
resolveColor,
|
|
||||||
ShapeElementModel,
|
ShapeElementModel,
|
||||||
type ShapeName,
|
|
||||||
ShapeStyle,
|
|
||||||
ShapeType,
|
|
||||||
StrokeStyle,
|
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import type {
|
import type { ToolbarActions } from '@blocksuite/affine-shared/services';
|
||||||
ToolbarGenericAction,
|
|
||||||
ToolbarModuleConfig,
|
|
||||||
} from '@blocksuite/affine-shared/services';
|
|
||||||
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
|
|
||||||
import {
|
|
||||||
createTextActions,
|
|
||||||
getRootBlock,
|
|
||||||
LINE_STYLE_LIST,
|
|
||||||
renderMenu,
|
|
||||||
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
|
||||||
import { AddTextIcon, ShapeIcon } from '@blocksuite/icons/lit';
|
|
||||||
import { html } from 'lit';
|
|
||||||
import isEqual from 'lodash-es/isEqual';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createMindmapLayoutActionMenu,
|
createMindmapLayoutActionMenu,
|
||||||
createMindmapStyleActionMenu,
|
createMindmapStyleActionMenu,
|
||||||
} from './mindmap';
|
} from './mindmap';
|
||||||
|
|
||||||
|
const mindmapActions = [
|
||||||
|
{
|
||||||
|
id: 'a.mindmap-style',
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
return models.some(hasGrouped);
|
||||||
|
},
|
||||||
|
content(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
if (!models.length) return null;
|
||||||
|
|
||||||
|
let mindmaps = models
|
||||||
|
.map(model => model.group)
|
||||||
|
.filter(model => ctx.matchModel(model, MindmapElementModel));
|
||||||
|
if (!mindmaps.length) return null;
|
||||||
|
|
||||||
|
// Not displayed when there is both a normal shape and a mindmap shape.
|
||||||
|
if (models.length !== mindmaps.length) return null;
|
||||||
|
|
||||||
|
mindmaps = Array.from(new Set(mindmaps));
|
||||||
|
|
||||||
|
return createMindmapStyleActionMenu(ctx, mindmaps);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b.mindmap-layout',
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
return models.some(hasGrouped);
|
||||||
|
},
|
||||||
|
content(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
if (!models.length) return null;
|
||||||
|
|
||||||
|
let mindmaps = models
|
||||||
|
.map(model => model.group)
|
||||||
|
.filter(model => ctx.matchModel(model, MindmapElementModel));
|
||||||
|
if (!mindmaps.length) return null;
|
||||||
|
|
||||||
|
// Not displayed when there is both a normal shape and a mindmap shape.
|
||||||
|
if (models.length !== mindmaps.length) return null;
|
||||||
|
|
||||||
|
mindmaps = Array.from(new Set(mindmaps));
|
||||||
|
|
||||||
|
// It's a sub node.
|
||||||
|
if (models.length === 1 && mindmaps[0].tree.element !== models[0])
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return createMindmapLayoutActionMenu(ctx, mindmaps);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const satisfies ToolbarActions;
|
||||||
|
|
||||||
export const builtinShapeToolbarConfig = {
|
export const builtinShapeToolbarConfig = {
|
||||||
actions: [
|
...shapeToolbarConfig,
|
||||||
{
|
actions: [...mindmapActions, ...shapeToolbarConfig.actions],
|
||||||
id: 'a.mindmap-style',
|
};
|
||||||
when(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
return models.some(hasGrouped);
|
|
||||||
},
|
|
||||||
content(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
if (!models.length) return null;
|
|
||||||
|
|
||||||
let mindmaps = models
|
|
||||||
.map(model => model.group)
|
|
||||||
.filter(model => ctx.matchModel(model, MindmapElementModel));
|
|
||||||
if (!mindmaps.length) return null;
|
|
||||||
|
|
||||||
// Not displayed when there is both a normal shape and a mindmap shape.
|
|
||||||
if (models.length !== mindmaps.length) return null;
|
|
||||||
|
|
||||||
mindmaps = Array.from(new Set(mindmaps));
|
|
||||||
|
|
||||||
return createMindmapStyleActionMenu(ctx, mindmaps);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'b.mindmap-layout',
|
|
||||||
when(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
return models.some(hasGrouped);
|
|
||||||
},
|
|
||||||
content(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
if (!models.length) return null;
|
|
||||||
|
|
||||||
let mindmaps = models
|
|
||||||
.map(model => model.group)
|
|
||||||
.filter(model => ctx.matchModel(model, MindmapElementModel));
|
|
||||||
if (!mindmaps.length) return null;
|
|
||||||
|
|
||||||
// Not displayed when there is both a normal shape and a mindmap shape.
|
|
||||||
if (models.length !== mindmaps.length) return null;
|
|
||||||
|
|
||||||
mindmaps = Array.from(new Set(mindmaps));
|
|
||||||
|
|
||||||
// It's a sub node.
|
|
||||||
if (models.length === 1 && mindmaps[0].tree.element !== models[0])
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return createMindmapLayoutActionMenu(ctx, mindmaps);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'c.switch-type',
|
|
||||||
when(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
return models.length > 0 && models.every(model => !hasGrouped(model));
|
|
||||||
},
|
|
||||||
content(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
if (!models.length) return null;
|
|
||||||
|
|
||||||
const shapeName =
|
|
||||||
getMostCommonValue<ShapeToolOption, 'shapeName'>(
|
|
||||||
models.map(model => ({
|
|
||||||
shapeName: getShapeName(model.shapeType, model.radius),
|
|
||||||
})),
|
|
||||||
'shapeName'
|
|
||||||
) ?? ShapeType.Rect;
|
|
||||||
|
|
||||||
const onPick = (shapeName: ShapeName) => {
|
|
||||||
const shapeType = getShapeType(shapeName);
|
|
||||||
const radius = getShapeRadius(shapeName);
|
|
||||||
|
|
||||||
ctx.std.store.captureSync();
|
|
||||||
|
|
||||||
for (const model of models) {
|
|
||||||
ctx.std
|
|
||||||
.get(EdgelessCRUDIdentifier)
|
|
||||||
.updateElement(model.id, { shapeType, radius });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return renderMenu({
|
|
||||||
icon: ShapeIcon(),
|
|
||||||
label: 'Switch type',
|
|
||||||
items: ShapeComponentConfig.map(item => ({
|
|
||||||
key: item.tooltip,
|
|
||||||
value: item.name,
|
|
||||||
// TODO(@fundon): should add a feature flag to switch style
|
|
||||||
icon: item.generalIcon,
|
|
||||||
disabled: item.disabled,
|
|
||||||
})),
|
|
||||||
currentValue: shapeName,
|
|
||||||
onPick,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'd.style',
|
|
||||||
// TODO(@fundon): should add a feature flag
|
|
||||||
when: false,
|
|
||||||
content(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
if (!models.length) return null;
|
|
||||||
|
|
||||||
const field = 'shapeStyle';
|
|
||||||
const shapeStyle =
|
|
||||||
getMostCommonValue(models, field) ?? ShapeStyle.General;
|
|
||||||
const onPick = (value: boolean) => {
|
|
||||||
const shapeStyle = value ? ShapeStyle.Scribbled : ShapeStyle.General;
|
|
||||||
const fontFamily = value ? FontFamily.Kalam : FontFamily.Inter;
|
|
||||||
|
|
||||||
for (const model of models) {
|
|
||||||
ctx.std
|
|
||||||
.get(EdgelessCRUDIdentifier)
|
|
||||||
.updateElement(model.id, { shapeStyle, fontFamily });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return renderMenu({
|
|
||||||
label: 'Style',
|
|
||||||
items: LINE_STYLE_LIST,
|
|
||||||
currentValue: shapeStyle === ShapeStyle.Scribbled,
|
|
||||||
onPick,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'e.color',
|
|
||||||
when(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
return models.length > 0 && models.every(model => !hasGrouped(model));
|
|
||||||
},
|
|
||||||
content(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
if (!models.length) return null;
|
|
||||||
|
|
||||||
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
|
|
||||||
const theme = ctx.theme.edgeless$.value;
|
|
||||||
|
|
||||||
const firstModel = models[0];
|
|
||||||
const originalFillColor = firstModel.fillColor;
|
|
||||||
const originalStrokeColor = firstModel.strokeColor;
|
|
||||||
|
|
||||||
const mapped = models.map(
|
|
||||||
({ filled, fillColor, strokeColor, strokeWidth, strokeStyle }) => ({
|
|
||||||
fillColor: filled
|
|
||||||
? resolveColor(fillColor, theme)
|
|
||||||
: DefaultTheme.transparent,
|
|
||||||
strokeColor: resolveColor(strokeColor, theme),
|
|
||||||
strokeWidth,
|
|
||||||
strokeStyle,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const fillColor =
|
|
||||||
getMostCommonValue(mapped, 'fillColor') ??
|
|
||||||
resolveColor(DefaultTheme.shapeFillColor, theme);
|
|
||||||
const strokeColor =
|
|
||||||
getMostCommonValue(mapped, 'strokeColor') ??
|
|
||||||
resolveColor(DefaultTheme.shapeStrokeColor, theme);
|
|
||||||
const strokeWidth =
|
|
||||||
getMostCommonValue(mapped, 'strokeWidth') ?? LineWidth.Four;
|
|
||||||
const strokeStyle =
|
|
||||||
getMostCommonValue(mapped, 'strokeStyle') ?? StrokeStyle.Solid;
|
|
||||||
|
|
||||||
const onPickFillColor = (e: CustomEvent<PickColorEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const d = e.detail;
|
|
||||||
|
|
||||||
const field = 'fillColor';
|
|
||||||
|
|
||||||
if (d.type === 'pick') {
|
|
||||||
const value = d.detail.value;
|
|
||||||
const filled = isTransparent(value);
|
|
||||||
for (const model of models) {
|
|
||||||
const props = packColor(field, value);
|
|
||||||
// If `filled` can be set separately, this logic can be removed
|
|
||||||
if (field && !model.filled) {
|
|
||||||
const color = getTextColor(value, filled);
|
|
||||||
Object.assign(props, { filled, color });
|
|
||||||
}
|
|
||||||
ctx.std
|
|
||||||
.get(EdgelessCRUDIdentifier)
|
|
||||||
.updateElement(model.id, props);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const model of models) {
|
|
||||||
model[d.type === 'start' ? 'stash' : 'pop'](field);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onPickStrokeColor = (e: CustomEvent<PickColorEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const d = e.detail;
|
|
||||||
|
|
||||||
const field = 'strokeColor';
|
|
||||||
|
|
||||||
if (d.type === 'pick') {
|
|
||||||
const value = d.detail.value;
|
|
||||||
for (const model of models) {
|
|
||||||
const props = packColor(field, value);
|
|
||||||
ctx.std
|
|
||||||
.get(EdgelessCRUDIdentifier)
|
|
||||||
.updateElement(model.id, props);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const model of models) {
|
|
||||||
model[d.type === 'start' ? 'stash' : 'pop'](field);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onPickStrokeStyle = (e: CustomEvent<LineDetailType>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const { type, value } = e.detail;
|
|
||||||
|
|
||||||
if (type === 'size') {
|
|
||||||
const strokeWidth = value;
|
|
||||||
for (const model of models) {
|
|
||||||
ctx.std
|
|
||||||
.get(EdgelessCRUDIdentifier)
|
|
||||||
.updateElement(model.id, { strokeWidth });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strokeStyle = value;
|
|
||||||
for (const model of models) {
|
|
||||||
ctx.std
|
|
||||||
.get(EdgelessCRUDIdentifier)
|
|
||||||
.updateElement(model.id, { strokeStyle });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<edgeless-shape-color-picker
|
|
||||||
@pickFillColor=${onPickFillColor}
|
|
||||||
@pickStrokeColor=${onPickStrokeColor}
|
|
||||||
@pickStrokeStyle=${onPickStrokeStyle}
|
|
||||||
.payload=${{
|
|
||||||
fillColor,
|
|
||||||
strokeColor,
|
|
||||||
strokeWidth,
|
|
||||||
strokeStyle,
|
|
||||||
originalFillColor,
|
|
||||||
originalStrokeColor,
|
|
||||||
theme,
|
|
||||||
enableCustomColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</edgeless-shape-color-picker>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'f.text',
|
|
||||||
tooltip: 'Add text',
|
|
||||||
icon: AddTextIcon(),
|
|
||||||
when(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
return models.length === 1 && !hasGrouped(models[0]) && !models[0].text;
|
|
||||||
},
|
|
||||||
run(ctx) {
|
|
||||||
const model = ctx.getCurrentModelByType(ShapeElementModel);
|
|
||||||
if (!model) return;
|
|
||||||
|
|
||||||
const rootBlock = getRootBlock(ctx);
|
|
||||||
if (!rootBlock) return;
|
|
||||||
|
|
||||||
mountShapeTextEditor(model, rootBlock);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// id: `g.text`
|
|
||||||
...createTextActions(ShapeElementModel, 'shape', (ctx, model, props) => {
|
|
||||||
// No need to adjust element bounds
|
|
||||||
if (props['textAlign']) {
|
|
||||||
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, props);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const xywh = normalizeShapeBound(
|
|
||||||
model,
|
|
||||||
Bound.fromXYWH(model.deserializedXYWH)
|
|
||||||
).serialize();
|
|
||||||
|
|
||||||
ctx.std
|
|
||||||
.get(EdgelessCRUDIdentifier)
|
|
||||||
.updateElement(model.id, { ...props, xywh });
|
|
||||||
}).map<ToolbarGenericAction>(action => ({
|
|
||||||
...action,
|
|
||||||
id: `g.text-${action.id}`,
|
|
||||||
when(ctx) {
|
|
||||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
|
||||||
return (
|
|
||||||
models.length > 0 &&
|
|
||||||
models.every(model => !hasGrouped(model) && model.text)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
|
|
||||||
when: ctx => ctx.getSurfaceModelsByType(ShapeElementModel).length > 0,
|
|
||||||
} as const satisfies ToolbarModuleConfig;
|
|
||||||
|
|
||||||
// When the shape is filled with black color, the text color should be white.
|
|
||||||
// When the shape is transparent, the text color should be set according to the theme.
|
|
||||||
// Otherwise, the text color should be black.
|
|
||||||
function getTextColor(fillColor: Color, isNotTransparent = false) {
|
|
||||||
if (isNotTransparent) {
|
|
||||||
if (isEqual(fillColor, DefaultTheme.black)) {
|
|
||||||
return DefaultTheme.white;
|
|
||||||
} else if (isEqual(fillColor, DefaultTheme.white)) {
|
|
||||||
return DefaultTheme.black;
|
|
||||||
} else if (isEqual(fillColor, DefaultTheme.pureBlack)) {
|
|
||||||
return DefaultTheme.pureWhite;
|
|
||||||
} else if (isEqual(fillColor, DefaultTheme.pureWhite)) {
|
|
||||||
return DefaultTheme.pureBlack;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// aka `DefaultTheme.pureBlack`
|
|
||||||
return DefaultTheme.shapeTextColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasGrouped(model: ShapeElementModel) {
|
function hasGrouped(model: ShapeElementModel) {
|
||||||
return model.group instanceof MindmapElementModel;
|
return model.group instanceof MindmapElementModel;
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ import { EdgelessFrameOrderMenu } from './edgeless/components/toolbar/present/fr
|
|||||||
import { EdgelessNavigatorSettingButton } from './edgeless/components/toolbar/present/navigator-setting-button.js';
|
import { EdgelessNavigatorSettingButton } from './edgeless/components/toolbar/present/navigator-setting-button.js';
|
||||||
import { EdgelessPresentButton } from './edgeless/components/toolbar/present/present-button.js';
|
import { EdgelessPresentButton } from './edgeless/components/toolbar/present/present-button.js';
|
||||||
import { PresentationToolbar } from './edgeless/components/toolbar/presentation-toolbar.js';
|
import { PresentationToolbar } from './edgeless/components/toolbar/presentation-toolbar.js';
|
||||||
import { EdgelessToolbarShapeDraggable } from './edgeless/components/toolbar/shape/shape-draggable.js';
|
|
||||||
import { EdgelessShapeToolButton } from './edgeless/components/toolbar/shape/shape-tool-button.js';
|
|
||||||
import { OverlayScrollbar } from './edgeless/components/toolbar/template/overlay-scrollbar.js';
|
import { OverlayScrollbar } from './edgeless/components/toolbar/template/overlay-scrollbar.js';
|
||||||
import { AffineTemplateLoading } from './edgeless/components/toolbar/template/template-loading.js';
|
import { AffineTemplateLoading } from './edgeless/components/toolbar/template/template-loading.js';
|
||||||
import { EdgelessTemplatePanel } from './edgeless/components/toolbar/template/template-panel.js';
|
import { EdgelessTemplatePanel } from './edgeless/components/toolbar/template/template-panel.js';
|
||||||
@@ -168,7 +166,6 @@ function registerEdgelessToolbarComponents() {
|
|||||||
EdgelessMindmapToolButton
|
EdgelessMindmapToolButton
|
||||||
);
|
);
|
||||||
customElements.define('edgeless-note-tool-button', EdgelessNoteToolButton);
|
customElements.define('edgeless-note-tool-button', EdgelessNoteToolButton);
|
||||||
customElements.define('edgeless-shape-tool-button', EdgelessShapeToolButton);
|
|
||||||
customElements.define('edgeless-template-button', EdgelessTemplateButton);
|
customElements.define('edgeless-template-button', EdgelessTemplateButton);
|
||||||
|
|
||||||
// Menus
|
// Menus
|
||||||
@@ -181,10 +178,6 @@ function registerEdgelessToolbarComponents() {
|
|||||||
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
|
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
|
||||||
|
|
||||||
// Toolbar components
|
// Toolbar components
|
||||||
customElements.define(
|
|
||||||
'edgeless-toolbar-shape-draggable',
|
|
||||||
EdgelessToolbarShapeDraggable
|
|
||||||
);
|
|
||||||
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
|
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
|
||||||
|
|
||||||
// Frame order components
|
// Frame order components
|
||||||
@@ -322,8 +315,6 @@ declare global {
|
|||||||
'edgeless-frame-order-menu': EdgelessFrameOrderMenu;
|
'edgeless-frame-order-menu': EdgelessFrameOrderMenu;
|
||||||
'edgeless-navigator-setting-button': EdgelessNavigatorSettingButton;
|
'edgeless-navigator-setting-button': EdgelessNavigatorSettingButton;
|
||||||
'edgeless-present-button': EdgelessPresentButton;
|
'edgeless-present-button': EdgelessPresentButton;
|
||||||
'edgeless-toolbar-shape-draggable': EdgelessToolbarShapeDraggable;
|
|
||||||
'edgeless-shape-tool-button': EdgelessShapeToolButton;
|
|
||||||
'overlay-scrollbar': OverlayScrollbar;
|
'overlay-scrollbar': OverlayScrollbar;
|
||||||
'affine-template-loading': AffineTemplateLoading;
|
'affine-template-loading': AffineTemplateLoading;
|
||||||
'edgeless-templates-panel': EdgelessTemplatePanel;
|
'edgeless-templates-panel': EdgelessTemplatePanel;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@blocksuite/affine-model": "workspace:*",
|
"@blocksuite/affine-model": "workspace:*",
|
||||||
"@blocksuite/affine-rich-text": "workspace:*",
|
"@blocksuite/affine-rich-text": "workspace:*",
|
||||||
"@blocksuite/affine-shared": "workspace:*",
|
"@blocksuite/affine-shared": "workspace:*",
|
||||||
|
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
|
||||||
"@blocksuite/block-std": "workspace:*",
|
"@blocksuite/block-std": "workspace:*",
|
||||||
"@blocksuite/global": "workspace:*",
|
"@blocksuite/global": "workspace:*",
|
||||||
"@blocksuite/icons": "^2.2.6",
|
"@blocksuite/icons": "^2.2.6",
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
export * from './shape-draggable';
|
||||||
export * from './shape-menu';
|
export * from './shape-menu';
|
||||||
|
export * from './shape-tool-button';
|
||||||
export * from './shape-tool-element';
|
export * from './shape-tool-element';
|
||||||
|
|||||||
@@ -2,12 +2,6 @@ import {
|
|||||||
CanvasElementType,
|
CanvasElementType,
|
||||||
EdgelessCRUDIdentifier,
|
EdgelessCRUDIdentifier,
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import {
|
|
||||||
ellipseSvg,
|
|
||||||
roundedSvg,
|
|
||||||
ShapeTool,
|
|
||||||
triangleSvg,
|
|
||||||
} from '@blocksuite/affine-gfx-shape';
|
|
||||||
import {
|
import {
|
||||||
getShapeRadius,
|
getShapeRadius,
|
||||||
getShapeType,
|
getShapeType,
|
||||||
@@ -30,6 +24,8 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
|
import { ShapeTool } from '../shape-tool.js';
|
||||||
|
import { ellipseSvg, roundedSvg, triangleSvg } from '../toolbar/icons.js';
|
||||||
import type { DraggableShape } from './utils.js';
|
import type { DraggableShape } from './utils.js';
|
||||||
import { buildVariablesObject } from './utils.js';
|
import { buildVariablesObject } from './utils.js';
|
||||||
|
|
||||||
@@ -210,6 +206,7 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
|
|||||||
this._setShapeOverlayLock(false);
|
this._setShapeOverlayLock(false);
|
||||||
this.readyToDrop = false;
|
this.readyToDrop = false;
|
||||||
|
|
||||||
|
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
||||||
this.gfx.tool.setTool('default');
|
this.gfx.tool.setTool('default');
|
||||||
this.gfx.selection.set({
|
this.gfx.selection.set({
|
||||||
elements: [id],
|
elements: [id],
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ShapeTool } from '@blocksuite/affine-gfx-shape';
|
|
||||||
import { type ShapeName, ShapeType } from '@blocksuite/affine-model';
|
import { type ShapeName, ShapeType } from '@blocksuite/affine-model';
|
||||||
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
|
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||||
import { css, html, LitElement } from 'lit';
|
import { css, html, LitElement } from 'lit';
|
||||||
|
|
||||||
|
import { ShapeTool } from '../shape-tool.js';
|
||||||
import type { DraggableShape } from './utils.js';
|
import type { DraggableShape } from './utils.js';
|
||||||
|
|
||||||
export class EdgelessShapeToolButton extends EdgelessToolbarToolMixin(
|
export class EdgelessShapeToolButton extends EdgelessToolbarToolMixin(
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { EdgelessShapeMenu, EdgelessShapeToolElement } from './draggable';
|
import {
|
||||||
|
EdgelessShapeMenu,
|
||||||
|
EdgelessShapeToolButton,
|
||||||
|
EdgelessShapeToolElement,
|
||||||
|
EdgelessToolbarShapeDraggable,
|
||||||
|
} from './draggable';
|
||||||
import { EdgelessShapeTextEditor } from './text/edgeless-shape-text-editor';
|
import { EdgelessShapeTextEditor } from './text/edgeless-shape-text-editor';
|
||||||
|
|
||||||
export function effects() {
|
export function effects() {
|
||||||
@@ -8,6 +13,11 @@ export function effects() {
|
|||||||
'edgeless-shape-tool-element',
|
'edgeless-shape-tool-element',
|
||||||
EdgelessShapeToolElement
|
EdgelessShapeToolElement
|
||||||
);
|
);
|
||||||
|
customElements.define('edgeless-shape-tool-button', EdgelessShapeToolButton);
|
||||||
|
customElements.define(
|
||||||
|
'edgeless-toolbar-shape-draggable',
|
||||||
|
EdgelessToolbarShapeDraggable
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -15,5 +25,7 @@ declare global {
|
|||||||
'edgeless-shape-text-editor': EdgelessShapeTextEditor;
|
'edgeless-shape-text-editor': EdgelessShapeTextEditor;
|
||||||
'edgeless-shape-menu': EdgelessShapeMenu;
|
'edgeless-shape-menu': EdgelessShapeMenu;
|
||||||
'edgeless-shape-tool-element': EdgelessShapeToolElement;
|
'edgeless-shape-tool-element': EdgelessShapeToolElement;
|
||||||
|
'edgeless-toolbar-shape-draggable': EdgelessToolbarShapeDraggable;
|
||||||
|
'edgeless-shape-tool-button': EdgelessShapeToolButton;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './icons';
|
export * from './icons';
|
||||||
|
export * from './shape';
|
||||||
export * from './shape-menu-config';
|
export * from './shape-menu-config';
|
||||||
|
|||||||
328
blocksuite/affine/gfx/shape/src/toolbar/shape.ts
Normal file
328
blocksuite/affine/gfx/shape/src/toolbar/shape.ts
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import {
|
||||||
|
EdgelessCRUDIdentifier,
|
||||||
|
normalizeShapeBound,
|
||||||
|
} from '@blocksuite/affine-block-surface';
|
||||||
|
import {
|
||||||
|
packColor,
|
||||||
|
type PickColorEvent,
|
||||||
|
} from '@blocksuite/affine-components/color-picker';
|
||||||
|
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
|
||||||
|
import {
|
||||||
|
type Color,
|
||||||
|
DefaultTheme,
|
||||||
|
FontFamily,
|
||||||
|
getShapeName,
|
||||||
|
getShapeRadius,
|
||||||
|
getShapeType,
|
||||||
|
isTransparent,
|
||||||
|
LineWidth,
|
||||||
|
MindmapElementModel,
|
||||||
|
resolveColor,
|
||||||
|
ShapeElementModel,
|
||||||
|
type ShapeName,
|
||||||
|
ShapeStyle,
|
||||||
|
ShapeType,
|
||||||
|
StrokeStyle,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
|
import type {
|
||||||
|
ToolbarGenericAction,
|
||||||
|
ToolbarModuleConfig,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
|
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
|
||||||
|
import {
|
||||||
|
createTextActions,
|
||||||
|
getRootBlock,
|
||||||
|
LINE_STYLE_LIST,
|
||||||
|
renderMenu,
|
||||||
|
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||||
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
|
import { AddTextIcon, ShapeIcon } from '@blocksuite/icons/lit';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
|
||||||
|
import type { ShapeToolOption } from '../shape-tool';
|
||||||
|
import { mountShapeTextEditor } from '../text';
|
||||||
|
import { ShapeComponentConfig } from './shape-menu-config';
|
||||||
|
|
||||||
|
export const shapeToolbarConfig = {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'c.switch-type',
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
return models.length > 0 && models.every(model => !hasGrouped(model));
|
||||||
|
},
|
||||||
|
content(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
if (!models.length) return null;
|
||||||
|
|
||||||
|
const shapeName =
|
||||||
|
getMostCommonValue<ShapeToolOption, 'shapeName'>(
|
||||||
|
models.map(model => ({
|
||||||
|
shapeName: getShapeName(model.shapeType, model.radius),
|
||||||
|
})),
|
||||||
|
'shapeName'
|
||||||
|
) ?? ShapeType.Rect;
|
||||||
|
|
||||||
|
const onPick = (shapeName: ShapeName) => {
|
||||||
|
const shapeType = getShapeType(shapeName);
|
||||||
|
const radius = getShapeRadius(shapeName);
|
||||||
|
|
||||||
|
ctx.std.store.captureSync();
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
ctx.std
|
||||||
|
.get(EdgelessCRUDIdentifier)
|
||||||
|
.updateElement(model.id, { shapeType, radius });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderMenu({
|
||||||
|
icon: ShapeIcon(),
|
||||||
|
label: 'Switch type',
|
||||||
|
items: ShapeComponentConfig.map(item => ({
|
||||||
|
key: item.tooltip,
|
||||||
|
value: item.name,
|
||||||
|
// TODO(@fundon): should add a feature flag to switch style
|
||||||
|
icon: item.generalIcon,
|
||||||
|
disabled: item.disabled,
|
||||||
|
})),
|
||||||
|
currentValue: shapeName,
|
||||||
|
onPick,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'd.style',
|
||||||
|
// TODO(@fundon): should add a feature flag
|
||||||
|
when: false,
|
||||||
|
content(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
if (!models.length) return null;
|
||||||
|
|
||||||
|
const field = 'shapeStyle';
|
||||||
|
const shapeStyle =
|
||||||
|
getMostCommonValue(models, field) ?? ShapeStyle.General;
|
||||||
|
const onPick = (value: boolean) => {
|
||||||
|
const shapeStyle = value ? ShapeStyle.Scribbled : ShapeStyle.General;
|
||||||
|
const fontFamily = value ? FontFamily.Kalam : FontFamily.Inter;
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
ctx.std
|
||||||
|
.get(EdgelessCRUDIdentifier)
|
||||||
|
.updateElement(model.id, { shapeStyle, fontFamily });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderMenu({
|
||||||
|
label: 'Style',
|
||||||
|
items: LINE_STYLE_LIST,
|
||||||
|
currentValue: shapeStyle === ShapeStyle.Scribbled,
|
||||||
|
onPick,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e.color',
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
return models.length > 0 && models.every(model => !hasGrouped(model));
|
||||||
|
},
|
||||||
|
content(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
if (!models.length) return null;
|
||||||
|
|
||||||
|
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
|
||||||
|
const theme = ctx.theme.edgeless$.value;
|
||||||
|
|
||||||
|
const firstModel = models[0];
|
||||||
|
const originalFillColor = firstModel.fillColor;
|
||||||
|
const originalStrokeColor = firstModel.strokeColor;
|
||||||
|
|
||||||
|
const mapped = models.map(
|
||||||
|
({ filled, fillColor, strokeColor, strokeWidth, strokeStyle }) => ({
|
||||||
|
fillColor: filled
|
||||||
|
? resolveColor(fillColor, theme)
|
||||||
|
: DefaultTheme.transparent,
|
||||||
|
strokeColor: resolveColor(strokeColor, theme),
|
||||||
|
strokeWidth,
|
||||||
|
strokeStyle,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const fillColor =
|
||||||
|
getMostCommonValue(mapped, 'fillColor') ??
|
||||||
|
resolveColor(DefaultTheme.shapeFillColor, theme);
|
||||||
|
const strokeColor =
|
||||||
|
getMostCommonValue(mapped, 'strokeColor') ??
|
||||||
|
resolveColor(DefaultTheme.shapeStrokeColor, theme);
|
||||||
|
const strokeWidth =
|
||||||
|
getMostCommonValue(mapped, 'strokeWidth') ?? LineWidth.Four;
|
||||||
|
const strokeStyle =
|
||||||
|
getMostCommonValue(mapped, 'strokeStyle') ?? StrokeStyle.Solid;
|
||||||
|
|
||||||
|
const onPickFillColor = (e: CustomEvent<PickColorEvent>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const d = e.detail;
|
||||||
|
|
||||||
|
const field = 'fillColor';
|
||||||
|
|
||||||
|
if (d.type === 'pick') {
|
||||||
|
const value = d.detail.value;
|
||||||
|
const filled = isTransparent(value);
|
||||||
|
for (const model of models) {
|
||||||
|
const props = packColor(field, value);
|
||||||
|
// If `filled` can be set separately, this logic can be removed
|
||||||
|
if (field && !model.filled) {
|
||||||
|
const color = getTextColor(value, filled);
|
||||||
|
Object.assign(props, { filled, color });
|
||||||
|
}
|
||||||
|
ctx.std
|
||||||
|
.get(EdgelessCRUDIdentifier)
|
||||||
|
.updateElement(model.id, props);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
model[d.type === 'start' ? 'stash' : 'pop'](field);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onPickStrokeColor = (e: CustomEvent<PickColorEvent>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const d = e.detail;
|
||||||
|
|
||||||
|
const field = 'strokeColor';
|
||||||
|
|
||||||
|
if (d.type === 'pick') {
|
||||||
|
const value = d.detail.value;
|
||||||
|
for (const model of models) {
|
||||||
|
const props = packColor(field, value);
|
||||||
|
ctx.std
|
||||||
|
.get(EdgelessCRUDIdentifier)
|
||||||
|
.updateElement(model.id, props);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
model[d.type === 'start' ? 'stash' : 'pop'](field);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onPickStrokeStyle = (e: CustomEvent<LineDetailType>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const { type, value } = e.detail;
|
||||||
|
|
||||||
|
if (type === 'size') {
|
||||||
|
const strokeWidth = value;
|
||||||
|
for (const model of models) {
|
||||||
|
ctx.std
|
||||||
|
.get(EdgelessCRUDIdentifier)
|
||||||
|
.updateElement(model.id, { strokeWidth });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strokeStyle = value;
|
||||||
|
for (const model of models) {
|
||||||
|
ctx.std
|
||||||
|
.get(EdgelessCRUDIdentifier)
|
||||||
|
.updateElement(model.id, { strokeStyle });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<edgeless-shape-color-picker
|
||||||
|
@pickFillColor=${onPickFillColor}
|
||||||
|
@pickStrokeColor=${onPickStrokeColor}
|
||||||
|
@pickStrokeStyle=${onPickStrokeStyle}
|
||||||
|
.payload=${{
|
||||||
|
fillColor,
|
||||||
|
strokeColor,
|
||||||
|
strokeWidth,
|
||||||
|
strokeStyle,
|
||||||
|
originalFillColor,
|
||||||
|
originalStrokeColor,
|
||||||
|
theme,
|
||||||
|
enableCustomColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</edgeless-shape-color-picker>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'f.text',
|
||||||
|
tooltip: 'Add text',
|
||||||
|
icon: AddTextIcon(),
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
return models.length === 1 && !hasGrouped(models[0]) && !models[0].text;
|
||||||
|
},
|
||||||
|
run(ctx) {
|
||||||
|
const model = ctx.getCurrentModelByType(ShapeElementModel);
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
const rootBlock = getRootBlock(ctx);
|
||||||
|
if (!rootBlock) return;
|
||||||
|
|
||||||
|
mountShapeTextEditor(model, rootBlock);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// id: `g.text`
|
||||||
|
...createTextActions(ShapeElementModel, 'shape', (ctx, model, props) => {
|
||||||
|
// No need to adjust element bounds
|
||||||
|
if (props['textAlign']) {
|
||||||
|
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, props);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xywh = normalizeShapeBound(
|
||||||
|
model,
|
||||||
|
Bound.fromXYWH(model.deserializedXYWH)
|
||||||
|
).serialize();
|
||||||
|
|
||||||
|
ctx.std
|
||||||
|
.get(EdgelessCRUDIdentifier)
|
||||||
|
.updateElement(model.id, { ...props, xywh });
|
||||||
|
}).map<ToolbarGenericAction>(action => ({
|
||||||
|
...action,
|
||||||
|
id: `g.text-${action.id}`,
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||||
|
return (
|
||||||
|
models.length > 0 &&
|
||||||
|
models.every(model => !hasGrouped(model) && model.text)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
|
||||||
|
when: ctx => ctx.getSurfaceModelsByType(ShapeElementModel).length > 0,
|
||||||
|
} as const satisfies ToolbarModuleConfig;
|
||||||
|
|
||||||
|
// When the shape is filled with black color, the text color should be white.
|
||||||
|
// When the shape is transparent, the text color should be set according to the theme.
|
||||||
|
// Otherwise, the text color should be black.
|
||||||
|
function getTextColor(fillColor: Color, isNotTransparent = false) {
|
||||||
|
if (isNotTransparent) {
|
||||||
|
if (isEqual(fillColor, DefaultTheme.black)) {
|
||||||
|
return DefaultTheme.white;
|
||||||
|
} else if (isEqual(fillColor, DefaultTheme.white)) {
|
||||||
|
return DefaultTheme.black;
|
||||||
|
} else if (isEqual(fillColor, DefaultTheme.pureBlack)) {
|
||||||
|
return DefaultTheme.pureWhite;
|
||||||
|
} else if (isEqual(fillColor, DefaultTheme.pureWhite)) {
|
||||||
|
return DefaultTheme.pureBlack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// aka `DefaultTheme.pureBlack`
|
||||||
|
return DefaultTheme.shapeTextColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasGrouped(model: ShapeElementModel) {
|
||||||
|
return model.group instanceof MindmapElementModel;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
{ "path": "../../model" },
|
{ "path": "../../model" },
|
||||||
{ "path": "../../rich-text" },
|
{ "path": "../../rich-text" },
|
||||||
{ "path": "../../shared" },
|
{ "path": "../../shared" },
|
||||||
|
{ "path": "../../widgets/widget-edgeless-toolbar" },
|
||||||
{ "path": "../../../framework/block-std" },
|
{ "path": "../../../framework/block-std" },
|
||||||
{ "path": "../../../framework/global" },
|
{ "path": "../../../framework/global" },
|
||||||
{ "path": "../../../framework/store" }
|
{ "path": "../../../framework/store" }
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ export const PackageList = [
|
|||||||
'blocksuite/affine/model',
|
'blocksuite/affine/model',
|
||||||
'blocksuite/affine/rich-text',
|
'blocksuite/affine/rich-text',
|
||||||
'blocksuite/affine/shared',
|
'blocksuite/affine/shared',
|
||||||
|
'blocksuite/affine/widgets/widget-edgeless-toolbar',
|
||||||
'blocksuite/framework/block-std',
|
'blocksuite/framework/block-std',
|
||||||
'blocksuite/framework/global',
|
'blocksuite/framework/global',
|
||||||
'blocksuite/framework/store',
|
'blocksuite/framework/store',
|
||||||
|
|||||||
@@ -2983,6 +2983,7 @@ __metadata:
|
|||||||
"@blocksuite/affine-model": "workspace:*"
|
"@blocksuite/affine-model": "workspace:*"
|
||||||
"@blocksuite/affine-rich-text": "workspace:*"
|
"@blocksuite/affine-rich-text": "workspace:*"
|
||||||
"@blocksuite/affine-shared": "workspace:*"
|
"@blocksuite/affine-shared": "workspace:*"
|
||||||
|
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*"
|
||||||
"@blocksuite/block-std": "workspace:*"
|
"@blocksuite/block-std": "workspace:*"
|
||||||
"@blocksuite/global": "workspace:*"
|
"@blocksuite/global": "workspace:*"
|
||||||
"@blocksuite/icons": "npm:^2.2.6"
|
"@blocksuite/icons": "npm:^2.2.6"
|
||||||
|
|||||||
Reference in New Issue
Block a user