diff --git a/blocksuite/affine/blocks/code/src/code-block.ts b/blocksuite/affine/blocks/code/src/code-block.ts index ba386b88c..7ea57a4fb 100644 --- a/blocksuite/affine/blocks/code/src/code-block.ts +++ b/blocksuite/affine/blocks/code/src/code-block.ts @@ -51,6 +51,10 @@ export class CodeBlockComponent extends CaptionedBlockComponent return modelPreview; }); + collapsed$: Signal = computed( + () => !!this.model.props.collapsed$.value + ); + highlightTokens$: Signal = signal([]); languageName$: Signal = computed(() => { @@ -417,6 +421,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent CodeBlockPreviewIdentifier(this.model.props.language ?? '') ); const shouldRenderPreview = preview && previewContext; + const collapsed = this.collapsed$.value; return html`
mobile: IS_MOBILE, wrap: this.model.props.wrap, 'disable-line-numbers': !showLineNumbers, + collapsed, })} > }} > + ${collapsed + ? html`` + : nothing}
this.store.updateBlock(this.model, { wrap }); } + setCollapsed(collapsed: boolean) { + this.store.updateBlock(this.model, { collapsed }); + } + @query('rich-text') private accessor _richTextElement: RichText | null = null; diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts b/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts index bfd92ce08..113c11a73 100644 --- a/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts +++ b/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts @@ -9,6 +9,7 @@ import { WithDisposable } from '@blocksuite/global/lit'; import { noop } from '@blocksuite/global/utils'; import { MoreVerticalIcon } from '@blocksuite/icons/lit'; import { flip, offset } from '@floating-ui/dom'; +import { effect } from '@preact/signals-core'; import { css, html, LitElement } from 'lit'; import { property, query, state } from 'lit/decorators.js'; @@ -108,6 +109,17 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) { this.closeCurrentMenu(); } + override connectedCallback() { + super.connectedCallback(); + // Mirror the collapsed$ signal from the block component into local @state + // so this LitElement re-renders when it changes. + this.disposables.add( + effect(() => { + this._collapsed = this.context.blockComponent.collapsed$.value; + }) + ); + } + override render() { return html` @@ -136,6 +148,9 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) { @state() private accessor _moreMenuOpen = false; + @state() + private accessor _collapsed = false; + @property({ attribute: false }) accessor context!: CodeBlockToolbarContext; diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/config.ts b/blocksuite/affine/blocks/code/src/code-toolbar/config.ts index e6bcdd1a2..776926e3a 100644 --- a/blocksuite/affine/blocks/code/src/code-toolbar/config.ts +++ b/blocksuite/affine/blocks/code/src/code-toolbar/config.ts @@ -1,9 +1,11 @@ import { CancelWrapIcon, CaptionIcon, + CollapseCodeIcon, CopyIcon, DeleteIcon, DuplicateIcon, + ExpandCodeIcon, WrapIcon, } from '@blocksuite/affine-components/icons'; import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar'; @@ -85,6 +87,38 @@ export const PRIMARY_GROUPS: MenuItemGroup[] = [ }; }, }, + { + type: 'collapse', + when: ({ doc }) => !doc.readonly, + generate: ({ blockComponent }) => { + return { + action: () => { + blockComponent.setCollapsed(!blockComponent.collapsed$.value); + }, + render: item => { + const collapsed = blockComponent.collapsed$.value; + const icon = collapsed ? ExpandCodeIcon : CollapseCodeIcon; + const label = collapsed ? 'Expand code' : 'Collapse code'; + return html` + { + e.stopPropagation(); + item.action(); + }} + > + ${icon} + + `; + }, + }; + }, + }, { type: 'caption', label: 'Caption', diff --git a/blocksuite/affine/blocks/code/src/styles.ts b/blocksuite/affine/blocks/code/src/styles.ts index c8cdfeb86..652c0b033 100644 --- a/blocksuite/affine/blocks/code/src/styles.ts +++ b/blocksuite/affine/blocks/code/src/styles.ts @@ -80,4 +80,35 @@ export const codeBlockStyles = css` affine-code .affine-code-block-preview { padding: 12px; } + + /* ── Collapsed state ──────────────────────────────────────────────── */ + + /* Clamp the rich-text to the first 8 lines */ + .affine-code-block-container.collapsed rich-text { + display: block; + max-height: calc(8 * var(--affine-line-height)); + overflow: hidden; + } + + /* Reduce bottom padding so the fade sits flush with the border */ + .affine-code-block-container.collapsed { + padding-bottom: 0; + } + + /* Gradient overlay that fades to the block background */ + .affine-code-block-container .code-collapsed-fade { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 80px; + background: linear-gradient( + to bottom, + transparent, + var(--affine-background-code-block) + ); + border-radius: 0 0 10px 10px; + pointer-events: none; + z-index: 1; + } `; diff --git a/blocksuite/affine/components/src/icons/text.ts b/blocksuite/affine/components/src/icons/text.ts index e53d1dc15..d9ae15afe 100644 --- a/blocksuite/affine/components/src/icons/text.ts +++ b/blocksuite/affine/components/src/icons/text.ts @@ -265,6 +265,16 @@ export const CancelWrapIcon = icons.CancelWrapIcon({ height: '20', }); +export const CollapseCodeIcon = icons.CollapseIcon({ + width: '20', + height: '20', +}); + +export const ExpandCodeIcon = icons.ToggleRightIcon({ + width: '20', + height: '20', +}); + // Attachment export const ViewIcon = icons.ViewIcon({ diff --git a/blocksuite/affine/model/src/blocks/code/code-model.ts b/blocksuite/affine/model/src/blocks/code/code-model.ts index 16e354496..fe7264d04 100644 --- a/blocksuite/affine/model/src/blocks/code/code-model.ts +++ b/blocksuite/affine/model/src/blocks/code/code-model.ts @@ -14,6 +14,7 @@ type CodeBlockProps = { caption: string; preview?: boolean; lineNumber?: boolean; + collapsed?: boolean; comments?: Record; } & BlockMeta; @@ -27,6 +28,7 @@ export const CodeBlockSchema = defineBlockSchema({ caption: '', preview: undefined, lineNumber: undefined, + collapsed: undefined, comments: undefined, 'meta:createdAt': undefined, 'meta:createdBy': undefined,