From 195864fc881c157d85669c2362c85e825de6c240 Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Mon, 22 Sep 2025 18:24:11 +0800 Subject: [PATCH] feat(core): edit icon in navigation panel (#13595) ## 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. --- .../fragments/doc-title/src/doc-title.ts | 2 + .../icon-name-editor/icon-name-editor.css.ts | 12 +- .../icon-name-editor.stories.tsx | 4 +- .../ui/icon-name-editor/icon-name-editor.tsx | 69 ++++++++--- .../block-suite-editor/doc-icon-picker.css.ts | 31 +++++ .../block-suite-editor/doc-icon-picker.tsx | 82 +++++++++++++ .../block-suite-editor/lit-adaper.tsx | 4 + .../nodes/collection/index.tsx | 4 + .../nodes/collection/operations.tsx | 8 +- .../navigation-panel/nodes/doc/index.tsx | 4 + .../navigation-panel/nodes/folder/index.tsx | 1 + .../navigation-panel/nodes/tag/index.tsx | 4 + .../navigation-panel/sections/tags/index.tsx | 12 +- .../components/navigation-panel/tree/node.tsx | 114 +++++++++++++----- .../core/src/modules/db/schema/schema.ts | 12 ++ .../src/modules/doc-display-meta/index.ts | 4 +- .../services/doc-display-meta.ts | 54 ++++----- .../core/src/modules/explorer-icon/index.ts | 13 ++ .../explorer-icon/services/explorer-icon.ts | 21 ++++ .../explorer-icon/store/explorer-icon.ts | 39 ++++++ packages/frontend/core/src/modules/index.ts | 2 + .../core/src/utils/extract-emoji-icon.ts | 3 + packages/frontend/i18n/src/i18n.gen.ts | 4 + packages/frontend/i18n/src/resources/en.json | 1 + tests/affine-local/e2e/links.spec.ts | 82 ++++++------- 25 files changed, 450 insertions(+), 136 deletions(-) create mode 100644 packages/frontend/core/src/blocksuite/block-suite-editor/doc-icon-picker.css.ts create mode 100644 packages/frontend/core/src/blocksuite/block-suite-editor/doc-icon-picker.tsx create mode 100644 packages/frontend/core/src/modules/explorer-icon/index.ts create mode 100644 packages/frontend/core/src/modules/explorer-icon/services/explorer-icon.ts create mode 100644 packages/frontend/core/src/modules/explorer-icon/store/explorer-icon.ts diff --git a/blocksuite/affine/fragments/doc-title/src/doc-title.ts b/blocksuite/affine/fragments/doc-title/src/doc-title.ts index acf3baab4..61c966c58 100644 --- a/blocksuite/affine/fragments/doc-title/src/doc-title.ts +++ b/blocksuite/affine/fragments/doc-title/src/doc-title.ts @@ -19,6 +19,7 @@ const DOC_BLOCK_CHILD_PADDING = 24; export class DocTitle extends WithDisposable(ShadowlessElement) { static override styles = css` + .doc-icon-container, .doc-title-container { box-sizing: border-box; font-family: var(--affine-font-family); @@ -49,6 +50,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) { /* Extra small devices (phones, 640px and down) */ @container viewport (width <= 640px) { + .doc-icon-container, .doc-title-container { padding-left: ${DOC_BLOCK_CHILD_PADDING}px; padding-right: ${DOC_BLOCK_CHILD_PADDING}px; diff --git a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts index 1446acdc0..01431acbc 100644 --- a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts +++ b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts @@ -14,13 +14,19 @@ export const contentRoot = style({ }); export const iconPicker = style({ - border: `1px solid ${cssVarV2.layer.insideBorder.border}`, - width: 32, - height: 32, padding: 0, +}); +globalStyle(`${iconPicker} span:has(svg)`, { + lineHeight: 0, +}); + +export const iconNamePickerIcon = style({ flexShrink: 0, fontSize: 24, borderRadius: 4, + width: 32, + height: 32, + border: `1px solid ${cssVarV2.layer.insideBorder.border}`, selectors: { '&[data-icon-type="emoji"]': { fontSize: 20, diff --git a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.stories.tsx b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.stories.tsx index 514b92f58..56f2f696f 100644 --- a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.stories.tsx +++ b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.stories.tsx @@ -16,10 +16,10 @@ export default { } satisfies Meta; export const Basic: StoryFn = () => { - const [icon, setIcon] = useState('👋'); + const [icon, setIcon] = useState('👋'); const [name, setName] = useState('Hello'); - const handleIconChange = useCallback((_: IconType, icon: string) => { + const handleIconChange = useCallback((_?: IconType, icon?: string) => { setIcon(icon); }, []); const handleNameChange = useCallback((name: string) => { diff --git a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.tsx b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.tsx index f477a933f..e177e6685 100644 --- a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.tsx +++ b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx'; import { useTheme } from 'next-themes'; import { type ReactNode, useCallback, useState } from 'react'; -import { Button } from '../button'; +import { Button, type ButtonProps } from '../button'; import Input from '../input'; import { Menu, type MenuProps } from '../menu'; import * as styles from './icon-name-editor.css'; @@ -12,11 +12,11 @@ import * as styles from './icon-name-editor.css'; export type IconType = 'emoji' | 'affine-icon' | 'blob'; export interface IconEditorProps { - iconType: IconType; - icon: string; + iconType?: IconType; + icon?: string; closeAfterSelect?: boolean; iconPlaceholder?: ReactNode; - onIconChange?: (type: IconType, icon: string) => void; + onIconChange?: (type?: IconType, icon?: string) => void; triggerClassName?: string; } @@ -24,6 +24,8 @@ export interface IconAndNameEditorContentProps extends IconEditorProps { name: string; namePlaceholder?: string; onNameChange?: (name: string) => void; + onEnter?: () => void; + inputTestId?: string; } export interface IconAndNameEditorMenuProps @@ -33,20 +35,23 @@ export interface IconAndNameEditorMenuProps onOpenChange?: (open: boolean) => void; width?: string | number; + skipIfNotChanged?: boolean; } -const IconRenderer = ({ +export const IconRenderer = ({ iconType, icon, + fallback, }: { iconType: IconType; icon: string; + fallback?: ReactNode; }) => { switch (iconType) { case 'emoji': - return
{icon}
; + return
{icon ?? fallback}
; default: - throw new Error(`Unsupported icon type: ${iconType}`); + return
{fallback}
; } }; @@ -59,9 +64,11 @@ export const IconEditor = ({ onIconChange, alignOffset, sideOffset = 4, + triggerVariant, }: IconEditorProps & { alignOffset?: number; sideOffset?: number; + triggerVariant?: ButtonProps['variant']; }) => { const [isPickerOpen, setIsPickerOpen] = useState(false); const { resolvedTheme } = useTheme(); @@ -99,11 +106,18 @@ export const IconEditor = ({ } >