feat(core): replace emoji-mart with affine icon picker (#13644)

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

- New Features
  - Unified icon picker with consistent rendering across the app.
  - Picker can auto-close after selection.
  - “Remove” now clears the icon selection.

- Refactor
- Icon handling consolidated across editors, navigation, and document
titles for consistent behavior.
  - Picker now opens on the Emoji panel by default.

- Style
  - Adjusted line-height and selectors for icon picker visuals.

- Chores
  - Removed unused emoji-mart dependencies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Cats Juice
2025-09-26 14:41:29 +08:00
committed by GitHub
parent c540400496
commit d272c4342d
16 changed files with 121 additions and 138 deletions

View File

@@ -6,7 +6,7 @@ export const docIconPickerTrigger = style({
height: 64,
padding: 2,
selectors: {
'&[data-icon-type="emoji"]': {
'&[data-icon-type="emoji"], &[data-icon-type="affine-icon"]': {
fontSize: 60,
lineHeight: 1,
},

View File

@@ -40,12 +40,15 @@ export const DocIconPicker = ({
const icon = useLiveData(explorerIconService.icon$('doc', docId));
const isPlaceholder = !icon?.type || !icon?.icon;
const isPlaceholder = !icon?.icon;
if (readonly) {
return isPlaceholder ? null : (
<div className={styles.docIconPickerTrigger} data-icon-type={icon?.type}>
<IconRenderer iconType={icon.type} icon={icon.icon} />
<div
className={styles.docIconPickerTrigger}
data-icon-type={icon?.icon?.type}
>
<IconRenderer data={icon.icon} />
</div>
);
}
@@ -53,14 +56,12 @@ export const DocIconPicker = ({
return (
<TitleContainer isPlaceholder={isPlaceholder}>
<IconEditor
iconType={icon?.type}
icon={icon?.icon}
onIconChange={(type, icon) => {
onIconChange={data => {
explorerIconService.setIcon({
where: 'doc',
id: docId,
type,
icon,
icon: data,
});
}}
closeAfterSelect={true}

View File

@@ -6,6 +6,8 @@ import {
type DropTargetTreeInstruction,
IconAndNameEditorMenu,
IconButton,
type IconData,
IconRenderer,
Menu,
MenuItem,
useDraggable,
@@ -13,7 +15,6 @@ import {
} from '@affine/component';
import { Guard } from '@affine/core/components/guard';
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
import type { ExplorerIconType } from '@affine/core/modules/db/schema/schema';
import { ExplorerIconService } from '@affine/core/modules/explorer-icon/services/explorer-icon';
import type { ExplorerType } from '@affine/core/modules/explorer-icon/store/explorer-icon';
import type { DocPermissionActions } from '@affine/core/modules/permissions';
@@ -142,13 +143,12 @@ export const NavigationPanelTreeNodeRenameModal = ({
);
const onIconChange = useCallback(
(type?: ExplorerIconType, icon?: string) => {
(data?: IconData) => {
if (!explorerIconConfig) return;
explorerIconService.setIcon({
where: explorerIconConfig.where,
id: explorerIconConfig.id,
type,
icon,
icon: data,
});
},
[explorerIconConfig, explorerIconService]
@@ -161,8 +161,7 @@ export const NavigationPanelTreeNodeRenameModal = ({
onIconChange={onIconChange}
onNameChange={handleRename}
name={rawName ?? ''}
iconType={explorerIcon?.type ?? 'emoji'}
icon={explorerIcon?.icon ?? ''}
icon={explorerIcon?.icon}
width={sidebarWidth - 16}
contentOptions={{
sideOffset: 36,
@@ -461,10 +460,7 @@ export const NavigationPanelTreeNode = ({
/>
</div>
<div className={styles.iconContainer}>
{/* Only emoji icon is supported for now */}
{explorerIcon && explorerIcon.type === 'emoji'
? explorerIcon.icon
: fallbackIcon}
<IconRenderer data={explorerIcon?.icon} fallback={fallbackIcon} />
</div>
</div>

View File

@@ -1,3 +1,4 @@
import type { IconData } from '@affine/component';
import {
type DBSchemaBuilder,
f,
@@ -51,8 +52,7 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = {
* ${doc|collection|folder|tag}:${id}
*/
id: f.string().primaryKey(),
type: f.enum('emoji', 'affine-icon', 'blob'),
icon: f.string(),
icon: f.json<IconData>(),
},
} as const satisfies DBSchemaBuilder;
export type AFFiNEWorkspaceDbSchema = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
@@ -61,10 +61,6 @@ export type DocProperties = ORMEntity<AFFiNEWorkspaceDbSchema['docProperties']>;
export type DocCustomPropertyInfo = ORMEntity<
AFFiNEWorkspaceDbSchema['docCustomPropertyInfo']
>;
export type ExplorerIcon = ORMEntity<AFFiNEWorkspaceDbSchema['explorerIcon']>;
export type ExplorerIconType = ORMEntity<
AFFiNEWorkspaceDbSchema['explorerIcon']
>['type'];
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
favorite: {

View File

@@ -29,6 +29,7 @@ import type { DocRecord, DocsService } from '../../doc';
import type { ExplorerIconService } from '../../explorer-icon/services/explorer-icon';
import type { I18nService } from '../../i18n';
import type { JournalService } from '../../journal';
import { getDocIconComponent } from './icon';
type IconType = 'rc' | 'lit';
interface DocDisplayIconOptions<T extends IconType> {
@@ -149,8 +150,10 @@ export class DocDisplayMetaService extends Service {
if (enableEmojiIcon) {
// const { emoji } = extractEmojiIcon(title);
// if (emoji) return () => emoji;
const icon = get(this.explorerIconService.icon$('doc', docId));
if (icon && icon.type === 'emoji') return () => icon.icon;
const icon = get(this.explorerIconService.icon$('doc', docId))?.icon;
if (icon) {
return getDocIconComponent(icon);
}
}
// title alias

View File

@@ -0,0 +1,7 @@
import { type IconData, IconRenderer } from '@affine/component';
export const getDocIconComponent = (icon: IconData) => {
const Icon = () => <IconRenderer data={icon} />;
Icon.displayName = 'DocIcon';
return Icon;
};

View File

@@ -1,7 +1,7 @@
import type { IconData } from '@affine/component';
import { Store } from '@toeverything/infra';
import type { WorkspaceDBService } from '../../db';
import type { ExplorerIconType } from '../../db/schema/schema';
export type ExplorerType = 'doc' | 'collection' | 'folder' | 'tag';
@@ -18,21 +18,15 @@ export class ExplorerIconStore extends Store {
return this.dbService.db.explorerIcon.get(`${type}:${id}`);
}
setIcon(options: {
where: ExplorerType;
id: string;
type?: ExplorerIconType;
icon?: string;
}) {
const { where, id, type, icon } = options;
setIcon(options: { where: ExplorerType; id: string; icon?: IconData }) {
const { where, id, icon } = options;
// remove icon
if (!type || !icon) {
if (!icon) {
return this.dbService.db.explorerIcon.delete(`${where}:${id}`);
}
// upsert icon
return this.dbService.db.explorerIcon.create({
id: `${where}:${id}`,
type,
icon,
});
}