refactor: move outline fragment to separate package (#10331)

### TL;DR

Moved outline functionality into a dedicated fragment package and updated vanilla-extract CSS dependency.

### What changed?

- Created new `@blocksuite/affine-fragment-outline` package
- Relocated outline-related code from presets to the new fragment package
- Updated imports across affected files to reference the new package location
- Upgraded `@vanilla-extract/css` dependency from 1.14.0/1.16.1 to 1.17.0
- Added necessary package configuration and TypeScript setup for the new fragment

### How to test?

1. Verify outline functionality works as expected in both desktop and mobile views
2. Check that outline panel, viewer, and mobile menu components render correctly
3. Ensure outline navigation and interactions continue to work
4. Confirm no regressions in outline-related features

### Why make this change?

This change improves code organization by isolating outline functionality into a dedicated package, following the modular architecture pattern. This makes the codebase more maintainable and allows for better separation of concerns. The vanilla-extract CSS upgrade ensures consistency across packages and provides access to the latest features and fixes.
This commit is contained in:
Saul-Mirone
2025-02-20 15:59:13 +00:00
parent 5ac15f12e6
commit b8dcb85007
41 changed files with 217 additions and 134 deletions

View File

@@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@preact/signals-core": "^1.8.0",
"@vanilla-extract/css": "^1.14.0",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
"yjs": "^13.6.21",
"zod": "^3.24.1"

View File

@@ -0,0 +1,45 @@
{
"name": "@blocksuite/affine-fragment-outline",
"description": "Outline fragment for BlockSuite.",
"type": "module",
"scripts": {
"build": "tsc",
"test:unit": "nx vite:test --run --passWithNoTests",
"test:unit:coverage": "nx vite:test --run --coverage",
"test:e2e": "playwright test"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-note": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
}

View File

@@ -1,18 +1,10 @@
import { effects } from '@blocksuite/affine-block-note/effects';
import { changeNoteDisplayMode } from '@blocksuite/affine-block-note';
import { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { matchModels } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement, SurfaceSelection } from '@blocksuite/block-std';
import {
changeNoteDisplayMode,
DocModeProvider,
matchModels,
NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/blocks';
import {
Bound,
noop,
SignalWatcher,
WithDisposable,
} from '@blocksuite/global/utils';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { consume } from '@lit/context';
import { effect, signal } from '@preact/signals-core';
@@ -22,8 +14,6 @@ import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { when } from 'lit/directives/when.js';
noop(effects);
import { type TocContext, tocContext } from '../config';
import type {
ClickBlockEvent,
@@ -79,10 +69,6 @@ export class OutlinePanelBody extends SignalWatcher(
return this.editor.doc;
}
private get edgeless() {
return this.editor.querySelector('affine-edgeless-root');
}
get viewportPadding(): [number, number, number, number] {
const fitPadding = this._context.fitPadding$.value;
return fitPadding.length === 4
@@ -93,9 +79,9 @@ export class OutlinePanelBody extends SignalWatcher(
}
private _deSelectNoteInEdgelessMode(note: NoteBlockModel) {
if (!this.edgeless) return;
const gfx = this.editor.std.get(GfxControllerIdentifier);
const selection = gfx.selection;
const { selection } = this.edgeless.service;
if (!selection.has(note.id)) return;
const selectedIds = selection.selectedIds.filter(id => id !== note.id);
selection.set({
@@ -116,18 +102,12 @@ export class OutlinePanelBody extends SignalWatcher(
}
private _fitToElement(e: FitViewEvent) {
const edgeless = this.edgeless;
if (!edgeless) return;
const gfx = this.editor.std.get(GfxControllerIdentifier);
const { block } = e.detail;
const bound = Bound.deserialize(block.xywh);
edgeless.service.viewport.setViewportByBound(
bound,
this.viewportPadding,
true
);
gfx.viewport.setViewportByBound(bound, this.viewportPadding, true);
}
// when display mode change to page only, we should de-select the note if it is selected in edgeless mode
@@ -200,6 +180,8 @@ export class OutlinePanelBody extends SignalWatcher(
private _selectNote(e: SelectEvent) {
const { selected, id, multiselect } = e.detail;
const gfx = this.editor.std.get(GfxControllerIdentifier);
const editorMode = this.editor.std.get(DocModeProvider).getEditorMode();
const note = this.doc.getBlock(id)?.model;
if (!note || !matchModels(note, [NoteBlockModel])) return;
@@ -213,8 +195,8 @@ export class OutlinePanelBody extends SignalWatcher(
selectedNotes = [note];
}
if (this.edgeless) {
this.edgeless?.service.selection.set({
if (editorMode === 'edgeless') {
gfx.selection.set({
elements: selectedNotes.map(({ id }) => id),
editing: false,
});

View File

@@ -1,9 +1,6 @@
import { type NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
import { createButtonPopper } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import {
createButtonPopper,
type NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { ArrowDownSmallIcon, InvisibleIcon } from '@blocksuite/icons/lit';
import type { BlockModel } from '@blocksuite/store';

View File

@@ -1,16 +1,16 @@
import type {
AttachmentBlockModel,
BookmarkBlockModel,
CodeBlockModel,
DatabaseBlockModel,
ImageBlockModel,
ListBlockModel,
ParagraphBlockModel,
RootBlockModel,
} from '@blocksuite/affine-model';
import { DocDisplayMetaProvider } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { ShadowlessElement } from '@blocksuite/block-std';
import {
type AttachmentBlockModel,
type BookmarkBlockModel,
type CodeBlockModel,
type DatabaseBlockModel,
DocDisplayMetaProvider,
type ImageBlockModel,
type ListBlockModel,
type ParagraphBlockModel,
type RootBlockModel,
} from '@blocksuite/blocks';
import { noop, SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { LinkedPageIcon } from '@blocksuite/icons/lit';
import type { DeltaInsert } from '@blocksuite/inline';

View File

@@ -1,5 +1,5 @@
import type { ParagraphBlockModel } from '@blocksuite/affine-model';
import type { EditorHost } from '@blocksuite/block-std';
import type { ParagraphBlockModel, Signal } from '@blocksuite/blocks';
import {
AttachmentIcon,
BlockIcon,
@@ -21,6 +21,7 @@ import {
TextIcon,
} from '@blocksuite/icons/lit';
import { createContext } from '@lit/context';
import type { Signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
const _16px = { width: '16px', height: '16px' };

View File

@@ -0,0 +1,39 @@
import { AFFINE_OUTLINE_NOTICE, OutlineNotice } from './body/outline-notice';
import {
AFFINE_OUTLINE_PANEL_BODY,
OutlinePanelBody,
} from './body/outline-panel-body';
import { AFFINE_OUTLINE_NOTE_CARD, OutlineNoteCard } from './card/outline-card';
import {
AFFINE_OUTLINE_BLOCK_PREVIEW,
OutlineBlockPreview,
} from './card/outline-preview';
import {
AFFINE_OUTLINE_PANEL_HEADER,
OutlinePanelHeader,
} from './header/outline-panel-header';
import {
AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU,
OutlineNotePreviewSettingMenu,
} from './header/outline-setting-menu';
import {
AFFINE_MOBILE_OUTLINE_MENU,
MobileOutlineMenu,
} from './mobile-outline-panel';
import { AFFINE_OUTLINE_PANEL, OutlinePanel } from './outline-panel';
import { AFFINE_OUTLINE_VIEWER, OutlineViewer } from './outline-viewer';
export function effects() {
customElements.define(
AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU,
OutlineNotePreviewSettingMenu
);
customElements.define(AFFINE_OUTLINE_NOTICE, OutlineNotice);
customElements.define(AFFINE_OUTLINE_PANEL, OutlinePanel);
customElements.define(AFFINE_OUTLINE_PANEL_HEADER, OutlinePanelHeader);
customElements.define(AFFINE_OUTLINE_NOTE_CARD, OutlineNoteCard);
customElements.define(AFFINE_OUTLINE_VIEWER, OutlineViewer);
customElements.define(AFFINE_MOBILE_OUTLINE_MENU, MobileOutlineMenu);
customElements.define(AFFINE_OUTLINE_BLOCK_PREVIEW, OutlineBlockPreview);
customElements.define(AFFINE_OUTLINE_PANEL_BODY, OutlinePanelBody);
}

View File

@@ -1,5 +1,5 @@
import { createButtonPopper } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import { createButtonPopper } from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { SettingsIcon, SortIcon } from '@blocksuite/icons/lit';
import { consume } from '@lit/context';

View File

@@ -1,16 +1,16 @@
import {
NoteDisplayMode,
ParagraphBlockModel,
RootBlockModel,
} from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { matchModels } from '@blocksuite/affine-shared/utils';
import {
type EditorHost,
PropTypes,
requiredProperties,
} from '@blocksuite/block-std';
import {
DocModeProvider,
matchModels,
NoteDisplayMode,
ParagraphBlockModel,
RootBlockModel,
} from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { signal } from '@preact/signals-core';

View File

@@ -1,10 +1,10 @@
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import {
type EditorHost,
PropTypes,
requiredProperties,
ShadowlessElement,
} from '@blocksuite/block-std';
import { DocModeProvider } from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { provide } from '@lit/context';
import { effect, signal } from '@preact/signals-core';

View File

@@ -1,14 +1,12 @@
import { NoteDisplayMode } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { scrollbarStyle } from '@blocksuite/affine-shared/styles';
import {
type EditorHost,
PropTypes,
requiredProperties,
ShadowlessElement,
} from '@blocksuite/block-std';
import {
DocModeProvider,
NoteDisplayMode,
scrollbarStyle,
} from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { TocIcon } from '@blocksuite/icons/lit';
import { provide } from '@lit/context';

View File

@@ -1,4 +1,4 @@
import type { NoteBlockModel, NoteDisplayMode } from '@blocksuite/blocks';
import type { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
export type ReorderEvent = CustomEvent<{
currentNumber: number;

View File

@@ -1,11 +1,10 @@
import {
BlocksUtils,
matchModels,
NoteBlockModel,
NoteDisplayMode,
ParagraphBlockModel,
RootBlockModel,
} from '@blocksuite/blocks';
} from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockModel, Store } from '@blocksuite/store';
import { headingKeys } from '../config.js';
@@ -35,14 +34,14 @@ export function getNotesFromDoc(
}
export function isRootBlock(block: BlockModel): block is RootBlockModel {
return BlocksUtils.matchModels(block, [RootBlockModel]);
return matchModels(block, [RootBlockModel]);
}
export function isHeadingBlock(
block: BlockModel
): block is ParagraphBlockModel {
return (
BlocksUtils.matchModels(block, [ParagraphBlockModel]) &&
matchModels(block, [ParagraphBlockModel]) &&
headingKeys.has(block.type$.value)
);
}

View File

@@ -1,9 +1,8 @@
import { getDocTitleByEditorHost } from '@blocksuite/affine-components/doc-title';
import { NoteDisplayMode } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import type { Viewport } from '@blocksuite/affine-shared/types';
import type { EditorHost } from '@blocksuite/block-std';
import {
DocModeProvider,
getDocTitleByEditorHost,
NoteDisplayMode,
} from '@blocksuite/blocks';
import { clamp, DisposableGroup } from '@blocksuite/global/utils';
import { getHeadingBlocksFromDoc } from './query.js';
@@ -13,7 +12,7 @@ export function scrollToBlock(host: EditorHost, blockId: string) {
const mode = docModeService.getEditorMode();
if (mode === 'edgeless') return;
if (editor.doc.root?.id === blockId) {
if (host.doc.root?.id === blockId) {
const docTitle = getDocTitleByEditorHost(host);
if (!docTitle) return;
@@ -89,7 +88,9 @@ function highlightBlock(host: EditorHost, blockId: string) {
if (host.doc.root?.id === blockId) return emptyClear;
const rootComponent = host.querySelector('affine-page-root');
const rootComponent = host.querySelector<
HTMLElement & { viewport: Viewport }
>('affine-page-root');
if (!rootComponent) return emptyClear;
if (!rootComponent.viewport) {

View File

@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./src"],
"references": [
{ "path": "../block-note" },
{ "path": "../components" },
{ "path": "../model" },
{ "path": "../shared" },
{ "path": "../../framework/block-std" },
{ "path": "../../framework/global" },
{ "path": "../../framework/inline" },
{ "path": "../../framework/store" }
]
}

View File

@@ -33,6 +33,7 @@
"@blocksuite/affine-block-table": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-fragment-frame-panel": "workspace:*",
"@blocksuite/affine-fragment-outline": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-drag-handle": "workspace:*",

View File

@@ -34,6 +34,7 @@ import { effects as componentToggleButtonEffects } from '@blocksuite/affine-comp
import { ToggleSwitch } from '@blocksuite/affine-components/toggle-switch';
import { effects as componentToolbarEffects } from '@blocksuite/affine-components/toolbar';
import { effects as fragmentFramePanelEffects } from '@blocksuite/affine-fragment-frame-panel/effects';
import { effects as fragmentOutlineEffects } from '@blocksuite/affine-fragment-outline/effects';
import { effects as widgetDragHandleEffects } from '@blocksuite/affine-widget-drag-handle/effects';
import { effects as widgetEdgelessAutoConnectEffects } from '@blocksuite/affine-widget-edgeless-auto-connect/effects';
import { effects as widgetFrameTitleEffects } from '@blocksuite/affine-widget-frame-title/effects';
@@ -229,6 +230,7 @@ export function effects() {
widgetEdgelessAutoConnectEffects();
fragmentFramePanelEffects();
fragmentOutlineEffects();
customElements.define('affine-page-root', PageRootBlockComponent);
customElements.define('affine-preview-root', PreviewRootBlockComponent);

View File

@@ -91,6 +91,7 @@ export {
Tooltip,
} from '@blocksuite/affine-components/toolbar';
export * from '@blocksuite/affine-fragment-frame-panel';
export * from '@blocksuite/affine-fragment-outline';
export * from '@blocksuite/affine-model';
export {
AttachmentAdapter,

View File

@@ -26,6 +26,7 @@
{ "path": "../affine/block-table" },
{ "path": "../affine/components" },
{ "path": "../affine/fragment-frame-panel" },
{ "path": "../affine/fragment-outline" },
{ "path": "../affine/model" },
{ "path": "../affine/shared" },
{ "path": "../affine/widget-drag-handle" },

View File

@@ -7,56 +7,12 @@ import {
PageEditor,
} from './editors/index.js';
import { CommentInput } from './fragments/comment/comment-input.js';
import {
AFFINE_MOBILE_OUTLINE_MENU,
AFFINE_OUTLINE_PANEL,
AFFINE_OUTLINE_VIEWER,
CommentPanel,
MobileOutlineMenu,
OutlinePanel,
OutlineViewer,
} from './fragments/index.js';
import {
AFFINE_OUTLINE_NOTICE,
OutlineNotice,
} from './fragments/outline/body/outline-notice.js';
import {
AFFINE_OUTLINE_PANEL_BODY,
OutlinePanelBody,
} from './fragments/outline/body/outline-panel-body.js';
import {
AFFINE_OUTLINE_NOTE_CARD,
OutlineNoteCard,
} from './fragments/outline/card/outline-card.js';
import {
AFFINE_OUTLINE_BLOCK_PREVIEW,
OutlineBlockPreview,
} from './fragments/outline/card/outline-preview.js';
import {
AFFINE_OUTLINE_PANEL_HEADER,
OutlinePanelHeader,
} from './fragments/outline/header/outline-panel-header.js';
import {
AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU,
OutlineNotePreviewSettingMenu,
} from './fragments/outline/header/outline-setting-menu.js';
import { CommentPanel } from './fragments/index.js';
export function effects() {
customElements.define('page-editor', PageEditor);
customElements.define('comment-input', CommentInput);
customElements.define(
AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU,
OutlineNotePreviewSettingMenu
);
customElements.define(AFFINE_OUTLINE_NOTICE, OutlineNotice);
customElements.define('comment-panel', CommentPanel);
customElements.define(AFFINE_OUTLINE_PANEL, OutlinePanel);
customElements.define(AFFINE_OUTLINE_PANEL_HEADER, OutlinePanelHeader);
customElements.define('affine-editor-container', AffineEditorContainer);
customElements.define(AFFINE_OUTLINE_NOTE_CARD, OutlineNoteCard);
customElements.define('edgeless-editor', EdgelessEditor);
customElements.define(AFFINE_OUTLINE_VIEWER, OutlineViewer);
customElements.define(AFFINE_MOBILE_OUTLINE_MENU, MobileOutlineMenu);
customElements.define(AFFINE_OUTLINE_BLOCK_PREVIEW, OutlineBlockPreview);
customElements.define(AFFINE_OUTLINE_PANEL_BODY, OutlinePanelBody);
}

View File

@@ -1,2 +1 @@
export * from './comment/index.js';
export * from './outline/index.js';