diff --git a/blocksuite/presets/package.json b/blocksuite/presets/package.json index 5f10f56618..80da8d40ec 100644 --- a/blocksuite/presets/package.json +++ b/blocksuite/presets/package.json @@ -18,12 +18,15 @@ "@blocksuite/block-std": "workspace:*", "@blocksuite/blocks": "workspace:*", "@blocksuite/global": "workspace:*", + "@blocksuite/icons": "^2.2.2", "@blocksuite/inline": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.10", + "@lit/context": "^1.1.3", "@lottiefiles/dotlottie-wc": "^0.4.0", "@preact/signals-core": "^1.8.0", "@toeverything/theme": "^1.1.7", + "@vanilla-extract/css": "^1.17.0", "lit": "^3.2.0", "yjs": "^13.6.21", "zod": "^3.23.8" diff --git a/blocksuite/presets/src/fragments/outline/body/outline-notice.css.ts b/blocksuite/presets/src/fragments/outline/body/outline-notice.css.ts new file mode 100644 index 0000000000..86417b17e8 --- /dev/null +++ b/blocksuite/presets/src/fragments/outline/body/outline-notice.css.ts @@ -0,0 +1,86 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const outlineNotice = style({ + position: 'absolute', + left: 0, + bottom: '8px', + padding: '10px 18px', + display: 'flex', + width: '100%', + boxSizing: 'border-box', + gap: '14px', + fontStyle: 'normal', + fontSize: '12px', + flexDirection: 'column', + borderRadius: '8px', + backgroundColor: cssVar('--affine-background-overlay-panel-color'), +}); + +export const outlineNoticeHeader = style({ + display: 'flex', + width: '100%', + height: '20px', + alignItems: 'center', + justifyContent: 'space-between', +}); + +export const outlineNoticeLabel = style({ + fontWeight: 600, + lineHeight: '20px', + color: cssVarV2('text/secondary'), +}); + +export const outlineNoticeCloseButton = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '20px', + height: '20px', + cursor: 'pointer', + color: cssVarV2('icon/primary'), +}); + +export const outlineNoticeBody = style({ + display: 'flex', + width: '100%', + gap: '2px', + flexDirection: 'column', +}); + +const outlineNoticeItem = style({ + display: 'flex', + height: '20px', + alignItems: 'center', + lineHeight: '20px', + color: cssVarV2('text/primary'), +}); + +export const notice = style([ + outlineNoticeItem, + { + fontWeight: 400, + }, +]); + +export const button = style([ + outlineNoticeItem, + { + display: 'flex', + gap: '2px', + fontWeight: 500, + textDecoration: 'underline', + cursor: 'pointer', + }, +]); + +export const buttonSpan = style({ + display: 'flex', + alignItems: 'center', + lineHeight: '20px', +}); + +export const buttonSvg = style({ + scale: 0.8, +}); diff --git a/blocksuite/presets/src/fragments/outline/body/outline-notice.ts b/blocksuite/presets/src/fragments/outline/body/outline-notice.ts index 041a805d89..ccbd1f09e4 100644 --- a/blocksuite/presets/src/fragments/outline/body/outline-notice.ts +++ b/blocksuite/presets/src/fragments/outline/body/outline-notice.ts @@ -1,89 +1,14 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; import { WithDisposable } from '@blocksuite/global/utils'; -import { css, html, LitElement, nothing } from 'lit'; +import { html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { SmallCloseIcon, SortingIcon } from '../../_common/icons.js'; - -const styles = css` - :host { - width: 100%; - box-sizing: border-box; - position: absolute; - left: 0; - bottom: 8px; - padding: 0 8px; - } - .outline-notice-container { - display: flex; - width: 100%; - box-sizing: border-box; - gap: 14px; - padding: 10px; - font-style: normal; - font-size: 12px; - flex-direction: column; - border-radius: 8px; - background-color: var(--affine-background-overlay-panel-color); - } - .outline-notice-header { - display: flex; - width: 100%; - height: 20px; - align-items: center; - justify-content: space-between; - } - .outline-notice-label { - font-weight: 600; - line-height: 20px; - color: var(--affine-text-secondary-color); - } - .outline-notice-close-button { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - cursor: pointer; - color: var(--affine-icon-color); - } - .outline-notice-body { - display: flex; - width: 100%; - gap: 2px; - flex-direction: column; - } - .outline-notice-item { - display: flex; - height: 20px; - align-items: center; - line-height: 20px; - color: var(--affine-text-primary-color); - } - .outline-notice-item.notice { - font-weight: 400; - } - .outline-notice-item.button { - display: flex; - gap: 2px; - font-weight: 500; - text-decoration: underline; - cursor: pointer; - } - .outline-notice-item.button span { - display: flex; - align-items: center; - line-height: 20px; - } - .outline-notice-item.button svg { - scale: 0.8; - } -`; +import * as styles from './outline-notice.css'; export const AFFINE_OUTLINE_NOTICE = 'affine-outline-notice'; -export class OutlineNotice extends WithDisposable(LitElement) { - static override styles = styles; - +export class OutlineNotice extends WithDisposable(ShadowlessElement) { private _handleNoticeButtonClick() { this.toggleNotesSorting(); this.setNoticeVisibility(false); @@ -94,29 +19,28 @@ export class OutlineNotice extends WithDisposable(LitElement) { return nothing; } - return html`
-
- SOME CONTENTS HIDDEN - this.setNoticeVisibility(false)} - >${SmallCloseIcon} -
-
-
- Some contents are not visible on edgeless. + return html` +
+
+ SOME CONTENTS HIDDEN + this.setNoticeVisibility(false)} + >${SmallCloseIcon}
-
- Click here or - ${SortingIcon} - to organize content. +
+
+ Some contents are not visible on edgeless. +
+
+ Click here or + ${SortingIcon} + to organize content. +
-
`; + `; } @property({ attribute: false }) diff --git a/blocksuite/presets/src/fragments/outline/body/outline-panel-body.css.ts b/blocksuite/presets/src/fragments/outline/body/outline-panel-body.css.ts new file mode 100644 index 0000000000..1c82a5d1f5 --- /dev/null +++ b/blocksuite/presets/src/fragments/outline/body/outline-panel-body.css.ts @@ -0,0 +1,62 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const outlinePanelBody = style({ + position: 'relative', + alignItems: 'start', + boxSizing: 'border-box', + width: '100%', + height: '100%', + padding: '0 8px', + flexGrow: 1, + overflowY: 'scroll', +}); + +export const cardList = style({ + position: 'relative', +}); + +export const edgelessCardListTitle = style({ + width: '100%', + fontSize: '14px', + lineHeight: '24px', + fontWeight: 500, + color: cssVarV2('text/secondary'), + paddingLeft: '8px', + height: '40px', + boxSizing: 'border-box', + padding: '6px 8px', + marginTop: '8px', +}); + +export const insertIndicator = style({ + height: '2px', + borderRadius: '1px', + backgroundColor: cssVar('brandColor'), + position: 'absolute', + top: 0, + left: 0, + right: 0, + contain: 'layout size', + width: '100%', +}); + +export const emptyPanel = style({ + display: 'flex', + flexDirection: 'column', + width: '100%', +}); + +export const emptyPanelPlaceholder = style({ + marginTop: '240px', + alignSelf: 'center', + width: '190px', + height: '48px', + color: cssVarV2('text/secondary'), + textAlign: 'center', + fontSize: '15px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '24px', +}); diff --git a/blocksuite/presets/src/fragments/outline/body/outline-panel-body.ts b/blocksuite/presets/src/fragments/outline/body/outline-panel-body.ts index fe794940ca..9fa67bdf3c 100644 --- a/blocksuite/presets/src/fragments/outline/body/outline-panel-body.ts +++ b/blocksuite/presets/src/fragments/outline/body/outline-panel-body.ts @@ -1,13 +1,9 @@ -import { SurfaceSelection } from '@blocksuite/block-std'; -import type { - EdgelessRootBlockComponent, - NoteBlockModel, -} from '@blocksuite/blocks'; +import { ShadowlessElement, SurfaceSelection } from '@blocksuite/block-std'; +import type { NoteBlockModel } from '@blocksuite/blocks'; import { BlocksUtils, matchFlavours, NoteDisplayMode, - ThemeProvider, } from '@blocksuite/blocks'; import { Bound, @@ -15,30 +11,33 @@ import { SignalWatcher, WithDisposable, } from '@blocksuite/global/utils'; -import type { Store } from '@blocksuite/store'; +import { consume } from '@lit/context'; import { effect, signal } from '@preact/signals-core'; -import { css, html, LitElement, nothing, type PropertyValues } from 'lit'; -import { property, query, state } from 'lit/decorators.js'; +import { html, nothing, type PropertyValues } from 'lit'; +import { property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { repeat } from 'lit/directives/repeat.js'; +import { when } from 'lit/directives/when.js'; -import type { AffineEditorContainer } from '../../../editors/editor-container.js'; +import type { AffineEditorContainer } from '../../../editors/editor-container'; +import { editorContext } from '../config'; import type { ClickBlockEvent, DisplayModeChangeEvent, FitViewEvent, SelectEvent, -} from '../utils/custom-events.js'; -import { startDragging } from '../utils/drag.js'; +} from '../utils/custom-events'; +import { startDragging } from '../utils/drag'; import { getHeadingBlocksFromDoc, getNotesFromDoc, isHeadingBlock, -} from '../utils/query.js'; +} from '../utils/query'; import { observeActiveHeadingDuringScroll, scrollToBlockWithHighlight, -} from '../utils/scroll.js'; +} from '../utils/scroll'; +import * as styles from './outline-panel-body.css'; type OutlineNoteItem = { note: NoteBlockModel; @@ -54,76 +53,19 @@ type OutlineNoteItem = { number: number; }; -const styles = css` - .outline-panel-body-container { - position: relative; - display: flex; - align-items: start; - box-sizing: border-box; - flex-direction: column; - width: 100%; - height: 100%; - padding: 0 8px; - } - - .panel-list { - position: relative; - width: 100%; - } - - .panel-list .hidden-title { - width: 100%; - font-size: 14px; - line-height: 24px; - font-weight: 500; - color: var(--affine-text-secondary-color); - padding-left: 8px; - height: 40px; - box-sizing: border-box; - padding: 6px 8px; - margin-top: 8px; - } - - .insert-indicator { - height: 2px; - border-radius: 1px; - background-color: var(--affine-brand-color); - border-radius: 1px; - position: absolute; - contain: layout size; - width: 100%; - } - - .no-note-container { - display: flex; - flex-direction: column; - width: 100%; - } - - .note-placeholder { - margin-top: 240px; - align-self: center; - width: 190px; - height: 48px; - color: var(--affine-text-secondary-color, #8e8d91); - text-align: center; - /* light/base */ - font-size: 15px; - font-style: normal; - font-weight: 400; - line-height: 24px; - } -`; - export const AFFINE_OUTLINE_PANEL_BODY = 'affine-outline-panel-body'; export class OutlinePanelBody extends SignalWatcher( - WithDisposable(LitElement) + WithDisposable(ShadowlessElement) ) { - static override styles = styles; - private readonly _activeHeadingId$ = signal(null); + private readonly _dragging$ = signal(false); + + private readonly _pageVisibleNoteItems$ = signal([]); + + private readonly _edgelessOnlyNoteItems$ = signal([]); + private _clearHighlightMask = () => {}; private _docDisposables: DisposableGroup | null = null; @@ -132,6 +74,21 @@ export class OutlinePanelBody extends SignalWatcher( private _lockActiveHeadingId = false; + private get _shouldRenderEmptyPanel() { + return ( + this._pageVisibleNoteItems$.value.length === 0 && + this._edgelessOnlyNoteItems$.value.length === 0 + ); + } + + private get doc() { + return this.editor.doc; + } + + private get edgeless() { + return this.editor.querySelector('affine-edgeless-root'); + } + get viewportPadding(): [number, number, number, number] { return this.fitPadding ? ([0, 0, 0, 0].map((val, idx) => @@ -174,6 +131,8 @@ export class OutlinePanelBody extends SignalWatcher( }; private _drag() { + const pageVisibleNotes = this._pageVisibleNoteItems$.peek(); + const selectedVisibleNotes = this._selectedNotes$.peek().filter(id => { const model = this.doc.getBlock(id)?.model; return ( @@ -185,7 +144,7 @@ export class OutlinePanelBody extends SignalWatcher( if ( selectedVisibleNotes.length === 0 || - !this._pageVisibleNotes.length || + !pageVisibleNotes.length || !this.doc.root ) return; @@ -199,12 +158,11 @@ export class OutlinePanelBody extends SignalWatcher( this._selectedNotes$.value = selectedVisibleNotes; } - this._dragging = true; + this._dragging$.value = true; // cache the notes in case it is changed by other peers const children = this.doc.root.children.slice() as NoteBlockModel[]; - const notes = this._pageVisibleNotes; - const notesMap = this._pageVisibleNotes.reduce((map, note, index) => { + const notesMap = pageVisibleNotes.reduce((map, note, index) => { map.set(note.note.id, { ...note, number: index + 1, @@ -215,11 +173,11 @@ export class OutlinePanelBody extends SignalWatcher( startDragging({ container: this, document: this.ownerDocument, - host: this.domHost ?? this.ownerDocument, + host: this.ownerDocument, doc: this.doc, - outlineListContainer: this.panelListElement, + outlineListContainer: this._pageVisibleList, onDragEnd: insertIdx => { - this._dragging = false; + this._dragging$.value = false; this.insertIndex = undefined; if (insertIdx === undefined) return; @@ -228,7 +186,7 @@ export class OutlinePanelBody extends SignalWatcher( insertIdx, selectedVisibleNotes, notesMap, - notes, + pageVisibleNotes, children ); }, @@ -240,8 +198,11 @@ export class OutlinePanelBody extends SignalWatcher( } private _EmptyPanel() { - return html`
-
+ return html`
+
Use headings to create a table of contents.
`; @@ -330,77 +291,6 @@ export class OutlinePanelBody extends SignalWatcher( }); } - private _PanelList(withEdgelessOnlyNotes: boolean) { - const selectedNotesSet = new Set(this._selectedNotes$.value); - const theme = this.editor.std.get(ThemeProvider).theme; - - return html`
- ${this.insertIndex !== undefined - ? html`
` - : nothing} - ${this._pageVisibleNotes.length - ? repeat( - this._pageVisibleNotes, - item => item.note.id, - (item, idx) => html` - { - this._scrollToBlock(e.detail.blockId).catch(console.error); - }} - @displaymodechange=${this._handleDisplayModeChange} - > - ` - ) - : html`${nothing}`} - ${withEdgelessOnlyNotes - ? html`
Hidden Contents
- ${repeat( - this._edgelessOnlyNotes, - note => note.note.id, - (item, idx) => - html`` - )} ` - : nothing} -
`; - } - private _renderDocTitle() { if (!this.doc.root) return nothing; @@ -431,6 +321,61 @@ export class OutlinePanelBody extends SignalWatcher( >`; } + private _renderNoteCards(items: OutlineNoteItem[]) { + return repeat( + items, + item => item.note.id, + (item, idx) => + html` { + this._scrollToBlock(e.detail.blockId).catch(console.error); + }} + >` + ); + } + + private _renderPageVisibleCardList() { + return html`
+ ${when( + this.insertIndex !== undefined, + () => + html`
` + )} + ${this._renderNoteCards(this._pageVisibleNoteItems$.value)} +
`; + } + + private _renderEdgelessOnlyCardList() { + const items = this._edgelessOnlyNoteItems$.value; + return html`
+ ${when( + items.length > 0, + () => + html`
Hidden Contents
` + )} + ${this._renderNoteCards(items)} +
`; + } + private async _scrollToBlock(blockId: string) { this._lockActiveHeadingId = true; this._activeHeadingId$.value = blockId; @@ -471,69 +416,41 @@ export class OutlinePanelBody extends SignalWatcher( this._docDisposables = new DisposableGroup(); this._docDisposables.add( effect(() => { - this._updateNotes(); this._updateNoticeVisibility(); }) ); + this._docDisposables.add( - this.doc.slots.blockUpdated.on(payload => { - if ( - payload.type === 'update' && - payload.flavour === 'affine:note' && - payload.props.key === 'displayMode' - ) { - this._updateNotes(); - } + effect(() => { + this._updateNotes(); }) ); } - /** - * There are two cases that we should render note list: - * 1. There are headings in the notes - * 2. No headings, but there are blocks in the notes and note sorting option is enabled - */ - private _shouldRenderNoteList(noteItems: OutlineNoteItem[]) { - if (!noteItems.length) return false; + private _updateNotes() { + if (this._dragging$.value) return; - let hasHeadings = false; - let hasChildrenBlocks = false; - - for (const noteItem of noteItems) { - for (const block of noteItem.note.children) { - hasChildrenBlocks = true; + const isRenderableNote = (item: OutlineNoteItem) => { + let hasHeadings = false; + for (const block of item.note.children) { if (isHeadingBlock(block)) { hasHeadings = true; break; } } - if (hasHeadings) { - break; - } - } + return hasHeadings || this.enableNotesSorting; + }; - return hasHeadings || (this.enableNotesSorting && hasChildrenBlocks); - } - - private _updateNotes() { - const rootModel = this.doc.root; - - if (this._dragging) return; - - if (!rootModel) { - this._pageVisibleNotes = []; - return; - } - - this._pageVisibleNotes = getNotesFromDoc(this.doc, [ + this._pageVisibleNoteItems$.value = getNotesFromDoc(this.doc, [ NoteDisplayMode.DocAndEdgeless, NoteDisplayMode.DocOnly, - ]); - this._edgelessOnlyNotes = getNotesFromDoc(this.doc, [ + ]).filter(isRenderableNote); + + this._edgelessOnlyNoteItems$.value = getNotesFromDoc(this.doc, [ NoteDisplayMode.EdgelessOnly, - ]); + ]).filter(isRenderableNote); } private _updateNoticeVisibility() { @@ -544,9 +461,8 @@ export class OutlinePanelBody extends SignalWatcher( return; } - const shouldShowNotice = this._pageVisibleNotes.some( - note => note.note.displayMode === NoteDisplayMode.DocOnly - ); + const shouldShowNotice = + getNotesFromDoc(this.doc, [NoteDisplayMode.DocOnly]).length > 0; if (shouldShowNotice && !this.noticeVisible) { this.setNoticeVisibility(true); @@ -580,6 +496,8 @@ export class OutlinePanelBody extends SignalWatcher( override connectedCallback(): void { super.connectedCallback(); + this.classList.add(styles.outlinePanelBody); + this.disposables.add( observeActiveHeadingDuringScroll( () => this.editor, @@ -603,54 +521,33 @@ export class OutlinePanelBody extends SignalWatcher( } override render() { - const shouldRenderPageVisibleNotes = this._shouldRenderNoteList( - this._pageVisibleNotes - ); - const shouldRenderEdgelessOnlyNotes = - this.renderEdgelessOnlyNotes && - this._shouldRenderNoteList(this._edgelessOnlyNotes); - - const shouldRenderEmptyPanel = - !shouldRenderPageVisibleNotes && !shouldRenderEdgelessOnlyNotes; - return html` -
- ${this._renderDocTitle()} - ${shouldRenderEmptyPanel - ? this._EmptyPanel() - : this._PanelList(shouldRenderEdgelessOnlyNotes)} -
+ ${this._renderDocTitle()} + ${when( + this._shouldRenderEmptyPanel, + () => this._EmptyPanel(), + () => html` + ${this._renderPageVisibleCardList()} + ${this._renderEdgelessOnlyCardList()} + ` + )} `; } override willUpdate(_changedProperties: PropertyValues) { - if (_changedProperties.has('doc') || _changedProperties.has('edgeless')) { + if (_changedProperties.has('editor')) { this._setDocDisposables(); } - if (_changedProperties.has('edgeless')) { - this._clearHighlightMask(); + if (_changedProperties.has('enableNotesSorting')) { + this._updateNoticeVisibility(); } } - @state() - private accessor _dragging = false; - - @state() - private accessor _edgelessOnlyNotes: OutlineNoteItem[] = []; - - @state() - private accessor _pageVisibleNotes: OutlineNoteItem[] = []; - - @property({ attribute: false }) - accessor doc!: Store; - - @property({ attribute: false }) - accessor domHost!: Document | HTMLElement; - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent | null; + @query('.page-visible-card-list') + private accessor _pageVisibleList!: HTMLElement; + @consume({ context: editorContext }) @property({ attribute: false }) accessor editor!: AffineEditorContainer; @@ -666,12 +563,6 @@ export class OutlinePanelBody extends SignalWatcher( @property({ attribute: false }) accessor noticeVisible!: boolean; - @query('.outline-panel-body-container') - accessor OutlinePanelContainer!: HTMLElement; - - @query('.panel-list') - accessor panelListElement!: HTMLElement; - @property({ attribute: false }) accessor renderEdgelessOnlyNotes: boolean = true; diff --git a/blocksuite/presets/src/fragments/outline/card/outline-card.css.ts b/blocksuite/presets/src/fragments/outline/card/outline-card.css.ts new file mode 100644 index 0000000000..38e4919dff --- /dev/null +++ b/blocksuite/presets/src/fragments/outline/card/outline-card.css.ts @@ -0,0 +1,162 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const outlineCard = style({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + boxSizing: 'border-box', + + selectors: { + '&[data-status="placeholder"]': { + pointerEvents: 'none', + opacity: 0.5, + }, + '&[data-sortable="true"]': { + padding: '2px 0px', + }, + }, +}); + +export const cardPreview = style({ + position: 'relative', + width: '100%', + borderRadius: '4px', + cursor: 'default', + userSelect: 'none', + ':hover': { + background: cssVarV2('layer/background/hoverOverlay'), + }, + selectors: { + [`${outlineCard}[data-status="selected"] &`]: { + background: cssVarV2('layer/background/hoverOverlay'), + }, + [`${outlineCard}[data-status="placeholder"] &`]: { + background: cssVarV2('layer/background/hoverOverlay'), + opacity: 0.9, + }, + }, +}); + +export const cardHeader = style({ + padding: '0 8px', + width: '100%', + minHeight: '28px', + display: 'none', + alignItems: 'center', + gap: '8px', + boxSizing: 'border-box', + selectors: { + [`${outlineCard}[data-sortable="true"] &`]: { + display: 'flex', + }, + [`${outlineCard}[data-invisible="false"] &:hover`]: { + cursor: 'grab', + }, + }, +}); + +const invisibleCard = style({ + selectors: { + [`${outlineCard}[data-invisible="true"] &`]: { + color: cssVarV2('text/disable'), + pointerEvents: 'none', + }, + }, +}); + +export const headerIcon = style([ + { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + invisibleCard, +]); + +export const headerNumber = style([ + { + textAlign: 'center', + fontSize: cssVar('fontSm'), + color: cssVar('brandColor'), + fontWeight: 500, + lineHeight: '14px', + }, + invisibleCard, +]); + +export const divider = style({ + height: '1px', + flex: 1, + borderTop: `1px dashed ${cssVar('borderColor')}`, + transform: 'translateY(50%)', +}); + +export const displayModeButtonGroup = style({ + display: 'none', + position: 'absolute', + right: '8px', + top: '-6px', + paddingTop: '8px', + paddingBottom: '8px', + alignItems: 'center', + gap: '4px', + fontSize: '12px', + fontWeight: 500, + lineHeight: '20px', + + selectors: { + [`${cardPreview}:hover &`]: { + display: 'flex', + }, + }, +}); + +export const displayModeButton = style({ + display: 'flex', + borderRadius: '4px', + backgroundColor: cssVar('hoverColor'), + alignItems: 'center', +}); + +export const currentModeLabel = style({ + display: 'flex', + padding: '2px 0px 2px 4px', + alignItems: 'center', +}); + +export const cardContent = style([ + { + fontFamily: cssVar('fontSansFamily'), + userSelect: 'none', + color: cssVarV2('text/primary'), + + ':hover': { + cursor: 'pointer', + }, + }, + invisibleCard, +]); + +export const modeChangePanel = style({ + position: 'absolute', + display: 'none', + background: cssVarV2('layer/background/overlayPanel'), + borderRadius: '8px', + boxShadow: cssVar('shadow2'), + boxSizing: 'border-box', + padding: '8px', + fontSize: cssVar('fontSm'), + color: cssVarV2('text/primary'), + lineHeight: '22px', + fontWeight: 400, + fontFamily: cssVar('fontSansFamily'), + + selectors: { + '&[data-show]': { + display: 'flex', + }, + }, +}); diff --git a/blocksuite/presets/src/fragments/outline/card/outline-card.ts b/blocksuite/presets/src/fragments/outline/card/outline-card.ts index 7ebd2dd10e..811f4ff4aa 100644 --- a/blocksuite/presets/src/fragments/outline/card/outline-card.ts +++ b/blocksuite/presets/src/fragments/outline/card/outline-card.ts @@ -1,5 +1,5 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; import { - type ColorScheme, createButtonPopper, type NoteBlockModel, NoteDisplayMode, @@ -7,189 +7,19 @@ import { once, } from '@blocksuite/blocks'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; -import type { BlockModel, Store } from '@blocksuite/store'; -import { baseTheme } from '@toeverything/theme'; -import { css, html, LitElement, unsafeCSS } from 'lit'; +import type { BlockModel } from '@blocksuite/store'; +import { html } from 'lit'; import { property, query, state } from 'lit/decorators.js'; -import { classMap } from 'lit/directives/class-map.js'; -import { HiddenIcon, SmallArrowDownIcon } from '../../_common/icons.js'; -import type { SelectEvent } from '../utils/custom-events.js'; - -const styles = css` - :host { - display: block; - position: relative; - } - - .card-container { - position: relative; - - display: flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - } - - .card-preview { - position: relative; - - width: 100%; - - border-radius: 4px; - - cursor: default; - user-select: none; - } - - .card-preview:hover { - background: var(--affine-hover-color); - } - - .card-header-container { - padding: 0 8px; - width: 100%; - min-height: 28px; - display: none; - align-items: center; - gap: 8px; - box-sizing: border-box; - } - - .card-header-container.enable-sorting { - display: flex; - } - - .card-header-container .card-number { - text-align: center; - font-size: var(--affine-font-sm); - font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; - color: var(--affine-brand-color, #1e96eb); - font-weight: 500; - line-height: 14px; - line-height: 20px; - } - - .card-header-container .card-header-icon { - display: flex; - align-items: center; - justify-content: center; - } - - .card-header-container .card-divider { - height: 1px; - flex: 1; - border-top: 1px dashed var(--affine-border-color); - transform: translateY(50%); - } - - .display-mode-button-group { - display: none; - position: absolute; - right: 8px; - top: -6px; - padding-top: 8px; - padding-bottom: 8px; - align-items: center; - gap: 4px; - font-size: 12px; - font-weight: 500; - line-height: 20px; - } - - .card-preview:hover .display-mode-button-group { - display: flex; - } - - .display-mode-button-label { - color: var(--affine-text-primary-color); - } - - .display-mode-button { - display: flex; - border-radius: 4px; - background-color: var(--affine-hover-color); - align-items: center; - } - - .current-mode-label { - display: flex; - padding: 2px 0px 2px 4px; - align-items: center; - } - - note-display-mode-panel { - position: absolute; - display: none; - background: var(--affine-background-overlay-panel-color); - border-radius: 8px; - box-shadow: var(--affine-shadow-2); - box-sizing: border-box; - padding: 8px; - font-size: var(--affine-font-sm); - color: var(--affine-text-primary-color); - line-height: 22px; - font-weight: 400; - font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; - } - - note-display-mode-panel[data-show] { - display: flex; - } - - .card-content { - font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; - user-select: none; - color: var(--affine-text-primary-color); - } - - .card-preview .card-content:hover { - cursor: pointer; - } - - .card-container[data-invisible='false'] - .card-preview - .card-header-container:hover { - cursor: grab; - } - - .card-container.placeholder { - pointer-events: none; - opacity: 0.5; - } - - .card-container.selected .card-preview { - background: var(--affine-hover-color); - } - - .card-container.placeholder .card-preview { - background: var(--affine-hover-color); - opacity: 0.9; - } - - .card-container[data-sortable='true'] { - padding: 2px 0; - } - - .card-container[data-invisible='true'] .card-header-container .card-number, - .card-container[data-invisible='true'] - .card-header-container - .card-header-icon, - .card-container[data-invisible='true'] .card-preview .card-content { - color: var(--affine-text-disable-color); - pointer-events: none; - } - - .card-preview outline-block-preview:hover { - color: var(--affine-brand-color); - } -`; +import { HiddenIcon, SmallArrowDownIcon } from '../../_common/icons'; +import type { SelectEvent } from '../utils/custom-events'; +import * as styles from './outline-card.css'; export const AFFINE_OUTLINE_NOTE_CARD = 'affine-outline-note-card'; -export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) { - static override styles = styles; - +export class OutlineNoteCard extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { private _displayModePopper: ReturnType | null = null; @@ -283,6 +113,10 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) { } } + get invisible() { + return this.note.displayMode === NoteDisplayMode.EdgelessOnly; + } + override updated() { this._displayModePopper = createButtonPopper( this._displayModeButtonGroup, @@ -302,50 +136,49 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) { override render() { const { children, displayMode } = this.note; const currentMode = this._getCurrentModeLabel(displayMode); - const cardHeaderClasses = classMap({ - 'card-header-container': true, - 'enable-sorting': this.enableNotesSorting, - }); return html`
- ${html`
+ ${html`
${ this.invisible - ? html`${HiddenIcon}` - : html`${this.number}` + ? html`${HiddenIcon}` + : html`${this.number}` } - -
- Show in + +
+ Show in { e.stopPropagation(); this._displayModePopper?.toggle(); }} @dblclick=${(e: MouseEvent) => e.stopPropagation()} > -
- ${currentMode} +
+ ${currentMode} ${SmallArrowDownIcon}
{ @@ -355,7 +188,7 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) { >
`} -
+
${children.map(block => { return html` &': { + color: cssVarV2('text/emphasis'), + }, + '&:not(:has(span))': { + display: 'none', + }, + }, +}); + +export const icon = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '22px', + height: '22px', + boxSizing: 'border-box', + padding: '4px', + background: cssVarV2('layer/background/secondary'), + borderRadius: '4px', + color: cssVarV2('icon/primary'), +}); + +export const iconDisabled = style({ + color: cssVarV2('icon/disable'), +}); + +export const text = style({ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + flex: 1, + fontSize: cssVar('fontSm'), + lineHeight: '22px', + height: '22px', +}); + +export const textGeneral = style({ + fontWeight: 400, + paddingLeft: '28px', +}); + +export const subtypeStyles = { + title: style({ + fontWeight: 600, + paddingLeft: '0', + }), + h1: style({ + fontWeight: 600, + paddingLeft: '0', + }), + h2: style({ + fontWeight: 600, + paddingLeft: '4px', + }), + h3: style({ + fontWeight: 600, + paddingLeft: '12px', + }), + h4: style({ + fontWeight: 600, + paddingLeft: '16px', + }), + h5: style({ + fontWeight: 600, + paddingLeft: '20px', + }), + h6: style({ + fontWeight: 600, + paddingLeft: '24px', + }), +}; + +export const textSpan = style({ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', +}); + +export const linkedDocText = style({ + fontSize: 'inherit', + borderBottom: `0.5px solid ${cssVar('dividerColor')}`, + whiteSpace: 'break-spaces', + marginRight: '2px', +}); + +export const linkedDocPreviewUnavailable = style({ + color: cssVarV2('text/disable'), +}); + +export const linkedDocTextUnavailable = style({ + color: cssVarV2('text/disable'), + textDecoration: 'line-through', +}); diff --git a/blocksuite/presets/src/fragments/outline/card/outline-preview.ts b/blocksuite/presets/src/fragments/outline/card/outline-preview.ts index 88cd799bdc..0e89418fc5 100644 --- a/blocksuite/presets/src/fragments/outline/card/outline-preview.ts +++ b/blocksuite/presets/src/fragments/outline/card/outline-preview.ts @@ -1,22 +1,28 @@ import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; -import type { - AttachmentBlockModel, - BookmarkBlockModel, - CodeBlockModel, - DatabaseBlockModel, - ImageBlockModel, - ListBlockModel, - ParagraphBlockModel, - RootBlockModel, +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'; -import { css, html, LitElement, nothing } from 'lit'; +import { consume } from '@lit/context'; +import { html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; -import { SmallLinkedDocIcon } from '../../_common/icons.js'; -import { placeholderMap, previewIconMap } from '../config.js'; +import type { AffineEditorContainer } from '../../../editors/editor-container.js'; +import { editorContext, placeholderMap, previewIconMap } from '../config.js'; import { isHeadingBlock, isRootBlock } from '../utils/query.js'; +import * as styles from './outline-preview.css'; type ValuesOf = T[K]; @@ -24,147 +30,16 @@ function assertType(value: unknown): asserts value is T { noop(value); } -const styles = css` - :host { - display: block; - width: 100%; - font-family: var(--affine-font-family); - } - - :host(:hover) { - cursor: pointer; - background: var(--affine-hover-color); - } - - :host(.active) { - color: var(--affine-text-emphasis-color); - } - - .outline-block-preview { - width: 100%; - box-sizing: border-box; - padding: 6px 8px; - white-space: nowrap; - display: flex; - justify-content: start; - align-items: center; - gap: 8px; - } - - .icon { - display: flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - box-sizing: border-box; - padding: 4px; - background: var(--affine-background-secondary-color); - border-radius: 4px; - color: var(--affine-icon-color); - } - - .icon.disabled { - color: var(--affine-disabled-icon-color); - } - - .text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - - font-size: var(--affine-font-sm); - line-height: 22px; - height: 22px; - } - - .text.general, - .subtype.text, - .subtype.quote { - font-weight: 400; - padding-left: 28px; - } - - .subtype.title, - .subtype.h1, - .subtype.h2, - .subtype.h3, - .subtype.h4, - .subtype.h5, - .subtype.h6 { - font-weight: 600; - } - - .subtype.title { - padding-left: 0; - } - .subtype.h1 { - padding-left: 0; - } - .subtype.h2 { - padding-left: 4px; - } - .subtype.h3 { - padding-left: 12px; - } - .subtype.h4 { - padding-left: 16px; - } - .subtype.h5 { - padding-left: 20px; - } - .subtype.h6 { - padding-left: 24px; - } - - .outline-block-preview:not(:has(span)) { - display: none; - } - - .text span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .linked-doc-preview svg { - width: 1.1em; - height: 1.1em; - vertical-align: middle; - font-size: inherit; - margin-bottom: 0.1em; - } - - .linked-doc-text { - font-size: inherit; - border-bottom: 0.5px solid var(--affine-divider-color); - white-space: break-spaces; - margin-right: 2px; - } - - .linked-doc-preview.unavailable svg { - color: var(--affine-text-disable-color); - } - - .linked-doc-preview.unavailable .linked-doc-text { - color: var(--affine-text-disable-color); - text-decoration: line-through; - } -`; - export const AFFINE_OUTLINE_BLOCK_PREVIEW = 'affine-outline-block-preview'; export class OutlineBlockPreview extends SignalWatcher( - WithDisposable(LitElement) + WithDisposable(ShadowlessElement) ) { - static override styles = styles; - private _TextBlockPreview(block: ParagraphBlockModel | ListBlockModel) { const deltas: DeltaInsert[] = block.text.yText.toDelta(); if (!block.text.length) return nothing; - const iconClass = this.disabledIcon ? 'icon disabled' : 'icon'; + const iconClass = this.disabledIcon ? styles.iconDisabled : styles.icon; const previewText = deltas.map(delta => { if (delta.attributes?.reference) { @@ -174,37 +49,65 @@ export class OutlineBlockPreview extends SignalWatcher( doc => doc.id === refAttribute.pageId ); const unavailable = !refMeta; - const title = unavailable ? 'Deleted doc' : refMeta.title; + const docDisplayMetaService = this.editor.std.get( + DocDisplayMetaProvider + ); + + const icon = unavailable + ? LinkedPageIcon({ width: '1.1em', height: '1.1em' }) + : docDisplayMetaService.icon(refMeta.id).value; + const title = unavailable + ? 'Deleted doc' + : docDisplayMetaService.title(refMeta.id).value; + return html`${SmallLinkedDocIcon} - + ${icon} + ${title.length ? title : 'Untitled'}`; } else { // If not linked doc, render the text. return delta.insert.toString().trim().length > 0 - ? html`${delta.insert.toString()}` + ? html`${delta.insert.toString()}` : nothing; } }); - return html`${previewText} + const headingClass = + block.type in styles.subtypeStyles + ? styles.subtypeStyles[block.type as keyof typeof styles.subtypeStyles] + : ''; + + return html`${previewText} ${this.showPreviewIcon ? html`${previewIconMap[block.type]}` : nothing}`; } override render() { - return html`
+ return html`
${this.renderBlockByFlavour()}
`; } renderBlockByFlavour() { const { block } = this; - const iconClass = this.disabledIcon ? 'icon disabled' : 'icon'; + const iconClass = this.disabledIcon ? styles.iconDisabled : styles.icon; if ( !this.enableNotesSorting && @@ -217,7 +120,10 @@ export class OutlineBlockPreview extends SignalWatcher( case 'affine:page': assertType(block); return block.title.length > 0 - ? html` + ? html` ${block.title$.value} ` : nothing; @@ -230,7 +136,7 @@ export class OutlineBlockPreview extends SignalWatcher( case 'affine:bookmark': assertType(block); return html` - ${block.title || block.url || placeholderMap['bookmark']} ${this.showPreviewIcon @@ -242,7 +148,7 @@ export class OutlineBlockPreview extends SignalWatcher( case 'affine:code': assertType(block); return html` - ${block.language ?? placeholderMap['code']} ${this.showPreviewIcon @@ -252,7 +158,7 @@ export class OutlineBlockPreview extends SignalWatcher( case 'affine:database': assertType(block); return html` - ${block.title.toString().length ? block.title.toString() : placeholderMap['database']}(block); return html` - ${block.caption?.length ? block.caption : placeholderMap['image']}(block); return html` - ${block.name?.length ? block.name : placeholderMap['attachment']}; diff --git a/blocksuite/presets/src/fragments/outline/config.ts b/blocksuite/presets/src/fragments/outline/config.ts index 9e1b3e8319..953ee33b8b 100644 --- a/blocksuite/presets/src/fragments/outline/config.ts +++ b/blocksuite/presets/src/fragments/outline/config.ts @@ -1,6 +1,8 @@ import type { ParagraphBlockModel } from '@blocksuite/blocks'; +import { createContext } from '@lit/context'; import type { TemplateResult } from 'lit'; +import type { AffineEditorContainer } from '../../editors/editor-container.js'; import { BlockPreviewIcon, SmallAttachmentIcon, @@ -84,3 +86,6 @@ export type OutlineSettingsDataType = { showIcons: boolean; enableSorting: boolean; }; + +export const editorContext = + createContext('editorContext'); diff --git a/blocksuite/presets/src/fragments/outline/header/outline-panel-header.css.ts b/blocksuite/presets/src/fragments/outline/header/outline-panel-header.css.ts new file mode 100644 index 0000000000..b6da50f343 --- /dev/null +++ b/blocksuite/presets/src/fragments/outline/header/outline-panel-header.css.ts @@ -0,0 +1,44 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const host = style({}); + +export const container = style({ + display: 'flex', + width: '100%', + height: '40px', + alignItems: 'center', + justifyContent: 'space-between', + boxSizing: 'border-box', + padding: '8px 16px', +}); + +export const noteSettingContainer = style({ + display: 'flex', + alignItems: 'center', + gap: '8px', +}); + +export const label = style({ + width: '119px', + height: '22px', + fontSize: '14px', + fontWeight: 500, + lineHeight: '22px', + color: cssVarV2('text/secondary'), +}); + +export const notePreviewSettingContainer = style({ + display: 'none', + justifyContent: 'center', + alignItems: 'center', + background: cssVarV2('layer/background/overlayPanel'), + boxShadow: cssVar('shadow2'), + borderRadius: '8px', + selectors: { + '&[data-show]': { + display: 'flex', + }, + }, +}); diff --git a/blocksuite/presets/src/fragments/outline/header/outline-panel-header.ts b/blocksuite/presets/src/fragments/outline/header/outline-panel-header.ts index 0066c3f7de..09e67db0e5 100644 --- a/blocksuite/presets/src/fragments/outline/header/outline-panel-header.ts +++ b/blocksuite/presets/src/fragments/outline/header/outline-panel-header.ts @@ -1,84 +1,15 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; import { createButtonPopper } from '@blocksuite/blocks'; import { WithDisposable } from '@blocksuite/global/utils'; -import { css, html, LitElement } from 'lit'; +import { html } from 'lit'; import { property, query, state } from 'lit/decorators.js'; import { SettingsIcon, SortingIcon } from '../../_common/icons.js'; - -const styles = css` - :host { - display: flex; - width: 100%; - height: 40px; - align-items: center; - justify-content: space-between; - box-sizing: border-box; - padding: 8px 16px; - } - - .outline-panel-header-container { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 100%; - box-sizing: border-box; - padding-right: 6px; - } - - .note-setting-container { - display: flex; - align-items: center; - gap: 8px; - } - - .outline-panel-header-label { - width: 119px; - height: 22px; - font-size: 14px; - font-weight: 500; - line-height: 22px; - color: var(--affine-text-secondary-color, #8e8d91); - } - - .note-sorting-button { - justify-self: end; - } - - .note-setting-button svg, - .note-sorting-button svg { - color: var(--affine-icon-secondary); - } - - .note-setting-button:hover svg, - .note-setting-button.active svg, - .note-sorting-button:hover svg { - color: var(--affine-icon-color); - } - - .note-sorting-button.active svg { - color: var(--affine-primary-color); - } - - .note-preview-setting-container { - display: none; - justify-content: center; - align-items: center; - background: var(--affine-background-overlay-panel-color); - box-shadow: var(--affine-shadow-2); - border-radius: 8px; - } - - .note-preview-setting-container[data-show] { - display: flex; - } -`; +import * as styles from './outline-panel-header.css'; export const AFFINE_OUTLINE_PANEL_HEADER = 'affine-outline-panel-header'; -export class OutlinePanelHeader extends WithDisposable(LitElement) { - static override styles = styles; - +export class OutlinePanelHeader extends WithDisposable(ShadowlessElement) { private _notePreviewSettingMenuPopper: ReturnType< typeof createButtonPopper > | null = null; @@ -101,13 +32,11 @@ export class OutlinePanelHeader extends WithDisposable(LitElement) { } override render() { - return html`
-
- Table of Contents + return html`
+
+ Table of Contents
-
+
`; } - @query('.note-preview-setting-container') + @query(`.${styles.notePreviewSettingContainer}`) private accessor _notePreviewSettingMenu!: HTMLDivElement; - @query('.note-setting-button') + @query(`.${styles.noteSettingContainer}`) private accessor _noteSettingButton!: HTMLDivElement; @state() diff --git a/blocksuite/presets/src/fragments/outline/header/outline-setting-menu.css.ts b/blocksuite/presets/src/fragments/outline/header/outline-setting-menu.css.ts new file mode 100644 index 0000000000..dfd07c69d9 --- /dev/null +++ b/blocksuite/presets/src/fragments/outline/header/outline-setting-menu.css.ts @@ -0,0 +1,47 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const host = style({}); + +export const notePreviewSettingMenuContainer = style({ + padding: '8px', + width: '220px', + display: 'flex', + flexDirection: 'column', +}); + +export const notePreviewSettingMenuItem = style({ + display: 'flex', + boxSizing: 'border-box', + width: '100%', + height: '28px', + padding: '4px 12px', + alignItems: 'center', +}); + +export const settingLabel = style({ + fontFamily: 'sans-serif', + fontSize: '12px', + fontWeight: 500, + lineHeight: '20px', + color: cssVarV2('text/secondary'), + padding: '0 4px', +}); + +export const action = style({ + gap: '4px', +}); + +export const actionLabel = style({ + width: '138px', + height: '20px', + padding: '0 4px', + fontSize: '12px', + fontWeight: 500, + lineHeight: '20px', + color: cssVarV2('text/primary'), +}); + +export const toggleButton = style({ + display: 'flex', +}); diff --git a/blocksuite/presets/src/fragments/outline/header/outline-setting-menu.ts b/blocksuite/presets/src/fragments/outline/header/outline-setting-menu.ts index 6bc29c7a76..5e31717a1d 100644 --- a/blocksuite/presets/src/fragments/outline/header/outline-setting-menu.ts +++ b/blocksuite/presets/src/fragments/outline/header/outline-setting-menu.ts @@ -1,76 +1,27 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; import { WithDisposable } from '@blocksuite/global/utils'; -import { css, html, LitElement } from 'lit'; +import { html } from 'lit'; import { property } from 'lit/decorators.js'; -const styles = css` - :host { - display: block; - box-sizing: border-box; - padding: 8px; - width: 220px; - } - - .note-preview-setting-menu-container { - display: flex; - flex-direction: column; - box-sizing: border-box; - width: 100%; - } - - .note-preview-setting-menu-item { - display: flex; - box-sizing: border-box; - width: 100%; - height: 28px; - padding: 4px 12px; - align-items: center; - } - - .note-preview-setting-menu-item .setting-label { - font-family: sans-serif; - font-size: 12px; - font-weight: 500; - line-height: 20px; - color: var(--affine-text-secondary-color); - padding: 0 4px; - } - - .note-preview-setting-menu-item.action { - gap: 4px; - } - - .note-preview-setting-menu-item .action-label { - width: 138px; - height: 20px; - padding: 0 4px; - font-size: 12px; - font-weight: 500; - line-height: 20px; - color: var(--affine-text-primary-color); - } - - .note-preview-setting-menu-item .toggle-button { - display: flex; - } -`; +import * as styles from './outline-setting-menu.css'; export const AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU = 'affine-outline-note-preview-setting-menu'; -export class OutlineNotePreviewSettingMenu extends WithDisposable(LitElement) { - static override styles = styles; - +export class OutlineNotePreviewSettingMenu extends WithDisposable( + ShadowlessElement +) { override render() { return html`
e.stopPropagation()} > -
-
Settings
+
+
Settings
-
-
Show type icon
-
+
+
Show type icon
+
block.id, this.renderItem)} -
- `; + return repeat(items, block => block.id, this.renderItem); } @property({ attribute: false }) diff --git a/blocksuite/presets/src/fragments/outline/outline-panel.css.ts b/blocksuite/presets/src/fragments/outline/outline-panel.css.ts new file mode 100644 index 0000000000..dc528ca48b --- /dev/null +++ b/blocksuite/presets/src/fragments/outline/outline-panel.css.ts @@ -0,0 +1,15 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const outlinePanel = style({ + display: 'flex', + flexDirection: 'column', + backgroundColor: cssVarV2('layer/background/primary'), + boxSizing: 'border-box', + width: '100%', + height: '100%', + fontFamily: cssVar('fontSansFamily'), + paddingTop: '8px', + position: 'relative', +}); diff --git a/blocksuite/presets/src/fragments/outline/outline-panel.ts b/blocksuite/presets/src/fragments/outline/outline-panel.ts index c1324756cf..b6e6c31386 100644 --- a/blocksuite/presets/src/fragments/outline/outline-panel.ts +++ b/blocksuite/presets/src/fragments/outline/outline-panel.ts @@ -1,61 +1,30 @@ +import { + PropTypes, + requiredProperties, + ShadowlessElement, +} from '@blocksuite/block-std'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { provide } from '@lit/context'; import { effect } from '@preact/signals-core'; -import { baseTheme } from '@toeverything/theme'; -import { css, html, LitElement, type PropertyValues, unsafeCSS } from 'lit'; +import { html, type PropertyValues } from 'lit'; import { property, state } from 'lit/decorators.js'; import type { AffineEditorContainer } from '../../editors/editor-container.js'; -import { type OutlineSettingsDataType, outlineSettingsKey } from './config.js'; - -const styles = css` - :host { - display: block; - width: 100%; - height: 100%; - } - - .outline-panel-container { - background-color: var(--affine-background-primary-color); - box-sizing: border-box; - - display: flex; - flex-direction: column; - align-items: stretch; - - height: 100%; - font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; - padding-top: 8px; - position: relative; - } - - .outline-panel-body { - flex-grow: 1; - width: 100%; - - overflow-y: scroll; - } - .outline-panel-body::-webkit-scrollbar { - width: 4px; - } - .outline-panel-body::-webkit-scrollbar-thumb { - border-radius: 2px; - } - .outline-panel-body:hover::-webkit-scrollbar-thumb { - background-color: var(--affine-black-30); - } - .outline-panel-body::-webkit-scrollbar-track { - background-color: transparent; - } - .outline-panel-body::-webkit-scrollbar-corner { - display: none; - } -`; +import { + editorContext, + type OutlineSettingsDataType, + outlineSettingsKey, +} from './config.js'; +import * as styles from './outline-panel.css'; export const AFFINE_OUTLINE_PANEL = 'affine-outline-panel'; -export class OutlinePanel extends SignalWatcher(WithDisposable(LitElement)) { - static override styles = styles; - +@requiredProperties({ + editor: PropTypes.object, +}) +export class OutlinePanel extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { private readonly _setNoticeVisibility = (visibility: boolean) => { this._noticeVisible = visibility; }; @@ -79,10 +48,6 @@ export class OutlinePanel extends SignalWatcher(WithDisposable(LitElement)) { return this.editor.doc; } - get edgeless() { - return this.editor.querySelector('affine-edgeless-root'); - } - get host() { return this.editor.host; } @@ -113,6 +78,8 @@ export class OutlinePanel extends SignalWatcher(WithDisposable(LitElement)) { override connectedCallback() { super.connectedCallback(); + this.classList.add(styles.outlinePanel); + this.disposables.add( effect(() => { if (this.editor.mode === 'edgeless') { @@ -138,33 +105,27 @@ export class OutlinePanel extends SignalWatcher(WithDisposable(LitElement)) { if (!this.host) return; return html` -
- - - - -
+ + + + `; } @@ -177,6 +138,7 @@ export class OutlinePanel extends SignalWatcher(WithDisposable(LitElement)) { @state() private accessor _showPreviewIcon = false; + @provide({ context: editorContext }) @property({ attribute: false }) accessor editor!: AffineEditorContainer; diff --git a/blocksuite/presets/src/fragments/outline/outline-viewer.ts b/blocksuite/presets/src/fragments/outline/outline-viewer.ts index 02da29f7ec..2384dbf5b3 100644 --- a/blocksuite/presets/src/fragments/outline/outline-viewer.ts +++ b/blocksuite/presets/src/fragments/outline/outline-viewer.ts @@ -1,14 +1,20 @@ -import { PropTypes, requiredProperties } from '@blocksuite/block-std'; +import { + PropTypes, + requiredProperties, + ShadowlessElement, +} from '@blocksuite/block-std'; import { NoteDisplayMode, scrollbarStyle } from '@blocksuite/blocks'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { provide } from '@lit/context'; import { signal } from '@preact/signals-core'; -import { css, html, LitElement, nothing } from 'lit'; +import { css, html, nothing } from 'lit'; import { property, query, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { repeat } from 'lit/directives/repeat.js'; import type { AffineEditorContainer } from '../../editors/editor-container.js'; import { TocIcon } from '../_common/icons.js'; +import { editorContext } from './config.js'; import { getHeadingBlocksFromDoc } from './utils/query.js'; import { observeActiveHeadingDuringScroll, @@ -20,9 +26,11 @@ export const AFFINE_OUTLINE_VIEWER = 'affine-outline-viewer'; @requiredProperties({ editor: PropTypes.object, }) -export class OutlineViewer extends SignalWatcher(WithDisposable(LitElement)) { +export class OutlineViewer extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { static override styles = css` - :host { + affine-outline-viewer { display: flex; } .outline-viewer-root { @@ -117,9 +125,7 @@ export class OutlineViewer extends SignalWatcher(WithDisposable(LitElement)) { } .outline-viewer-item { - display: flex; - align-items: center; - align-self: stretch; + width: 100%; } .outline-viewer-root:hover { @@ -281,6 +287,7 @@ export class OutlineViewer extends SignalWatcher(WithDisposable(LitElement)) { @state() private accessor _showViewer: boolean = false; + @provide({ context: editorContext }) @property({ attribute: false }) accessor editor!: AffineEditorContainer; diff --git a/blocksuite/presets/src/fragments/outline/utils/query.ts b/blocksuite/presets/src/fragments/outline/utils/query.ts index 6c3fcbb1f0..a65c246c84 100644 --- a/blocksuite/presets/src/fragments/outline/utils/query.ts +++ b/blocksuite/presets/src/fragments/outline/utils/query.ts @@ -40,7 +40,7 @@ export function getNotesFromDoc( number: index + 1, }; - if (modes.includes(blockModel.displayMode)) { + if (modes.includes(blockModel.displayMode$.value)) { notes.push(OutlineNoteItem); } }); diff --git a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts index a3adeadbb1..fa086e8797 100644 --- a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts +++ b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts @@ -232,7 +232,7 @@ test.describe('edgeless note element toolbar', () => { const toc = page.locator('affine-outline-panel'); await toc.waitFor({ state: 'visible' }); const highlightNoteCards = toc.locator( - 'affine-outline-note-card > .selected' + 'affine-outline-note-card > [data-status="selected"]' ); expect(highlightNoteCards).toHaveCount(1); }); diff --git a/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts index 9f86f3de64..f61d3d27cf 100644 --- a/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts +++ b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts @@ -40,7 +40,7 @@ async function openTocPanel(page: Page) { } function getTocHeading(panel: Locator, level: number) { - return panel.locator(`.h${level} span`); + return panel.getByTestId(`outline-block-preview-h${level}`).locator('span'); } async function dragNoteCard(page: Page, fromCard: Locator, toCard: Locator) { @@ -67,7 +67,7 @@ test('should display title and headings when there are non-empty headings in edi const toc = await openTocPanel(page); - await expect(toc.locator('.title')).toBeVisible(); + await expect(toc.getByTestId('outline-block-preview-title')).toBeVisible(); for (let i = 1; i <= 6; i++) { await expect(getTocHeading(toc, i)).toBeVisible(); await expect(getTocHeading(toc, i)).toContainText(`Heading ${i}`); @@ -76,7 +76,7 @@ test('should display title and headings when there are non-empty headings in edi test('should display placeholder when no headings', async ({ page }) => { const toc = await openTocPanel(page); - const noHeadingPlaceholder = toc.locator('.note-placeholder'); + const noHeadingPlaceholder = toc.getByTestId('empty-panel-placeholder'); await createTitle(page); await pressEnter(page); @@ -98,7 +98,7 @@ test('should not display headings when there are only empty headings', async ({ const toc = await openTocPanel(page); - await expect(toc.locator('.title')).toBeHidden(); + await expect(toc.getByTestId('outline-block-preview-title')).toBeHidden(); for (let i = 1; i <= 6; i++) { await expect(getTocHeading(toc, i)).toBeHidden(); } @@ -115,10 +115,12 @@ test('should update panel when modify or clear title or headings', async ({ await title.scrollIntoViewIfNeeded(); await title.click(); await type(page, 'xxx'); - await expect(toc.locator('.title')).toContainText(['Titlexxx']); + await expect(toc.getByTestId('outline-block-preview-title')).toContainText([ + 'Titlexxx', + ]); await selectAllByKeyboard(page); await pressBackspace(page); - await expect(toc.locator('.title')).toBeHidden(); + await expect(toc.getByTestId('outline-block-preview-title')).toBeHidden(); for (let i = 1; i <= 6; i++) { await headings[i - 1].click(); @@ -210,7 +212,7 @@ test('should scroll to title when click title in outline panel', async ({ const toc = await openTocPanel(page); - const titleInPanel = toc.locator('.title'); + const titleInPanel = toc.getByTestId('outline-block-preview-title'); await expect(title).not.toBeInViewport(); await titleInPanel.click(); @@ -225,7 +227,7 @@ test('visibility sorting should be enabled in edgeless mode and disabled in page const toc = await openTocPanel(page); - const sortingButton = toc.locator('.note-sorting-button'); + const sortingButton = toc.getByTestId('toggle-notes-sorting-button'); await expect(sortingButton).not.toHaveClass(/active/); expect(toc.locator('[data-sortable="false"]')).toHaveCount(1); @@ -250,10 +252,10 @@ test('should reorder notes when drag and drop note in outline panel', async ({ const toc = await openTocPanel(page); const docVisibleCards = toc.locator( - '.card-container[data-invisible="false"]' + 'affine-outline-note-card [data-invisible="false"]' ); const docInvisibleCards = toc.locator( - '.card-container[data-invisible="true"]' + 'affine-outline-note-card [data-invisible="true"]' ); await expect(docVisibleCards).toHaveCount(1); @@ -262,7 +264,7 @@ test('should reorder notes when drag and drop note in outline panel', async ({ while ((await docInvisibleCards.count()) > 0) { const card = docInvisibleCards.first(); await card.hover(); - await card.locator('.display-mode-button').click(); + await card.getByTestId('display-mode-button').click(); await card.locator('note-display-mode-panel').locator('.item.both').click(); } @@ -310,10 +312,10 @@ test.describe('advanced visibility control', () => { const toc = await openTocPanel(page); const docVisibleCard = toc.locator( - '.card-container[data-invisible="false"]' + 'affine-outline-note-card [data-invisible="false"]' ); const docInvisibleCard = toc.locator( - '.card-container[data-invisible="true"]' + 'affine-outline-note-card [data-invisible="true"]' ); await expect(docVisibleCard).toHaveCount(1); @@ -341,17 +343,17 @@ test.describe('advanced visibility control', () => { const toc = await openTocPanel(page); const docVisibleCard = toc.locator( - '.card-container[data-invisible="false"]' + 'affine-outline-note-card [data-invisible="false"]' ); const docInvisibleCard = toc.locator( - '.card-container[data-invisible="true"]' + 'affine-outline-note-card [data-invisible="true"]' ); await expect(docVisibleCard).toHaveCount(1); await expect(docInvisibleCard).toHaveCount(1); await docInvisibleCard.hover(); - await docInvisibleCard.locator('.display-mode-button').click(); + await docInvisibleCard.getByTestId('display-mode-button').click(); await docInvisibleCard .locator('note-display-mode-panel .item.both') .click(); diff --git a/tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts b/tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts index e37032ca9b..4aff3cd115 100644 --- a/tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts +++ b/tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts @@ -129,7 +129,7 @@ test('should highlight indicator when click item in outline panel', async ({ await indicators.first().hover({ force: true }); const headingsInPanel = Array.from({ length: 6 }, (_, i) => - viewer.locator(`.h${i + 1} > span`) + viewer.getByTestId(`outline-block-preview-h${i + 1}`) ); await headingsInPanel[2].click(); await expect(headings[2]).toBeVisible(); @@ -172,7 +172,9 @@ test('should hide edgeless-only note headings', async ({ page }) => { const viewer = page.locator('affine-outline-viewer'); await expect(viewer).toBeVisible(); - const h1InPanel = viewer.locator('.h1 > span'); + const h1InPanel = viewer + .getByTestId('outline-block-preview-h1') + .locator('span'); await h1InPanel.waitFor({ state: 'visible' }); expect(h1InPanel).toContainText(['Heading 1']); }); diff --git a/yarn.lock b/yarn.lock index d4254f24b3..2773ca7e2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3947,7 +3947,7 @@ __metadata: languageName: unknown linkType: soft -"@blocksuite/icons@npm:2.2.2, @blocksuite/icons@npm:^2.2.1": +"@blocksuite/icons@npm:2.2.2, @blocksuite/icons@npm:^2.2.1, @blocksuite/icons@npm:^2.2.2": version: 2.2.2 resolution: "@blocksuite/icons@npm:2.2.2" peerDependencies: @@ -4045,12 +4045,15 @@ __metadata: "@blocksuite/block-std": "workspace:*" "@blocksuite/blocks": "workspace:*" "@blocksuite/global": "workspace:*" + "@blocksuite/icons": "npm:^2.2.2" "@blocksuite/inline": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.10" + "@lit/context": "npm:^1.1.3" "@lottiefiles/dotlottie-wc": "npm:^0.4.0" "@preact/signals-core": "npm:^1.8.0" "@toeverything/theme": "npm:^1.1.7" + "@vanilla-extract/css": "npm:^1.17.0" "@vanilla-extract/vite-plugin": "npm:^4.0.19" lit: "npm:^3.2.0" vitest: "npm:^3.0.0" @@ -7776,7 +7779,7 @@ __metadata: languageName: node linkType: hard -"@lit/context@npm:^1.1.2": +"@lit/context@npm:^1.1.2, @lit/context@npm:^1.1.3": version: 1.1.3 resolution: "@lit/context@npm:1.1.3" dependencies: