mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-30 13:20:45 +08:00
feat(core): edit icon in navigation panel (#13595)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Rename dialog now edits per-item explorer icons (emoji or custom) and can skip name-change callbacks. Doc icon picker added to the editor with localized "Add icon" placeholder and readonly rendering. Icon editor supports fallbacks, trigger variants, and improved input/test-id wiring. - **Style** - Updated icon picker and trigger sizing and placeholder visuals; title/icon layout adjustments. - **Chores** - Explorer icon storage and module added to persist and serve icons across the app. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const docIconPickerTrigger = style({
|
||||
width: 64,
|
||||
height: 64,
|
||||
padding: 2,
|
||||
selectors: {
|
||||
'&[data-icon-type="emoji"]': {
|
||||
fontSize: 60,
|
||||
lineHeight: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const placeholder = style({
|
||||
padding: '4px',
|
||||
});
|
||||
export const placeholderContent = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
});
|
||||
export const placeholderContentIcon = style({
|
||||
color: cssVarV2.icon.secondary,
|
||||
fontSize: 16,
|
||||
});
|
||||
export const placeholderContentText = style({
|
||||
color: cssVarV2.text.secondary,
|
||||
fontSize: 12,
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { IconEditor, IconRenderer } from '@affine/component';
|
||||
import { ExplorerIconService } from '@affine/core/modules/explorer-icon/services/explorer-icon';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { SmileSolidIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import * as styles from './doc-icon-picker.css';
|
||||
|
||||
const TitleContainer = ({
|
||||
children,
|
||||
isPlaceholder,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isPlaceholder: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="doc-icon-container"
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
// title container has `padding-top`
|
||||
transform: isPlaceholder ? 'translateY(80%)' : 'translateY(50%)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DocIconPicker = ({
|
||||
docId,
|
||||
readonly,
|
||||
}: {
|
||||
docId: string;
|
||||
readonly?: boolean;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const explorerIconService = useService(ExplorerIconService);
|
||||
|
||||
const icon = useLiveData(explorerIconService.icon$('doc', docId));
|
||||
|
||||
const isPlaceholder = !icon?.type || !icon?.icon;
|
||||
|
||||
if (readonly) {
|
||||
return isPlaceholder ? null : (
|
||||
<div className={styles.docIconPickerTrigger} data-icon-type={icon?.type}>
|
||||
<IconRenderer iconType={icon.type} icon={icon.icon} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TitleContainer isPlaceholder={isPlaceholder}>
|
||||
<IconEditor
|
||||
iconType={icon?.type}
|
||||
icon={icon?.icon}
|
||||
onIconChange={(type, icon) => {
|
||||
explorerIconService.setIcon({
|
||||
where: 'doc',
|
||||
id: docId,
|
||||
type,
|
||||
icon,
|
||||
});
|
||||
}}
|
||||
closeAfterSelect={true}
|
||||
triggerVariant="plain"
|
||||
triggerClassName={
|
||||
isPlaceholder ? styles.placeholder : styles.docIconPickerTrigger
|
||||
}
|
||||
iconPlaceholder={
|
||||
<div className={styles.placeholderContent}>
|
||||
<SmileSolidIcon className={styles.placeholderContentIcon} />
|
||||
<span className={styles.placeholderContentText}>
|
||||
{t['com.affine.docIconPicker.placeholder']()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</TitleContainer>
|
||||
);
|
||||
};
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
WorkspacePropertiesTable,
|
||||
} from '../../components/properties';
|
||||
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
||||
import { DocIconPicker } from './doc-icon-picker';
|
||||
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
||||
import { StarterBar } from './starter-bar';
|
||||
import * as styles from './styles.css';
|
||||
@@ -254,6 +255,9 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
return (
|
||||
<>
|
||||
<div className={styles.affineDocViewport}>
|
||||
{!BUILD_CONFIG.isMobileEdition ? (
|
||||
<DocIconPicker docId={page.id} readonly={readonly || shared} />
|
||||
) : null}
|
||||
{!isJournal ? (
|
||||
<LitDocTitle doc={page} ref={onTitleRef} />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user