feat(editor): support preview mode in code block (#11805)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a preview mode for code blocks, allowing users to toggle between code and rendered previews (e.g., HTML output) directly within the editor.
  - Added a preview toggle button to the code block toolbar for supported languages.
  - Enabled dynamic rendering of code block previews using a shared WebContainer, allowing live HTML previews in an embedded iframe.
  - Added HTML preview support with loading and error states for enhanced user feedback.
  - Integrated the preview feature as a view extension provider for seamless framework support.

- **Bug Fixes**
  - Improved toolbar layout and button alignment for a cleaner user interface.

- **Tests**
  - Added end-to-end tests to verify the new code block preview functionality and language switching behavior.

- **Chores**
  - Updated development server configuration to include enhanced security headers.
  - Added a new runtime dependency for WebContainer support.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Flrande
2025-05-06 09:14:12 +00:00
parent 1e89aa48cb
commit f79dfe837f
18 changed files with 520 additions and 7 deletions

View File

@@ -30,7 +30,6 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
padding: 4px;
margin: 0;
display: flex;
justify-content: flex-end;
}
.code-toolbar-button {
@@ -39,6 +38,10 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
box-shadow: var(--affine-shadow-1);
border-radius: 4px;
}
.copy-code {
margin-left: auto;
}
`;
private _currentOpenMenu: AbortController | null = null;

View File

@@ -18,10 +18,6 @@ export class LanguageListButton extends WithDisposable(
SignalWatcher(LitElement)
) {
static override styles = css`
:host {
margin-right: auto;
}
.lang-button {
background-color: var(--affine-background-primary-color);
box-shadow: var(--affine-shadow-1);

View File

@@ -0,0 +1,97 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { css, html, LitElement, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import type { CodeBlockComponent } from '../../code-block';
import { CodeBlockPreviewIdentifier } from '../../code-preview-extension';
export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
static override styles = css`
.preview-toggle-container {
display: flex;
padding: 2px;
align-items: flex-start;
gap: 4px;
border-radius: 4px;
background: ${unsafeCSSVarV2('segment/background')};
}
.toggle-button {
display: flex;
padding: 0px 4px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 4px;
color: ${unsafeCSSVarV2('text/primary')};
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
.toggle-button:hover {
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.toggle-button.active {
background: ${unsafeCSSVarV2('segment/button')};
box-shadow:
var(--Shadow-buttonShadow-1-x, 0px) var(--Shadow-buttonShadow-1-y, 0px)
var(--Shadow-buttonShadow-1-blur, 1px) 0px
var(--Shadow-buttonShadow-1-color, rgba(0, 0, 0, 0.12)),
var(--Shadow-buttonShadow-2-x, 0px) var(--Shadow-buttonShadow-2-y, 1px)
var(--Shadow-buttonShadow-2-blur, 5px) 0px
var(--Shadow-buttonShadow-2-color, rgba(0, 0, 0, 0.12));
}
`;
private readonly _toggle = (value: boolean) => {
if (this.blockComponent.doc.readonly) return;
this.blockComponent.doc.updateBlock(this.blockComponent.model, {
preview: value,
});
};
get preview() {
return !!this.blockComponent.model.props.preview$.value;
}
override render() {
const lang = this.blockComponent.model.props.language$.value ?? '';
const previewContext = this.blockComponent.std.getOptional(
CodeBlockPreviewIdentifier(lang)
);
if (!previewContext) return nothing;
return html`
<div class="preview-toggle-container">
<div
class=${classMap({
'toggle-button': true,
active: !this.preview,
})}
@click=${() => this._toggle(false)}
>
Code
</div>
<div
class=${classMap({
'toggle-button': true,
active: this.preview,
})}
@click=${() => this._toggle(true)}
>
Preview
</div>
</div>
`;
}
@property({ attribute: false })
accessor blockComponent!: CodeBlockComponent;
}

View File

@@ -42,6 +42,18 @@ export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
};
},
},
{
type: 'preview',
generate: ({ blockComponent }) => {
return {
action: noop,
render: () => html`
<preview-button .blockComponent=${blockComponent}>
</preview-button>
`,
};
},
},
{
type: 'copy-code',
label: 'Copy code',