refactor(editor): new icon picker (#13658)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* In-tree icon picker for Callout blocks (emoji, app icons, images) with
popup UI and editor-wide extension/service.
* Callout toolbar adds background color presets, an icon-picker action,
and a destructive Delete action.

* **Refactor**
* Replaced legacy emoji workflow with icon-based rendering, updated
state, styling, and lifecycle for callouts.

* **Tests**
  * Updated callout E2E to reflect new default icon and picker behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: L-Sun <zover.v@gmail.com>
This commit is contained in:
3720
2025-09-29 19:06:14 +08:00
committed by GitHub
parent 8df7353722
commit 8006812bc0
24 changed files with 630 additions and 174 deletions
@@ -175,6 +175,7 @@ const usePreviewExtensions = () => {
.ai(enableAI, framework)
.theme(framework)
.database(framework)
.iconPicker(framework)
.linkedDoc(framework)
.paragraph(enableAI)
.linkPreview(framework)
@@ -117,6 +117,7 @@ const usePatchSpecs = (mode: DocMode, shared?: boolean) => {
.electron(framework)
.linkPreview(framework)
.codeBlockPreview(framework)
.iconPicker(framework)
.comment(enableComment, framework).value;
if (BUILD_CONFIG.isMobileEdition) {
@@ -16,6 +16,7 @@ import {
type AffineEditorViewOptions,
} from '@affine/core/blocksuite/view-extensions/editor-view/editor-view';
import { ElectronViewExtension } from '@affine/core/blocksuite/view-extensions/electron';
import { AffineIconPickerExtension } from '@affine/core/blocksuite/view-extensions/icon-picker';
import { AffineLinkPreviewExtension } from '@affine/core/blocksuite/view-extensions/link-preview-service';
import { MobileViewExtension } from '@affine/core/blocksuite/view-extensions/mobile';
import { PdfViewExtension } from '@affine/core/blocksuite/view-extensions/pdf';
@@ -58,6 +59,7 @@ type Configure = {
electron: (framework?: FrameworkProvider) => Configure;
linkPreview: (framework?: FrameworkProvider) => Configure;
codeBlockPreview: (framework?: FrameworkProvider) => Configure;
iconPicker: (framework?: FrameworkProvider) => Configure;
comment: (
enableComment?: boolean,
framework?: FrameworkProvider
@@ -86,6 +88,7 @@ class ViewProvider {
AffineThemeViewExtension,
AffineEditorViewExtension,
AffineEditorConfigViewExtension,
AffineIconPickerExtension,
CodeBlockPreviewViewExtension,
EdgelessBlockHeaderConfigViewExtension,
TurboRendererViewExtension,
@@ -123,6 +126,7 @@ class ViewProvider {
electron: this._configureElectron,
linkPreview: this._configureLinkPreview,
codeBlockPreview: this._configureCodeBlockHtmlPreview,
iconPicker: this._configureIconPicker,
comment: this._configureComment,
value: this._manager,
};
@@ -146,6 +150,7 @@ class ViewProvider {
.electron()
.linkPreview()
.codeBlockPreview()
.iconPicker()
.comment();
return this.config;
@@ -333,6 +338,11 @@ class ViewProvider {
return this.config;
};
private readonly _configureIconPicker = (framework?: FrameworkProvider) => {
this._manager.configure(AffineIconPickerExtension, { framework });
return this.config;
};
private readonly _configureComment = (
enableComment?: boolean,
framework?: FrameworkProvider
@@ -0,0 +1,23 @@
import { IconPickerServiceIdentifier } from '@blocksuite/affine/shared/services';
import { type ExtensionType } from '@blocksuite/affine/store';
import type { Container } from '@blocksuite/global/di';
import type { FrameworkProvider } from '@toeverything/infra';
import { IconPickerService } from '../../../modules/icon-picker/services/icon-picker';
/**
* Patch the icon picker service to make it available in BlockSuite
* @param framework
* @returns
*/
export function patchIconPickerService(
framework: FrameworkProvider
): ExtensionType {
return {
setup: (di: Container) => {
di.override(IconPickerServiceIdentifier, () => {
return framework.get(IconPickerService);
});
},
};
}
@@ -0,0 +1,32 @@
import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine/ext-loader';
import { FrameworkProvider } from '@toeverything/infra';
import { z } from 'zod';
import { patchIconPickerService } from './icon-picker-service';
const optionsSchema = z.object({
framework: z.instanceof(FrameworkProvider).optional(),
});
type AffineIconPickerViewOptions = z.infer<typeof optionsSchema>;
export class AffineIconPickerExtension extends ViewExtensionProvider<AffineIconPickerViewOptions> {
override name = 'affine-icon-picker-extension';
override schema = optionsSchema;
override setup(
context: ViewExtensionContext,
options?: AffineIconPickerViewOptions
) {
super.setup(context, options);
if (!options?.framework) {
return;
}
const { framework } = options;
context.register(patchIconPickerService(framework));
}
}
@@ -44,6 +44,7 @@ export const useAISpecs = () => {
.mobile(framework)
.electron(framework)
.linkPreview(framework)
.iconPicker(framework)
.codeBlockPreview(framework).value;
return manager.get('page');
@@ -0,0 +1,9 @@
import { type Framework } from '@toeverything/infra';
import { IconPickerService } from './services/icon-picker';
export { IconPickerService } from './services/icon-picker';
export function configureIconPickerModule(framework: Framework) {
framework.service(IconPickerService);
}
@@ -0,0 +1,16 @@
import { IconPicker, uniReactRoot } from '@affine/component';
// Import the identifier for internal use
import { type IconPickerService as IIconPickerService } from '@blocksuite/affine-shared/services';
import { Service } from '@toeverything/infra';
// Re-export types from BlockSuite shared services
export type {
IconData,
IconPickerService as IIconPickerService,
} from '@blocksuite/affine-shared/services';
export { IconPickerServiceIdentifier } from '@blocksuite/affine-shared/services';
export class IconPickerService extends Service implements IIconPickerService {
public readonly iconPickerComponent =
uniReactRoot.createUniComponent(IconPicker);
}
@@ -33,6 +33,7 @@ import { configureFavoriteModule } from './favorite';
import { configureFeatureFlagModule } from './feature-flag';
import { configureGlobalContextModule } from './global-context';
import { configureI18nModule } from './i18n';
import { configureIconPickerModule } from './icon-picker';
import { configureImportClipperModule } from './import-clipper';
import { configureImportTemplateModule } from './import-template';
import { configureIntegrationModule } from './integration';
@@ -132,4 +133,5 @@ export function configureCommonModules(framework: Framework) {
configureCommentModule(framework);
configureDocSummaryModule(framework);
configurePaywallModule(framework);
configureIconPickerModule(framework);
}