Files
AFFiNE-Mirror/blocksuite/affine/fragments/adapter-panel/src/header/adapter-panel-header.ts
T
donteatfriedrice a828c74f87 feat(editor): add experimental feature adapter panel to AFFiNE canary (#12489)
Closes: [BS-2539](https://linear.app/affine-design/issue/BS-2539/为-affine-添加-ef,并且支持在-affine-预览对应的功能)

> [!warning]
> This feature is only available in the canary build and is intended for debugging purposes.

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

- **New Features**
  - Introduced an "Adapter Panel" feature with a new sidebar tab for previewing document content in multiple formats (Markdown, PlainText, HTML, Snapshot), controllable via a feature flag.
  - Added a fully integrated adapter panel component with reactive UI elements for selecting adapters, toggling HTML preview modes, and updating content.
  - Provided a customizable adapter panel for both main app and playground environments, supporting content transformation pipelines and export previews.
  - Enabled seamless toggling and live updating of adapter panel content through intuitive menus and controls.

- **Localization**
  - Added English translations and descriptive settings for the Adapter Panel feature.

- **Chores**
  - Added new package and workspace dependencies along with TypeScript project references to support the Adapter Panel modules and components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 14:08:12 +00:00

120 lines
3.5 KiB
TypeScript

import { createLitPortal } from '@blocksuite/affine-components/portal';
import { SignalWatcher } from '@blocksuite/global/lit';
import { ArrowDownSmallIcon, FlipDirectionIcon } from '@blocksuite/icons/lit';
import { flip, offset } from '@floating-ui/dom';
import { consume } from '@lit/context';
import { css, html, LitElement } from 'lit';
import { property, query } from 'lit/decorators.js';
import { type AdapterPanelContext, adapterPanelContext } from '../config';
export const AFFINE_ADAPTER_PANEL_HEADER = 'affine-adapter-panel-header';
export class AdapterPanelHeader extends SignalWatcher(LitElement) {
static override styles = css`
.adapter-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--affine-background-primary-color);
}
.adapter-selector {
display: flex;
align-items: center;
justify-content: space-between;
width: 100px;
cursor: pointer;
border-radius: 4px;
border: 1px solid var(--affine-border-color);
padding: 4px 8px;
}
.adapter-selector:hover {
background: var(--affine-hover-color);
}
.adapter-selector-label {
display: flex;
align-items: center;
color: var(--affine-text-primary-color);
font-size: var(--affine-font-xs);
}
.update-button {
height: 20px;
width: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
color: var(--affine-icon-color);
}
.update-button:hover {
background-color: var(--affine-hover-color);
}
`;
get activeAdapter() {
return this._context.activeAdapter$.value;
}
private _adapterMenuAbortController: AbortController | null = null;
private readonly _toggleAdapterMenu = () => {
if (this._adapterMenuAbortController) {
this._adapterMenuAbortController.abort();
}
this._adapterMenuAbortController = new AbortController();
createLitPortal({
template: html`<affine-adapter-menu
.abortController=${this._adapterMenuAbortController}
></affine-adapter-menu>`,
portalStyles: {
zIndex: 'var(--affine-z-index-popover)',
},
container: this._adapterPanelHeader,
computePosition: {
referenceElement: this._adapterSelector,
placement: 'bottom-start',
middleware: [flip(), offset(4)],
autoUpdate: { animationFrame: true },
},
abortController: this._adapterMenuAbortController,
closeOnClickAway: true,
});
};
override render() {
return html`
<div class="adapter-panel-header">
<div class="adapter-selector" @click="${this._toggleAdapterMenu}">
<span class="adapter-selector-label">
${this.activeAdapter.label}
</span>
${ArrowDownSmallIcon({ width: '16px', height: '16px' })}
</div>
<div class="update-button" @click="${this.updateActiveContent}">
${FlipDirectionIcon({ width: '16px', height: '16px' })}
</div>
</div>
`;
}
@query('.adapter-panel-header')
private accessor _adapterPanelHeader!: HTMLDivElement;
@query('.adapter-selector')
private accessor _adapterSelector!: HTMLDivElement;
@property({ attribute: false })
accessor updateActiveContent: () => void = () => {};
@consume({ context: adapterPanelContext })
private accessor _context!: AdapterPanelContext;
}
declare global {
interface HTMLElementTagNameMap {
[AFFINE_ADAPTER_PANEL_HEADER]: AdapterPanelHeader;
}
}