feat(core): ai panel adaptation for edgeless theme (#11509)

### TL;DR

AI panel adaptation for Edgeless theme

> CLOSE BS-3017

![截屏2025-04-07 17.30.48.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/a2e83338-d795-4b16-b2a8-bdd4f922a4c3.png)
This commit is contained in:
yoyoyohamapi
2025-04-08 02:29:57 +00:00
parent 646182ea2a
commit 49c6ad7c04
7 changed files with 270 additions and 24 deletions

View File

@@ -1,5 +1,6 @@
import { createLitPortal } from '@blocksuite/affine/components/portal';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { ColorScheme } from '@blocksuite/affine/model';
import {
EditorHost,
PropTypes,
@@ -123,6 +124,7 @@ export class AIItemList extends WithDisposable(LitElement) {
item => item.name,
item =>
html`<ai-item
.theme=${this.theme}
.onClick=${this.onClick}
.item=${item}
.host=${this.host}
@@ -145,6 +147,9 @@ export class AIItemList extends WithDisposable(LitElement) {
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'ai-item-list';
@property({ attribute: false })
accessor theme: ColorScheme = ColorScheme.Light;
}
declare global {

View File

@@ -1,5 +1,6 @@
import { ArrowRightIcon, EnterIcon } from '@blocksuite/affine/components/icons';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { ColorScheme } from '@blocksuite/affine/model';
import {
EditorHost,
PropTypes,
@@ -27,6 +28,7 @@ export class AIItem extends WithDisposable(LitElement) {
return html`<div
data-testid=${testId}
data-app-theme=${this.theme}
class="menu-item ${className}"
@pointerdown=${(e: MouseEvent) => e.stopPropagation()}
@click=${() => {
@@ -59,6 +61,9 @@ export class AIItem extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor onClick: (() => void) | undefined;
@property({ attribute: false })
accessor theme: ColorScheme = ColorScheme.Light;
}
declare global {

View File

@@ -1,4 +1,10 @@
import { css } from 'lit';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { darkCssVariables, lightCssVariables } from '@toeverything/theme';
import {
darkCssVariablesV2,
lightCssVariablesV2,
} from '@toeverything/theme/v2';
import { css, unsafeCSS } from 'lit';
export const menuItemStyles = css`
.menu-item {
@@ -13,36 +19,41 @@ export const menuItemStyles = css`
border-radius: 4px;
box-sizing: border-box;
}
.menu-item:hover {
background: var(--affine-hover-color);
background: ${unsafeCSSVar('--affine-hover-color')};
cursor: pointer;
}
.item-icon {
display: flex;
color: var(--item-icon-color, var(--affine-brand-color));
color: ${unsafeCSSVar('--affine-brand-color')};
svg {
width: 20px;
height: 20px;
}
}
.menu-item:hover .item-icon {
color: var(--item-icon-hover-color, var(--affine-brand-color));
color: ${unsafeCSSVar('--affine-brand-color')};
}
.menu-item.discard:hover {
background: var(--affine-background-error-color);
background: ${unsafeCSSVar('--affine-background-error-color')};
.item-name,
.item-icon,
.enter-icon {
color: var(--affine-error-color);
color: ${unsafeCSSVar('--affine-error-color')};
}
}
.item-name {
display: flex;
padding: 0px 4px;
align-items: baseline;
flex: 1 0 0;
color: var(--affine-text-primary-color);
color: ${unsafeCSSVarV2('text/primary')};
text-align: start;
white-space: nowrap;
font-feature-settings:
@@ -55,7 +66,7 @@ export const menuItemStyles = css`
}
.item-beta {
color: var(--affine-text-secondary-color);
color: ${unsafeCSSVarV2('text/secondary')};
font-size: var(--affine-font-xs);
font-weight: 500;
margin-left: 0.5em;
@@ -63,14 +74,66 @@ export const menuItemStyles = css`
.enter-icon,
.arrow-right-icon {
color: var(--affine-icon-color);
color: ${unsafeCSSVarV2('icon/primary')};
display: flex;
}
.enter-icon {
opacity: 0;
}
.arrow-right-icon,
.menu-item:hover .enter-icon {
opacity: 1;
}
.menu-item[data-app-theme='light'] {
.item-name {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-text-primary'])};
}
.item-beta {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-text-secondary'])};
}
.enter-icon,
.arrow-right-icon {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-icon-primary'])};
}
}
.menu-item[data-app-theme='light']:hover {
background: ${unsafeCSS(lightCssVariables['--affine-hover-color'])};
}
.menu-item.discard[data-app-theme='light']:hover {
background: ${unsafeCSS(
lightCssVariables['--affine-background-error-color']
)};
}
.menu-item[data-app-theme='dark'] {
.item-name {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-text-primary'])};
}
.item-beta {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-text-secondary'])};
}
.enter-icon,
.arrow-right-icon {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-icon-primary'])};
}
}
.menu-item[data-app-theme='dark']:hover {
background: ${unsafeCSS(darkCssVariables['--affine-hover-color'])};
}
.menu-item.discard[data-app-theme='dark']:hover {
background: ${unsafeCSS(
darkCssVariables['--affine-background-error-color']
)};
}
`;

View File

@@ -1,5 +1,8 @@
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { DocModeProvider } from '@blocksuite/affine/shared/services';
import {
DocModeProvider,
ThemeProvider,
} from '@blocksuite/affine/shared/services';
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
import { type EditorHost } from '@blocksuite/affine/std';
import { cssVar } from '@toeverything/theme';
@@ -70,12 +73,23 @@ export class AskAIPanel extends WithDisposable(LitElement) {
return filteredConfig;
}
override connectedCallback() {
super.connectedCallback();
this.disposables.add(
this.host.std.get(ThemeProvider).app$.subscribe(() => {
this.requestUpdate();
})
);
}
override render() {
const style = styleMap({
minWidth: `${this.minWidth}px`,
});
const appTheme = this.host.std.get(ThemeProvider).app$.value;
return html`<div class="ask-ai-panel" style=${style}>
<ai-item-list
.theme=${appTheme}
.host=${this.host}
.groups=${this._actionGroups}
.onClick=${this.onItemClick}

View File

@@ -2,6 +2,7 @@ import {
AFFINE_VIEWPORT_OVERLAY_WIDGET,
type AffineViewportOverlayWidget,
} from '@blocksuite/affine/blocks/root';
import { ColorScheme } from '@blocksuite/affine/model';
import {
DocModeProvider,
NotificationProvider,
@@ -9,6 +10,7 @@ import {
ToolbarFlag,
ToolbarRegistryIdentifier,
} from '@blocksuite/affine/shared/services';
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
import {
getPageRootByElement,
stopPropagation,
@@ -27,7 +29,8 @@ import {
type Rect,
shift,
} from '@floating-ui/dom';
import { css, html, nothing, type PropertyValues } from 'lit';
import { darkCssVariables, lightCssVariables } from '@toeverything/theme';
import { css, html, nothing, type PropertyValues, unsafeCSS } from 'lit';
import { property, query } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
@@ -43,9 +46,10 @@ export class AffineAIPanelWidget extends WidgetComponent {
display: flex;
outline: none;
border-radius: var(--8, 8px);
border: 1px solid var(--affine-border-color);
background: var(--affine-background-overlay-panel-color);
box-shadow: var(--affine-overlay-shadow);
border: 1px solid;
border-color: ${unsafeCSSVar('--affine-border-color')};
background: ${unsafeCSSVar('backgroundOverlayPanelColor')};
box-shadow: ${unsafeCSSVar('overlayShadow')};
position: absolute;
width: max-content;
@@ -58,6 +62,21 @@ export class AffineAIPanelWidget extends WidgetComponent {
--affine-font-family: var(--affine-font-sans-family);
}
:host([data-app-theme='light']) {
background: ${unsafeCSS(
lightCssVariables['--affine-background-overlay-panel-color']
)};
border-color: ${unsafeCSS(lightCssVariables['--affine-border-color'])};
box-shadow: ${unsafeCSS(lightCssVariables['--affine-overlay-shadow'])};
}
:host([data-app-theme='dark']) {
background: ${unsafeCSS(
darkCssVariables['--affine-background-overlay-panel-color']
)};
border-color: ${unsafeCSS(darkCssVariables['--affine-border-color'])};
box-shadow: ${unsafeCSS(darkCssVariables['--affine-overlay-shadow'])};
}
.ai-panel-container {
display: flex;
flex-direction: column;
@@ -414,6 +433,13 @@ export class AffineAIPanelWidget extends WidgetComponent {
override connectedCallback() {
super.connectedCallback();
this.appTheme = this.std.get(ThemeProvider).app$.value;
this.disposables.add(
this.std.get(ThemeProvider).app$.subscribe(theme => {
this.appTheme = theme;
this.requestUpdate();
})
);
this.tabIndex = -1;
// No need to synchronize the contents of the input into the editor.
@@ -460,7 +486,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
if (!this.config) return nothing;
const config = this.config;
const theme = this.std.get(ThemeProvider).theme;
const theme = this.std.get(ThemeProvider).app$.value;
const mainTemplate = choose(this.state, [
[
'input',
@@ -470,6 +496,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
.onFinish=${this._inputFinish}
.onInput=${this.onInput}
.networkSearchConfig=${config.networkSearchConfig}
.theme=${theme}
></ai-panel-input>`,
],
[
@@ -564,6 +591,9 @@ export class AffineAIPanelWidget extends WidgetComponent {
@property()
accessor state: AffineAIPanelState = 'hidden';
@property({ attribute: 'data-app-theme', reflect: true })
accessor appTheme: ColorScheme = ColorScheme.Light;
}
export const aiPanelWidget = WidgetViewExtension(

View File

@@ -1,9 +1,14 @@
import { AIStarIcon } from '@blocksuite/affine/components/icons';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { ColorScheme } from '@blocksuite/affine/model';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { stopPropagation } from '@blocksuite/affine/shared/utils';
import { PublishIcon, SendIcon } from '@blocksuite/icons/lit';
import { css, html, LitElement, nothing } from 'lit';
import {
darkCssVariablesV2,
lightCssVariablesV2,
} from '@toeverything/theme/v2';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import type { AINetworkSearchConfig } from '../../type';
@@ -20,7 +25,6 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
display: flex;
align-items: flex-start;
gap: 8px;
background: var(--affine-background-overlay-panel-color);
}
.star {
@@ -47,7 +51,7 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
overflow: hidden;
padding: 0px;
color: var(--affine-text-primary-color);
color: ${unsafeCSSVarV2('text/primary')};
/* light/sm */
font-family: var(--affine-font-family);
@@ -58,11 +62,11 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
}
textarea::placeholder {
color: var(--affine-placeholder-color);
color: ${unsafeCSSVarV2('text/placeholder')};
}
textarea::-moz-placeholder {
color: var(--affine-placeholder-color);
color: ${unsafeCSSVarV2('text/placeholder')};
}
}
@@ -79,12 +83,15 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
color: ${unsafeCSSVarV2('button/pureWhiteText')};
}
}
.arrow[data-active] {
background: ${unsafeCSSVarV2('icon/activated')};
}
.arrow[data-active]:hover {
cursor: pointer;
}
.network {
display: flex;
align-items: center;
@@ -97,9 +104,90 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
color: ${unsafeCSSVarV2('icon/primary')};
}
}
.network[data-active='true'] svg {
color: ${unsafeCSSVarV2('icon/activated')};
}
:host([data-app-theme='light']) {
.network svg {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-icon-primary'])};
}
.network[data-active='true'] svg {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-icon-activated'])};
}
textarea {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-text-primary'])};
}
textarea::placeholder {
color: ${unsafeCSS(
lightCssVariablesV2['--affine-v2-text-placeholder']
)};
}
textarea::-moz-placeholder {
color: ${unsafeCSS(
lightCssVariablesV2['--affine-v2-text-placeholder']
)};
}
.arrow {
background: ${unsafeCSS(
lightCssVariablesV2['--affine-v2-icon-disable']
)};
}
.arrow[data-active] {
background: ${unsafeCSS(
lightCssVariablesV2['--affine-v2-icon-activated']
)};
}
.arrow svg {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-pure-white-text'])};
}
}
:host([data-app-theme='dark']) {
.network svg {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-icon-primary'])};
}
.network[data-active='true'] svg {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-icon-activated'])};
}
textarea {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-text-primary'])};
}
textarea::placeholder {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-text-placeholder'])};
}
textarea::-moz-placeholder {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-text-placeholder'])};
}
.arrow {
background: ${unsafeCSS(
darkCssVariablesV2['--affine-v2-icon-disable']
)};
}
.arrow[data-active] {
background: ${unsafeCSS(
darkCssVariablesV2['--affine-v2-icon-activated']
)};
}
.arrow svg {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-pure-white-text'])};
}
}
`;
private readonly _onInput = () => {
@@ -209,6 +297,9 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
@property({ attribute: false })
accessor onInput: ((input: string) => void) | undefined = undefined;
@property({ attribute: 'data-app-theme', reflect: true })
accessor theme: ColorScheme = ColorScheme.Light;
@query('textarea')
accessor textarea!: HTMLTextAreaElement;
}

View File

@@ -1,8 +1,11 @@
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { ThemeProvider } from '@blocksuite/affine/shared/services';
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
import { on, stopPropagation } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std';
import { css, html, LitElement, nothing } from 'lit';
import { darkCssVariables, lightCssVariables } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import type { AIItemGroupConfig } from '../../components/ai-item/types';
@@ -20,16 +23,44 @@ export class EdgelessCopilotPanel extends WithDisposable(LitElement) {
min-width: 330px;
max-height: 374px;
overflow-y: auto;
background: var(--affine-background-overlay-panel-color);
box-shadow: var(--affine-shadow-2);
background: ${unsafeCSSVar('--affine-background-overlay-panel-color')};
box-shadow: ${unsafeCSSVar('--affine-overlay-shadow')};
border-radius: 8px;
z-index: var(--affine-z-index-popover);
}
.edgeless-copilot-panel[data-app-theme='light'] {
background: ${unsafeCSS(
lightCssVariables['--affine-background-overlay-panel-color']
)};
box-shadow: ${unsafeCSS(lightCssVariables['--affine-overlay-shadow'])};
}
.edgeless-copilot-panel[data-app-theme='dark'] {
background: ${unsafeCSS(
darkCssVariables['--affine-background-overlay-panel-color']
)};
box-shadow: ${unsafeCSS(darkCssVariables['--affine-overlay-shadow'])};
}
.edgeless-copilot-panel[data-app-theme='dark'] ai-item {
background: blue;
}
${scrollbarStyle('.edgeless-copilot-panel')}
.edgeless-copilot-panel:hover::-webkit-scrollbar-thumb {
background-color: var(--affine-black-30);
}
.edgeless-copilot-panel[data-app-theme='light']:hover::-webkit-scrollbar-thumb {
background-color: ${unsafeCSS(lightCssVariables['--affine-black30'])};
}
.edgeless-copilot-panel[data-app-theme='dark']:hover::-webkit-scrollbar-thumb {
background-color: ${unsafeCSS(darkCssVariables['--affine-black30'])};
}
`;
private _getChain() {
@@ -40,6 +71,11 @@ export class EdgelessCopilotPanel extends WithDisposable(LitElement) {
super.connectedCallback();
this._disposables.add(on(this, 'wheel', stopPropagation));
this._disposables.add(on(this, 'pointerdown', stopPropagation));
this.disposables.add(
this.host.std.get(ThemeProvider).app$.subscribe(() => {
this.requestUpdate();
})
);
}
hide() {
@@ -47,6 +83,7 @@ export class EdgelessCopilotPanel extends WithDisposable(LitElement) {
}
override render() {
const appTheme = this.host.std.get(ThemeProvider).app$.value;
const chain = this._getChain();
const groups = this.groups.reduce((pre, group) => {
const filtered = group.items.filter(item =>
@@ -61,8 +98,9 @@ export class EdgelessCopilotPanel extends WithDisposable(LitElement) {
if (groups.every(group => group.items.length === 0)) return nothing;
return html`
<div class="edgeless-copilot-panel">
<div class="edgeless-copilot-panel" data-app-theme=${appTheme}>
<ai-item-list
.theme=${appTheme}
.onClick=${() => {
this.onClick?.();
}}