From e2c752d56f09b43ede40bba62e65ef518e1fb93c Mon Sep 17 00:00:00 2001 From: L-Sun Date: Fri, 28 Mar 2025 03:47:37 +0000 Subject: [PATCH] feat(editor): inner toolbar layout for block (#11243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [BS-2808](https://linear.app/affine-design/issue/BS-2808/组件内-toolbar-重构) --- .../toolbar/template/template-panel.ts | 8 +- .../affine/components/src/toolbar/styles.ts | 20 ++-- .../src/services/toolbar-service/config.ts | 6 +- .../src/services/toolbar-service/context.ts | 4 + .../src/services/toolbar-service/registry.ts | 11 +++ .../src/edgeless-toolbar.ts | 9 +- .../widgets/widget-toolbar/src/toolbar.ts | 63 +++++++++---- .../widgets/widget-toolbar/src/utils.ts | 91 ++++++++++++++----- 8 files changed, 150 insertions(+), 62 deletions(-) diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/template/template-panel.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/template/template-panel.ts index ebea2fad72..f18c2364d7 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/template/template-panel.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/template/template-panel.ts @@ -46,12 +46,8 @@ export class EdgelessTemplatePanel extends WithDisposable(LitElement) { display: flex; flex-direction: column; } - .edgeless-templates-panel[data-app-theme='light'] { - ${unsafeCSS(lightToolbarStyles.join('\n'))} - } - .edgeless-templates-panel[data-app-theme='dark'] { - ${unsafeCSS(darkToolbarStyles.join('\n'))} - } + ${unsafeCSS(lightToolbarStyles('.edgeless-templates-panel'))} + ${unsafeCSS(darkToolbarStyles('.edgeless-templates-panel'))} .search-bar { padding: 21px 24px; diff --git a/blocksuite/affine/components/src/toolbar/styles.ts b/blocksuite/affine/components/src/toolbar/styles.ts index 38ed6cc925..fc4beb884a 100644 --- a/blocksuite/affine/components/src/toolbar/styles.ts +++ b/blocksuite/affine/components/src/toolbar/styles.ts @@ -21,10 +21,18 @@ const toolbarColorKeys: Array = [ '--affine-hover-color-filled', ]; -export const lightToolbarStyles = toolbarColorKeys.map( - key => `${key}: ${unsafeCSS(combinedLightCssVariables[key])};` -); +export const lightToolbarStyles = (selector: string) => ` + ${selector}[data-app-theme='light'] { + ${toolbarColorKeys + .map(key => `${key}: ${unsafeCSS(combinedLightCssVariables[key])};`) + .join('\n')} + } +`; -export const darkToolbarStyles = toolbarColorKeys.map( - key => `${key}: ${unsafeCSS(combinedDarkCssVariables[key])};` -); +export const darkToolbarStyles = (selector: string) => ` + ${selector}[data-app-theme='dark'] { + ${toolbarColorKeys + .map(key => `${key}: ${unsafeCSS(combinedDarkCssVariables[key])};`) + .join('\n')} + } +`; diff --git a/blocksuite/affine/shared/src/services/toolbar-service/config.ts b/blocksuite/affine/shared/src/services/toolbar-service/config.ts index 0867597f9e..b426bfe28c 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/config.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/config.ts @@ -3,10 +3,14 @@ import type { Placement } from '@floating-ui/dom'; import type { ToolbarActions } from './action'; import type { ToolbarContext } from './context'; +export type ToolbarPlacement = + | Extract + | 'inner'; + export type ToolbarModuleConfig = { actions: ToolbarActions; when?: ((ctx: ToolbarContext) => boolean) | boolean; - placement?: Extract; + placement?: ToolbarPlacement; }; diff --git a/blocksuite/affine/shared/src/services/toolbar-service/context.ts b/blocksuite/affine/shared/src/services/toolbar-service/context.ts index b4d0723e0b..bf64d18eb3 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/context.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/context.ts @@ -123,6 +123,10 @@ abstract class ToolbarContextBase { return this.toolbarRegistry.flavour$; } + get placement$() { + return this.toolbarRegistry.placement$; + } + get message$() { return this.toolbarRegistry.message$; } diff --git a/blocksuite/affine/shared/src/services/toolbar-service/registry.ts b/blocksuite/affine/shared/src/services/toolbar-service/registry.ts index ed053a29c1..0697cf1744 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/registry.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/registry.ts @@ -4,6 +4,7 @@ import { type Container, createIdentifier } from '@blocksuite/global/di'; import { Extension, type ExtensionType } from '@blocksuite/store'; import { signal } from '@preact/signals-core'; +import type { ToolbarPlacement } from './config'; import { Flags } from './flags'; import type { ToolbarModule } from './module'; @@ -33,6 +34,8 @@ export class ToolbarRegistryExtension extends Extension { setFloating: (element?: Element) => void; } | null>(null); + placement$ = signal('top'); + flags = new Flags(); constructor(readonly std: BlockStdScope) { @@ -43,6 +46,14 @@ export class ToolbarRegistryExtension extends Extension { return this.std.provider.getAll(ToolbarModuleIdentifier); } + getModulePlacement(flavour: string, fallback: ToolbarPlacement = 'top') { + return ( + this.modules.get(`custom:${flavour}`)?.config.placement ?? + this.modules.get(flavour)?.config.placement ?? + fallback + ); + } + static override setup(di: Container) { di.addImpl(ToolbarRegistryIdentifier, this, [StdIdentifier]); } diff --git a/blocksuite/affine/widgets/widget-edgeless-toolbar/src/edgeless-toolbar.ts b/blocksuite/affine/widgets/widget-edgeless-toolbar/src/edgeless-toolbar.ts index 4ff98a3c4b..180451bd29 100644 --- a/blocksuite/affine/widgets/widget-edgeless-toolbar/src/edgeless-toolbar.ts +++ b/blocksuite/affine/widgets/widget-edgeless-toolbar/src/edgeless-toolbar.ts @@ -77,12 +77,9 @@ export class EdgelessToolbarWidget extends WidgetComponent { display: flex; justify-content: center; } - .edgeless-toolbar-wrapper[data-app-theme='light'] { - ${unsafeCSS(lightToolbarStyles.join('\n'))} - } - .edgeless-toolbar-wrapper[data-app-theme='dark'] { - ${unsafeCSS(darkToolbarStyles.join('\n'))} - } + ${unsafeCSS(lightToolbarStyles('.edgeless-toolbar-wrapper'))} + ${unsafeCSS(darkToolbarStyles('.edgeless-toolbar-wrapper'))} + .edgeless-toolbar-toggle-control { pointer-events: auto; padding-bottom: 16px; diff --git a/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts b/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts index 63c33a2cc1..9732834df8 100644 --- a/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts +++ b/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts @@ -17,6 +17,7 @@ import { ToolbarFlag as Flag, ToolbarRegistryIdentifier, } from '@blocksuite/affine-shared/services'; +import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { matchModels } from '@blocksuite/affine-shared/utils'; import { type BlockComponent, @@ -36,7 +37,7 @@ import { getCommonBoundWithRotation, } from '@blocksuite/global/gfx'; import { nextTick } from '@blocksuite/global/utils'; -import type { Placement, ReferenceElement, SideObject } from '@floating-ui/dom'; +import type { ReferenceElement, SideObject } from '@floating-ui/dom'; import { batch, effect, signal } from '@preact/signals-core'; import { css, unsafeCSS } from 'lit'; import groupBy from 'lodash-es/groupBy'; @@ -76,15 +77,32 @@ export class AffineToolbarWidget extends WidgetComponent { } } - editor-toolbar[data-app-theme='dark'] { - ${unsafeCSS(darkToolbarStyles.join('\n'))} - } - editor-toolbar[data-app-theme='light'] { - ${unsafeCSS(lightToolbarStyles.join('\n'))} - } - `; + editor-toolbar[data-placement='inner'] { + background-color: unset; + box-shadow: unset; + height: fit-content; + padding-top: 4px; + border-radius: 0; + border: unset; + justify-content: flex-end; + box-sizing: border-box; + gap: 4px; - placement$ = signal('top'); + editor-icon-button, + editor-menu-button { + background: ${unsafeCSSVarV2('button/iconButtonSolid')}; + color: ${unsafeCSSVarV2('text/primary')}; + box-shadow: ${unsafeCSSVar('buttonShadow')}; + border-radius: 4px; + } + editor-menu-button > div { + gap: 4px; + } + } + + ${unsafeCSS(darkToolbarStyles('editor-toolbar'))} + ${unsafeCSS(lightToolbarStyles('editor-toolbar'))} + `; sideOptions$ = signal | null>(null); @@ -213,7 +231,7 @@ export class AffineToolbarWidget extends WidgetComponent { this.sideOptions$.value = sideOptions; ctx.flavour$.value = flavour; - this.placement$.value = hasLocked ? 'top' : 'top-start'; + ctx.placement$.value = hasLocked ? 'top' : 'top-start'; ctx.flags.refresh(Flag.Surface); }); } @@ -228,7 +246,6 @@ export class AffineToolbarWidget extends WidgetComponent { super.connectedCallback(); const { - placement$, sideOptions$, referenceElement$, disposables, @@ -237,7 +254,7 @@ export class AffineToolbarWidget extends WidgetComponent { host, std, } = this; - const { flags, flavour$, message$ } = toolbarRegistry; + const { flags, flavour$, message$, placement$ } = toolbarRegistry; const context = new ToolbarContext(std); // TODO(@fundon): fix toolbar position shaking when the wheel scrolls @@ -266,7 +283,7 @@ export class AffineToolbarWidget extends WidgetComponent { sideOptions$.value = null; flavour$.value = 'affine:note'; - placement$.value = 'top'; + placement$.value = toolbarRegistry.getModulePlacement('affine:note'); flags.refresh(Flag.Text); }); }) @@ -317,7 +334,7 @@ export class AffineToolbarWidget extends WidgetComponent { sideOptions$.value = null; flavour$.value = 'affine:note'; - placement$.value = 'top'; + placement$.value = toolbarRegistry.getModulePlacement('affine:note'); flags.refresh(Flag.Native); }); }); @@ -338,7 +355,7 @@ export class AffineToolbarWidget extends WidgetComponent { if (block) { const modelFlavour = block.model.flavour; const existed = - toolbarRegistry.modules.has(modelFlavour) ?? + toolbarRegistry.modules.has(modelFlavour) || toolbarRegistry.modules.has(`custom:${modelFlavour}`); if (existed) { flavour = modelFlavour; @@ -366,7 +383,10 @@ export class AffineToolbarWidget extends WidgetComponent { sideOptions$.value = null; flavour$.value = flavour; - placement$.value = flavour === 'affine:note' ? 'top' : 'top-start'; + placement$.value = toolbarRegistry.getModulePlacement( + flavour, + flavour === 'affine:note' ? 'top' : 'top-start' + ); flags.refresh(Flag.Block); }); }) @@ -494,7 +514,7 @@ export class AffineToolbarWidget extends WidgetComponent { }) ); - // Handles elemets when updating + // Handles elements when updating disposables.add( context.gfx.surface$.subscribe(surface => { if (!surface) return; @@ -580,7 +600,7 @@ export class AffineToolbarWidget extends WidgetComponent { sideOptions$.value = null; flavour$.value = flavour; - placement$.value = 'top'; + placement$.value = toolbarRegistry.getModulePlacement(flavour); flags.refresh(Flag.Hovering); }); }) @@ -593,6 +613,13 @@ export class AffineToolbarWidget extends WidgetComponent { }) ); + // Update layout when placement changing to `inner` + disposables.add( + effect(() => { + toolbar.dataset.placement = placement$.value; + }) + ); + disposables.add( effect(() => { const value = flags.value$.value; diff --git a/blocksuite/affine/widgets/widget-toolbar/src/utils.ts b/blocksuite/affine/widgets/widget-toolbar/src/utils.ts index 1abdbbb033..eb8d6e2a4b 100644 --- a/blocksuite/affine/widgets/widget-toolbar/src/utils.ts +++ b/blocksuite/affine/widgets/widget-toolbar/src/utils.ts @@ -7,12 +7,13 @@ import { type ToolbarAction, type ToolbarActions, type ToolbarContext, + type ToolbarPlacement, } from '@blocksuite/affine-shared/services'; import { nextTick } from '@blocksuite/global/utils'; import { MoreVerticalIcon } from '@blocksuite/icons/lit'; import type { AutoUpdateOptions, - Placement, + ComputePositionConfig, ReferenceElement, SideObject, } from '@floating-ui/dom'; @@ -25,8 +26,9 @@ import { limitShift, offset, shift, + size, } from '@floating-ui/dom'; -import { html, render } from 'lit'; +import { html, nothing, render } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; import { join } from 'lit/directives/join.js'; import { keyed } from 'lit/directives/keyed.js'; @@ -51,34 +53,54 @@ export function autoUpdatePosition( toolbar: EditorToolbar, referenceElement: ReferenceElement, flavour: string, - placement: Placement, + placement: ToolbarPlacement, sideOptions: Partial | null, options: AutoUpdateOptions = { elementResize: false, animationFrame: true } ) { const isInline = flavour === 'affine:note'; const hasSurfaceScope = flavour.includes('surface'); + const isInner = placement === 'inner'; const offsetTop = sideOptions?.top ?? 0; const offsetBottom = sideOptions?.bottom ?? 0; const offsetY = offsetTop + (hasSurfaceScope ? 2 : 0); - const config = { - placement, - middleware: [ - offset(10 + offsetY), - isInline ? inline() : undefined, - shift(state => ({ - padding: { - top: 10, - right: 10, - bottom: 150, - left: 10, - }, - crossAxis: state.placement.includes('bottom'), - limiter: limitShift(), - })), - flip({ padding: 10 }), - hide(), - ], - }; + const config: Partial = isInner + ? { + placement: 'top-start', + middleware: [ + offset(({ rects }) => -rects.floating.height), + size({ + apply: ({ elements }) => { + elements.floating.style.width = `${ + elements.reference.getBoundingClientRect().width + }px`; + }, + }), + ], + } + : { + placement, + middleware: [ + offset(10 + offsetY), + size({ + apply: ({ elements }) => { + elements.floating.style.width = 'fit-content'; + }, + }), + isInline ? inline() : undefined, + shift(state => ({ + padding: { + top: 10, + right: 10, + bottom: 150, + left: 10, + }, + crossAxis: state.placement.includes('bottom'), + limiter: limitShift(), + })), + flip({ padding: 10 }), + hide(), + ], + }; const update = async () => { await Promise.race([ new Promise(resolve => { @@ -217,6 +239,8 @@ export function renderToolbar( a => a.placement === ActionPlacement.More ); + const innerToolbar = context.placement$.value === 'inner'; + if (moreActionGroup.length) { const moreMenuItems = renderActions( moreActionGroup, @@ -235,12 +259,20 @@ export function renderToolbar( aria-label="More menu" .contentPadding="${'8px'}" .button=${html` - + ${MoreVerticalIcon()} `} > -
+
${join(moreMenuItems, renderToolbarSeparator('horizontal'))}
@@ -251,7 +283,10 @@ export function renderToolbar( } render( - join(renderActions(primaryActionGroup, context), renderToolbarSeparator()), + join( + renderActions(primaryActionGroup, context), + innerToolbar ? nothing : renderToolbarSeparator() + ), toolbar ); } @@ -305,6 +340,7 @@ function renderActions( // TODO(@fundon): supports templates function renderActionItem(action: ToolbarAction, context: ToolbarContext) { + const innerToolbar = context.placement$.value === 'inner'; const ids = action.id.split('.'); const id = ids[ids.length - 1]; return html` @@ -316,6 +352,8 @@ function renderActionItem(action: ToolbarAction, context: ToolbarContext) { : action.active} ?disabled=${action.disabled} .tooltip=${action.tooltip} + .iconContainerPadding=${innerToolbar ? 4 : 2} + .iconSize=${innerToolbar ? '16px' : undefined} @click=${() => action.run?.(context)} > ${action.icon} @@ -327,6 +365,7 @@ function renderActionItem(action: ToolbarAction, context: ToolbarContext) { } function renderMenuActionItem(action: ToolbarAction, context: ToolbarContext) { + const innerToolbar = context.placement$.value === 'inner'; const ids = action.id.split('.'); const id = ids[ids.length - 1]; return html` @@ -341,6 +380,8 @@ function renderMenuActionItem(action: ToolbarAction, context: ToolbarContext) { : action.active} ?disabled=${action.disabled} .tooltip=${ifDefined(action.tooltip)} + .iconContainerPadding=${innerToolbar ? 4 : 2} + .iconSize=${innerToolbar ? '16px' : undefined} @click=${() => action.run?.(context)} > ${action.icon}