feat(editor): group configuration of note styles to panel from toolbar (#12230)

Close [BS-3401](https://linear.app/affine-design/issue/BS-3401/note-style-需要合并同类项)

![CleanShot 2025-05-12 at 14.23.01.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyRfgiN4RuBxJfrza3SG/ddf8623f-aa9c-491e-a7bf-90419fb19848.png)
This commit is contained in:
L-Sun
2025-05-12 09:42:52 +00:00
parent f3ca17fcb3
commit 42ae6e6e40
17 changed files with 685 additions and 462 deletions
@@ -1,175 +0,0 @@
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
import { ColorScheme, NoteShadow } from '@blocksuite/affine-model';
import { NoteShadowDuotoneIcon } from '@blocksuite/icons/lit';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { NoteNoShadowIcon, NoteShadowSampleIcon } from './icons';
const SHADOWS = [
{
type: NoteShadow.None,
styles: {
light: '',
dark: '',
},
tooltip: 'No shadow',
},
{
type: NoteShadow.Box,
styles: {
light:
'0px 0.2px 4.8px 0px rgba(66, 65, 73, 0.2), 0px 0px 1.6px 0px rgba(66, 65, 73, 0.2)',
dark: '0px 0.2px 6px 0px rgba(0, 0, 0, 0.44), 0px 0px 2px 0px rgba(0, 0, 0, 0.66)',
},
tooltip: 'Box shadow',
},
{
type: NoteShadow.Sticker,
styles: {
light:
'0px 9.6px 10.4px -4px rgba(66, 65, 73, 0.07), 0px 10.4px 7.2px -8px rgba(66, 65, 73, 0.22)',
dark: '0px 9.6px 10.4px -4px rgba(0, 0, 0, 0.66), 0px 10.4px 7.2px -8px rgba(0, 0, 0, 0.44)',
},
tooltip: 'Sticker shadow',
},
{
type: NoteShadow.Paper,
styles: {
light:
'0px 0px 0px 4px rgba(255, 255, 255, 1), 0px 1.2px 2.4px 4.8px rgba(66, 65, 73, 0.16)',
dark: '0px 1.2px 2.4px 4.8px rgba(0, 0, 0, 0.36), 0px 0px 0px 3.4px rgba(75, 75, 75, 1)',
},
tooltip: 'Paper shadow',
},
{
type: NoteShadow.Float,
styles: {
light:
'0px 5.2px 12px 0px rgba(66, 65, 73, 0.13), 0px 0px 0.4px 1px rgba(0, 0, 0, 0.06)',
dark: '0px 5.2px 12px 0px rgba(0, 0, 0, 0.66), 0px 0px 0.4px 1px rgba(0, 0, 0, 0.44)',
},
tooltip: 'Floation shadow',
},
{
type: NoteShadow.Film,
styles: {
light:
'0px 0px 0px 1.4px rgba(0, 0, 0, 1), 2.4px 2.4px 0px 1px rgba(0, 0, 0, 1)',
dark: '0px 0px 0px 1.4px rgba(178, 178, 178, 1), 2.4px 2.4px 0px 1px rgba(178, 178, 178, 1)',
},
tooltip: 'Film shadow',
},
];
export class EdgelessNoteShadowDropdownMenu extends LitElement {
static override styles = css`
:host {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.item {
padding: 8px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.item-icon {
display: flex;
justify-content: center;
align-items: center;
}
.item-icon svg rect:first-of-type {
fill: var(--background);
}
.item:hover {
background-color: var(--affine-hover-color);
}
.item[data-selected] {
border: 1px solid var(--affine-brand-color);
}
`;
select(value: NoteShadow) {
this.dispatchEvent(new CustomEvent('select', { detail: value }));
}
override render() {
const { value, background, theme } = this;
const isDark = theme === ColorScheme.Dark;
return html`
<editor-menu-button
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button
aria-label="Shadow style"
.tooltip="${'Shadow style'}"
>
${NoteShadowDuotoneIcon()} ${EditorChevronDown}
</editor-icon-button>
`}
>
<div
data-orientation="horizontal"
style=${styleMap({
'--background': background.startsWith('--')
? `var(${background})`
: background,
})}
>
${repeat(
SHADOWS,
shadow => shadow.type,
({ type, tooltip, styles: { dark, light } }, index) =>
html`<div
class="item"
?data-selected="${value === type}"
@click=${() => this.select(type)}
>
<editor-icon-button
class="item-icon"
data-testid="${type.replace('--', '')}"
.tooltip=${tooltip}
.tipPosition="${'bottom'}"
.iconContainerPadding=${0}
.hover=${false}
style=${styleMap({
boxShadow: `${isDark ? dark : light}`,
})}
>
${index === 0 ? NoteNoShadowIcon : NoteShadowSampleIcon}
</editor-icon-button>
</div>`
)}
</div>
</editor-menu-button>
`;
}
@property({ attribute: false })
accessor background!: string;
@property({ attribute: false })
accessor theme!: ColorScheme;
@property({ attribute: false })
accessor value!: NoteShadow;
}
declare global {
interface HTMLElementTagNameMap {
'edgeless-note-shadow-dropdown-menu': EdgelessNoteShadowDropdownMenu;
}
}
@@ -0,0 +1,206 @@
import { ColorScheme, NoteShadow } from '@blocksuite/affine-model';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css, html, LitElement, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { NoteNoShadowIcon, NoteShadowSampleIcon } from './icons';
type Shadow = {
type: NoteShadow;
light: Parameters<typeof styleMap>[0];
dark: Parameters<typeof styleMap>[0];
style: Parameters<typeof styleMap>[0];
tooltip: string;
};
const SHADOWS: Shadow[] = [
{
type: NoteShadow.None,
light: {},
dark: {},
style: {},
tooltip: 'No shadow',
},
{
type: NoteShadow.Box,
light: {
'--note-box-shadow-color-1': 'rgba(0, 0, 0, 0.14)',
'--note-box-shadow-color-2': 'rgba(0, 0, 0, 0.14)',
},
dark: {
'--note-box-shadow-color-1': 'rgba(0, 0, 0, 0.30)',
'--note-box-shadow-color-2': 'rgba(0, 0, 0, 0.44)',
},
style: {
boxShadow:
'0px 0.109px 2.621px var(--note-box-shadow-color-1), 0px 0px 0.874px var(--note-box-shadow-color-2)',
},
tooltip: 'Box shadow',
},
{
type: NoteShadow.Sticker,
light: {
'--note-sticker-shadow-color-1': 'rgba(0, 0, 0, 0.08)',
'--note-sticker-shadow-color-2': 'rgba(0, 0, 0, 0.10)',
},
dark: {
'--note-sticker-shadow-color-1': 'rgba(0, 0, 0, 0.22)',
'--note-sticker-shadow-color-2': 'rgba(0, 0, 0, 0.52)',
},
style: {
boxShadow:
'0px 5.243px 5.68px var(--note-sticker-shadow-color-1), 0px 5.68px 3.932px var(--note-sticker-shadow-color-2)',
},
tooltip: 'Sticker shadow',
},
{
type: NoteShadow.Paper,
light: {
'--note-paper-shadow-color-1': 'rgba(0, 0, 0, 0.14)',
'--note-paper-shadow-color-2': '#FFF',
},
dark: {
'--note-paper-shadow-color-1': 'rgba(0, 0, 0, 0.30)',
'--note-paper-shadow-color-2': '#7A7A7A',
},
style: {
border: '2px solid var(--note-paper-shadow-color-2)',
boxShadow: '0px 0.655px 1.311px var(--note-paper-shadow-color-1)',
},
tooltip: 'Paper shadow',
},
{
type: NoteShadow.Float,
light: {
'--note-float-shadow-color-1': 'rgba(0, 0, 0, 0.09)',
'--note-float-shadow-color-2': 'rgba(0, 0, 0, 0.10)',
},
dark: {
'--note-float-shadow-color-1': 'rgba(0, 0, 0, 0.30)',
'--note-float-shadow-color-2': 'rgba(0, 0, 0, 0.44)',
},
style: {
boxShadow:
'0px 2.84px 6.554px var(--note-float-shadow-color-1), 0px 0px 0.218px var(--note-float-shadow-color-2)',
},
tooltip: 'Floating shadow',
},
{
type: NoteShadow.Film,
light: {
'--note-film-shadow-color-1': '#000',
'--note-film-shadow-color-2': '#000',
},
dark: {
'--note-film-shadow-color-1': '#7A7A7A',
'--note-film-shadow-color-2': '#7A7A7A',
},
style: {
border: '1px solid var(--note-film-shadow-color-1)',
boxShadow: '2px 2px 0px var(--note-film-shadow-color-2)',
},
tooltip: 'Film shadow',
},
];
export class EdgelessNoteShadowMenu extends LitElement {
static override styles = css`
:host {
display: flex;
align-items: center;
justify-content: space-between;
}
.item {
padding: 4.369px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.item-icon {
display: flex;
justify-content: center;
align-items: center;
}
.item-icon svg {
width: 30px;
height: auto;
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/blackBorder')};
fill: ${unsafeCSSVarV2('layer/insideBorder/blackBorder')};
}
.item-icon svg rect:first-of-type {
fill: var(--background);
}
.item:hover {
background-color: var(--affine-hover-color);
}
.item[data-selected] {
border: 1px solid var(--affine-brand-color);
}
`;
select(value: NoteShadow) {
this.dispatchEvent(new CustomEvent('select', { detail: value }));
}
override willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has('background')) {
this.style.setProperty('--background', this.background);
}
}
override render() {
const { value, theme } = this;
const isDark = theme === ColorScheme.Dark;
return repeat(
SHADOWS,
shadow => shadow.type,
({ type, tooltip, style, light, dark }, index) =>
html`<div
class="item"
?data-selected="${value === type}"
@click=${() => this.select(type)}
>
<editor-icon-button
class="item-icon"
data-testid="${type.replace('--', '')}"
.tooltip=${tooltip}
.tipPosition="${'bottom'}"
.iconContainerPadding=${0}
.hover=${false}
style=${styleMap({
...(isDark ? dark : light),
...style,
})}
>
${index === 0 ? NoteNoShadowIcon : NoteShadowSampleIcon}
</editor-icon-button>
</div>`
);
}
@property({ attribute: false })
accessor background!: string;
@property({ attribute: false })
accessor theme!: ColorScheme;
@property({ attribute: false })
accessor value!: NoteShadow;
}
declare global {
interface HTMLElementTagNameMap {
'edgeless-note-shadow-menu': EdgelessNoteShadowMenu;
}
}
@@ -0,0 +1,400 @@
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
import {
packColorsWith,
type PickColorEvent,
preprocessColor,
} from '@blocksuite/affine-components/color-picker';
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
import type { SliderSelectEvent } from '@blocksuite/affine-components/slider';
import {
DefaultTheme,
NoteBlockModel,
type NoteProps,
type NoteShadow,
resolveColor,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import {
type ColorEvent,
getMostCommonResolvedValue,
stopPropagation,
} from '@blocksuite/affine-shared/utils';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ArrowLeftSmallIcon, PaletteIcon } from '@blocksuite/icons/lit';
import { BlockStdScope, PropTypes, requiredProperties } from '@blocksuite/std';
import { css, html, LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
@requiredProperties({
notes: PropTypes.arrayOf(model => model instanceof NoteBlockModel),
std: PropTypes.instanceOf(BlockStdScope),
})
export class EdgelessNoteStylePanel extends SignalWatcher(
WithDisposable(LitElement)
) {
@property({ attribute: false })
accessor notes!: NoteBlockModel[];
@property({ attribute: false })
accessor std!: BlockStdScope;
@query('.edgeless-note-style-panel')
private accessor _panel!: HTMLDivElement;
@state()
accessor tabType: 'style' | 'customColor' = 'style';
static override styles = css`
.edgeless-note-style-panel {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.edgeless-note-style-section {
display: flex;
flex-direction: column;
align-items: stretch;
}
.edgeless-note-style-section-title {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 4px;
height: 22px;
align-self: stretch;
color: ${unsafeCSSVarV2('text/secondary')};
font-feature-settings:
'liga' off,
'clig' off;
/* Client/sm */
font-family: var(--affine-font-family);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
edgeless-line-styles-panel {
display: flex;
flex-direction: row;
gap: 8px;
}
.edgeless-note-corner-radius-panel {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 8px;
affine-slider {
width: 168px;
}
input {
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
border-radius: 4px;
text-indent: 4px;
box-sizing: border-box;
width: 88px;
color: ${unsafeCSSVarV2('text/placeholder')};
}
input:focus {
outline: none;
border-color: ${unsafeCSSVarV2('input/border/active')};
color: ${unsafeCSSVarV2('text/primary')};
}
}
.edgeless-note-style-custom-color-panel {
display: flex;
flex-direction: column;
align-items: stretch;
}
.edgeless-note-custom-color-picker {
padding-top: 0px;
}
`;
private _styleChanged = false;
private _beforeChange() {
if (!this._styleChanged) {
// record the history
this.std.store.captureSync();
this._styleChanged = true;
}
}
private get _theme() {
return this.std.get(ThemeProvider).edgeless$.value;
}
private get _background() {
return (
getMostCommonResolvedValue(
this.notes.map(model => model.props),
'background',
background => resolveColor(background, this._theme)
) ?? resolveColor(DefaultTheme.noteBackgrounColor, this._theme)
);
}
private get _originalBackground() {
return this.notes[0].props.background;
}
private get _shadow() {
return this.notes[0].props.edgeless.style.shadowType;
}
private get _borderSize() {
return this.notes[0].props.edgeless.style.borderSize;
}
private get _borderStyle() {
return this.notes[0].props.edgeless.style.borderStyle;
}
private get _borderRadius() {
return this.notes[0].props.edgeless.style.borderRadius;
}
private readonly _switchToCustomColorTab = () => {
this.tabType = 'customColor';
};
private readonly _switchToStyleTab = () => {
this.tabType = 'style';
};
private readonly _selectColor = (e: ColorEvent) => {
this._beforeChange();
const color = e.detail.value;
const crud = this.std.get(EdgelessCRUDIdentifier);
this.notes.forEach(note => {
crud.updateElement(note.id, {
background: color,
} satisfies Partial<NoteProps>);
});
};
private readonly _pickColor = (e: PickColorEvent) => {
console.log(e);
};
private readonly _selectShadow = (e: CustomEvent<NoteShadow>) => {
this._beforeChange();
const shadowType = e.detail;
const crud = this.std.get(EdgelessCRUDIdentifier);
this.notes.forEach(note => {
crud.updateElement(note.id, {
edgeless: {
...note.props.edgeless,
style: {
...note.props.edgeless.style,
shadowType,
},
},
} satisfies Partial<NoteProps>);
});
};
private readonly _selectBorder = (e: CustomEvent<LineDetailType>) => {
this._beforeChange();
const { type, value } = e.detail;
const crud = this.std.get(EdgelessCRUDIdentifier);
if (type === 'size') {
const borderSize = value;
this.notes.forEach(note => {
const edgeless = note.props.edgeless;
crud.updateElement(note.id, {
edgeless: {
...edgeless,
style: {
...edgeless.style,
borderSize,
},
},
} satisfies Partial<NoteProps>);
});
} else {
const borderStyle = value;
this.notes.forEach(note => {
const edgeless = note.props.edgeless;
crud.updateElement(note.id, {
edgeless: { ...edgeless, style: { ...edgeless.style, borderStyle } },
} satisfies Partial<NoteProps>);
});
}
};
private readonly _selectBorderRadius = (
e: SliderSelectEvent | InputEvent
) => {
this._beforeChange();
let borderRadius = this._borderRadius;
if (e instanceof InputEvent) {
const target = e.target as HTMLInputElement;
const value = parseInt(target.value);
if (isNaN(value)) {
return;
}
borderRadius = value;
} else {
borderRadius = e.detail.value;
}
const crud = this.std.get(EdgelessCRUDIdentifier);
this.notes.forEach(note => {
crud.updateElement(note.id, {
edgeless: {
...note.props.edgeless,
style: { ...note.props.edgeless.style, borderRadius },
},
} satisfies Partial<NoteProps>);
});
};
private _renderStylePanel() {
return html` <div class="edgeless-note-style-panel">
<div class="edgeless-note-style-section">
<div class="edgeless-note-style-section-title">Fill color</div>
<edgeless-color-panel
role="listbox"
.value=${this._background}
.theme=${this._theme}
.palettes=${DefaultTheme.NoteBackgroundColorPalettes}
.hasTransparent=${false}
.columns=${DefaultTheme.NoteBackgroundColorPalettes.length + 1}
@select=${this._selectColor}
>
<edgeless-color-custom-button
slot="custom"
@click=${this._switchToCustomColorTab}
></edgeless-color-custom-button>
</edgeless-color-panel>
</div>
<div class="edgeless-note-style-section">
<div class="edgeless-note-style-section-title">Shadow</div>
<edgeless-note-shadow-menu
.background=${this._background}
.theme=${this._theme}
.value=${this._shadow}
@select=${this._selectShadow}
></edgeless-note-shadow-menu>
</div>
<div
class="edgeless-note-style-section"
data-testid="affine-note-border-style-panel"
>
<div class="edgeless-note-style-section-title">Border</div>
<edgeless-line-styles-panel
.lineSize=${this._borderSize}
.lineStyle=${this._borderStyle}
@select=${this._selectBorder}
></edgeless-line-styles-panel>
</div>
<div
class="edgeless-note-style-section"
data-testid="affine-note-corner-radius-panel"
>
<div class="edgeless-note-style-section-title">Corner Radius</div>
<div class="edgeless-note-corner-radius-panel">
<affine-slider
.value=${this._borderRadius}
.range=${{
points: [0, 4, 8, 16, 24, 32],
}}
@select=${this._selectBorderRadius}
></affine-slider>
<editor-toolbar-separator></editor-toolbar-separator>
<input
type="text"
inputmode="numeric"
pattern="[0-9]*"
min="0"
max="32"
.value=${this._borderRadius}
@input=${this._selectBorderRadius}
@click=${stopPropagation}
@pointerdown=${stopPropagation}
@cut=${stopPropagation}
@copy=${stopPropagation}
@paste=${stopPropagation}
/>
</div>
</div>
</div>`;
}
private _renderCustomColorPanel() {
const packed = packColorsWith(
this._theme,
this._background,
this._originalBackground
);
const type = packed.type === 'palette' ? 'normal' : packed.type;
const modes = packed.colors.map(
preprocessColor(window.getComputedStyle(this))
);
return html`<div class="edgeless-note-style-custom-color-panel">
<div class="edgeless-note-style-section-title">
<editor-icon-button
aria-label="Back"
.iconSize=${'16px'}
@click=${this._switchToStyleTab}
>
${ArrowLeftSmallIcon()}
</editor-icon-button>
Custom color
</div>
<edgeless-color-picker
class="edgeless-note-custom-color-picker"
.pick=${this._pickColor}
.colors=${{ type, modes }}
></edgeless-color-picker>
</div>`;
}
override firstUpdated() {
this.disposables.addFromEvent(this._panel, 'click', e => {
e.stopPropagation();
});
}
override render() {
return html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button aria-label="Note Style" .tooltip=${'Note Style'}>
${PaletteIcon()}
</editor-icon-button>
`}
>
${choose(this.tabType, [
['style', () => this._renderStylePanel()],
['customColor', () => this._renderCustomColorPanel()],
])}
</editor-menu-button>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'edgeless-note-style-panel': EdgelessNoteStylePanel;
}
}
@@ -5,23 +5,13 @@ export const NoteNoShadowIcon = html`
width="60"
height="72"
viewBox="0 0 60 72"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="60" height="72" />
<rect
x="0.5"
y="0.5"
width="58.0769"
height="71"
stroke="black"
stroke-opacity="0.1"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M21.9576 26.8962L38.6423 43.5809C42.5269 38.9268 42.2845 31.993 37.9149 27.6235C33.5454 23.254 26.6117 23.0115 21.9576 26.8962ZM37.1193 45.1038L20.4346 28.4192C16.55 33.0732 16.7924 40.007 21.162 44.3765C25.5315 48.746 32.4652 48.9885 37.1193 45.1038ZM19.639 26.1005C25.1063 20.6332 33.9706 20.6332 39.4379 26.1005C44.9053 31.5678 44.9053 40.4322 39.4379 45.8995C33.9706 51.3668 25.1063 51.3668 19.639 45.8995C14.1716 40.4322 14.1716 31.5678 19.639 26.1005Z"
fill="black"
fill="currentColor"
fill-opacity="0.1"
/>
</svg>
@@ -41,7 +31,7 @@ export const NoteShadowSampleIcon = html`
width="32.3077"
height="4.61538"
rx="2"
fill="black"
fill="currentColor"
fill-opacity="0.1"
/>
<rect
@@ -50,7 +40,7 @@ export const NoteShadowSampleIcon = html`
width="40.6154"
height="2.76923"
rx="1.38462"
fill="black"
fill="currentColor"
fill-opacity="0.1"
/>
<rect
@@ -59,7 +49,7 @@ export const NoteShadowSampleIcon = html`
width="40.6154"
height="2.76923"
rx="1.38462"
fill="black"
fill="currentColor"
fill-opacity="0.1"
/>
<rect
@@ -68,7 +58,7 @@ export const NoteShadowSampleIcon = html`
width="40.6154"
height="2.76923"
rx="1.38462"
fill="black"
fill="currentColor"
fill-opacity="0.1"
/>
<rect
@@ -77,7 +67,7 @@ export const NoteShadowSampleIcon = html`
width="13.8462"
height="2.76923"
rx="1.38462"
fill="black"
fill="currentColor"
fill-opacity="0.1"
/>
</svg>
@@ -1,19 +1,5 @@
import {
EdgelessCRUDIdentifier,
EdgelessLegacySlotIdentifier,
} from '@blocksuite/affine-block-surface';
import {
packColor,
type PickColorEvent,
} from '@blocksuite/affine-components/color-picker';
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
import {
DefaultTheme,
NoteBlockModel,
NoteDisplayMode,
type NoteShadow,
resolveColor,
} from '@blocksuite/affine-model';
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
import {
NotificationProvider,
SidebarExtensionIdentifier,
@@ -22,11 +8,9 @@ import {
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
import { getMostCommonResolvedValue } from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/gfx';
import {
AutoHeightIcon,
CornerIcon,
CustomizedHeightIcon,
InsertIntoPageIcon,
ScissorsIcon,
@@ -35,7 +19,6 @@ import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { computed } from '@preact/signals-core';
import { html } from 'lit';
import { keyed } from 'lit/directives/keyed.js';
import { changeNoteDisplayMode } from '../commands';
import { NoteConfigExtension } from '../config';
@@ -44,14 +27,6 @@ const trackBaseProps = {
category: 'note',
};
const CORNER_LIST = [
{ key: 'None', value: 0 },
{ key: 'Small', value: 8 },
{ key: 'Medium', value: 16 },
{ key: 'Large', value: 24 },
{ key: 'Huge', value: 32 },
] as const;
const builtinSurfaceToolbarConfig = {
actions: [
{
@@ -131,64 +106,6 @@ const builtinSurfaceToolbarConfig = {
};
},
},
{
id: 'c.color-picker',
when(ctx) {
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
return (
elements.length > 0 &&
elements[0].props.displayMode !== NoteDisplayMode.DocOnly
);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
if (!models.length) return null;
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
const theme = ctx.theme.edgeless$.value;
const firstModel = models[0];
const background =
getMostCommonResolvedValue(
models.map(model => model.props),
'background',
background => resolveColor(background, theme)
) ?? resolveColor(DefaultTheme.noteBackgrounColor, theme);
const onPick = (e: PickColorEvent) => {
const field = 'background';
if (e.type === 'pick') {
const color = e.detail.value;
for (const model of models) {
const props = packColor(field, color);
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, props);
}
return;
}
for (const model of models) {
model[e.type === 'start' ? 'stash' : 'pop'](field);
}
};
return html`
<edgeless-color-picker-button
class="background"
.label="${'Background'}"
.pick=${onPick}
.color=${background}
.colorPanelClass="${'small'}"
.theme=${theme}
.palettes=${DefaultTheme.NoteBackgroundColorPalettes}
.originalColor=${firstModel.props.background}
.enableCustomColor=${enableCustomColor}
>
</edgeless-color-picker-button>
`;
},
},
{
id: 'd.style',
when(ctx) {
@@ -200,147 +117,20 @@ const builtinSurfaceToolbarConfig = {
},
actions: [
{
id: 'a.shadow-style',
content(ctx) {
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
if (!models.length) return null;
const theme = ctx.theme.edgeless$.value;
const firstModel = models[0];
const { shadowType } = firstModel.props.edgeless.style;
const background =
getMostCommonResolvedValue(
models.map(model => model.props),
'background',
background => resolveColor(background, theme)
) ?? resolveColor(DefaultTheme.noteBackgrounColor, theme);
const onSelect = (e: CustomEvent<NoteShadow>) => {
e.stopPropagation();
const shadowType = e.detail;
for (const model of models) {
const edgeless = model.props.edgeless;
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
edgeless: {
...edgeless,
style: {
...edgeless.style,
shadowType,
},
},
});
}
};
return html`${keyed(
firstModel,
html`<edgeless-note-shadow-dropdown-menu
@select=${onSelect}
.value="${shadowType}"
.background="${background}"
.theme="${theme}"
></edgeless-note-shadow-dropdown-menu>`
)}`;
},
} satisfies ToolbarAction,
{
id: 'b.border-style',
content(ctx) {
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
if (!models.length) return null;
const firstModel = models[0];
const { borderSize, borderStyle } = firstModel.props.edgeless.style;
const onSelect = (e: CustomEvent<LineDetailType>) => {
e.stopPropagation();
const { type, value } = e.detail;
if (type === 'size') {
const borderSize = value;
for (const model of models) {
const edgeless = model.props.edgeless;
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
edgeless: {
...edgeless,
style: {
...edgeless.style,
borderSize,
},
},
});
}
return;
}
const borderStyle = value;
for (const model of models) {
const edgeless = model.props.edgeless;
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
edgeless: {
...edgeless,
style: {
...edgeless.style,
borderStyle,
},
},
});
}
};
return html`${keyed(
firstModel,
html`
<edgeless-note-border-dropdown-menu
@select=${onSelect}
.lineSize=${borderSize}
.lineStyle=${borderStyle}
></edgeless-note-border-dropdown-menu>
`
)}`;
},
} satisfies ToolbarAction,
{
id: 'c.corners',
label: 'Corners',
content(ctx) {
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
if (!models.length) return null;
const label = this.label;
const firstModel = models[0];
const borderRadius$ = computed(
() => firstModel.props.edgeless$.value.style.borderRadius
id: 'b.style',
when: ctx => {
const models = ctx.getSurfaceModels();
return (
models.length > 0 &&
models.every(model => model instanceof NoteBlockModel)
);
const onSelect = (e: CustomEvent<number>) => {
e.stopPropagation();
const borderRadius = e.detail;
for (const model of models) {
const edgeless = model.props.edgeless;
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
edgeless: {
...edgeless,
style: {
...edgeless.style,
borderRadius,
},
},
});
}
};
return html`${keyed(
firstModel,
html`<affine-size-dropdown-menu
@select=${onSelect}
.label="${label}"
.icon=${CornerIcon()}
.sizes=${CORNER_LIST}
.size$=${borderRadius$}
></affine-size-dropdown-menu>`
)}`;
},
content(ctx) {
const notes = ctx.getSurfaceModelsByType(NoteBlockModel);
return html`<edgeless-note-style-panel
.notes=${notes}
.std=${ctx.std}
></edgeless-note-style-panel>`;
},
} satisfies ToolbarAction,
],
+4 -6
View File
@@ -2,24 +2,21 @@ import { EdgelessNoteBackground } from './components/edgeless-note-background';
import { EdgelessNoteBorderDropdownMenu } from './components/edgeless-note-border-dropdown-menu';
import { EdgelessNoteDisplayModeDropdownMenu } from './components/edgeless-note-display-mode-dropdown-menu';
import { EdgelessNoteMask } from './components/edgeless-note-mask';
import { EdgelessNoteShadowDropdownMenu } from './components/edgeless-note-shadow-dropdown-menu';
import { EdgelessNoteShadowMenu } from './components/edgeless-note-shadow-menu';
import { EdgelessNoteStylePanel } from './components/edgeless-note-style-panel';
import { EdgelessPageBlockTitle } from './components/edgeless-page-block-title';
import { NoteBlockComponent } from './note-block';
import {
AFFINE_EDGELESS_NOTE,
EdgelessNoteBlockComponent,
} from './note-edgeless-block';
export function effects() {
customElements.define('affine-note', NoteBlockComponent);
customElements.define(AFFINE_EDGELESS_NOTE, EdgelessNoteBlockComponent);
customElements.define('edgeless-note-mask', EdgelessNoteMask);
customElements.define('edgeless-note-background', EdgelessNoteBackground);
customElements.define('edgeless-page-block-title', EdgelessPageBlockTitle);
customElements.define(
'edgeless-note-shadow-dropdown-menu',
EdgelessNoteShadowDropdownMenu
);
customElements.define('edgeless-note-shadow-menu', EdgelessNoteShadowMenu);
customElements.define(
'edgeless-note-border-dropdown-menu',
EdgelessNoteBorderDropdownMenu
@@ -28,4 +25,5 @@ export function effects() {
'edgeless-note-display-mode-dropdown-menu',
EdgelessNoteDisplayModeDropdownMenu
);
customElements.define('edgeless-note-style-panel', EdgelessNoteStylePanel);
}
@@ -2,7 +2,7 @@ import type { Color, ColorScheme, Palette } from '@blocksuite/affine-model';
import { DefaultTheme, resolveColor } from '@blocksuite/affine-model';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { ColorEvent } from '@blocksuite/affine-shared/utils';
import { css, html, LitElement } from 'lit';
import { css, html, LitElement, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
@@ -115,12 +115,12 @@ export class EdgelessColorPanel extends LitElement {
:host {
display: grid;
grid-gap: 4px;
grid-template-columns: repeat(9, 1fr);
grid-template-columns: repeat(var(--columns, 9), 1fr);
}
/* note */
:host(.small) {
grid-template-columns: repeat(6, 1fr);
grid-template-columns: repeat(var(--columns, 6), 1fr);
grid-gap: 8px;
}
@@ -156,6 +156,16 @@ export class EdgelessColorPanel extends LitElement {
return this.value && resolveColor(this.value, this.theme);
}
override willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has('columns')) {
if (this.columns) {
this.style.setProperty('--columns', this.columns.toString());
} else {
this.style.removeProperty('--columns');
}
}
}
override render() {
return html`
${repeat(
@@ -197,6 +207,9 @@ export class EdgelessColorPanel extends LitElement {
@property({ attribute: false })
accessor value: Color | null = null;
@property({ attribute: false })
accessor columns: number | undefined = undefined;
}
export class EdgelessTextColorIcon extends LitElement {
@@ -1,4 +1,5 @@
import { isTransparent } from '@blocksuite/affine-model';
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
import { html, svg, type TemplateResult } from 'lit';
export function TransparentIcon(hollowCircle = false) {
@@ -18,7 +19,7 @@ export function TransparentIcon(hollowCircle = false) {
fill-rule="evenodd"
clip-rule="evenodd"
d="M-1.17405 5.17857C-1.2241 5.5285 -1.25 5.88623 -1.25 6.25V8.39286H1.96429V11.6071H-1.25V13.75C-1.25 14.1138 -1.2241 14.4715 -1.17405 14.8214H1.96429V18.0357H0.0943102C0.602244 18.7639 1.23609 19.3978 1.96429 19.9057V18.0357L5.17857 18.0357V21.174C5.5285 21.2241 5.88623 21.25 6.25 21.25H8.39286V18.0357H11.6071V21.25H13.75C14.1138 21.25 14.4715 21.2241 14.8214 21.174V18.0357H18.0357L18.0357 19.9057C18.7639 19.3978 19.3978 18.7639 19.9057 18.0357L18.0357 18.0357V14.8214H21.174C21.2241 14.4715 21.25 14.1138 21.25 13.75V11.6071H18.0357V8.39286H21.25V6.25C21.25 5.88623 21.2241 5.5285 21.174 5.17857H18.0357V1.96429H19.9057C19.3978 1.23609 18.7639 0.602244 18.0357 0.09431L18.0357 1.96429H14.8214V-1.17405C14.4715 -1.2241 14.1138 -1.25 13.75 -1.25H11.6071V1.96429H8.39286V-1.25H6.25C5.88623 -1.25 5.5285 -1.2241 5.17857 -1.17405V1.96429H1.96429V0.0943099C1.23609 0.602244 0.602244 1.23609 0.0943099 1.96429H1.96429V5.17857H-1.17405ZM5.17857 5.17857V1.96429H8.39286V5.17857H5.17857ZM5.17857 8.39286H1.96429V5.17857H5.17857V8.39286ZM8.39286 8.39286V5.17857H11.6071V8.39286H8.39286ZM8.39286 11.6071V8.39286H5.17857V11.6071H1.96429V14.8214H5.17857V18.0357H8.39286V14.8214H11.6071V18.0357H14.8214V14.8214H18.0357V11.6071H14.8214V8.39286H18.0357V5.17857H14.8214V1.96429H11.6071V5.17857H14.8214V8.39286H11.6071V11.6071H8.39286ZM8.39286 11.6071V14.8214H5.17857V11.6071H8.39286ZM11.6071 11.6071H14.8214V14.8214H11.6071V11.6071Z"
fill="#D9D9D9"
fill="${cssVarV2('icon/tertiary')}"
/>
${CircleIcon}
</svg>
@@ -1,3 +1,4 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import type { Placement } from '@floating-ui/dom';
import type { TemplateResult } from 'lit';
import { css, html, LitElement, nothing } from 'lit';
@@ -19,7 +20,7 @@ export class EditorIconButton extends LitElement {
display: flex;
align-items: center;
padding: var(--icon-container-padding);
color: var(--affine-icon-color);
color: ${unsafeCSSVarV2('icon/primary')};
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
@@ -38,16 +38,13 @@ const Heavy = {
} as const;
const NoteBackgroundColorMap = {
Yellow: getColorByKey('edgeless/note/yellow'),
Orange: getColorByKey('edgeless/note/orange'),
Red: getColorByKey('edgeless/note/red'),
Magenta: getColorByKey('edgeless/note/magenta'),
Purple: getColorByKey('edgeless/note/purple'),
Blue: getColorByKey('edgeless/note/blue'),
Teal: getColorByKey('edgeless/note/teal'),
Orange: getColorByKey('edgeless/note/orange'),
Yellow: getColorByKey('edgeless/note/yellow'),
Green: getColorByKey('edgeless/note/green'),
Black: getColorByKey('edgeless/note/black'),
Grey: getColorByKey('edgeless/note/grey'),
Blue: getColorByKey('edgeless/note/blue'),
Purple: getColorByKey('edgeless/note/purple'),
Magenta: getColorByKey('edgeless/note/magenta'),
White: getColorByKey('edgeless/note/white'),
Transparent: Transparent,
} as const;
@@ -315,8 +315,9 @@ test.describe('edgeless note element toolbar', () => {
},
});
await toolbar.getByRole('button', { name: 'Shadow style' }).click();
await toolbar.getByTestId('affine-note-shadow-film').click();
await toolbar.getByRole('button', { name: 'Note Style' }).click();
const noteStylePanel = page.locator('edgeless-note-style-panel');
await noteStylePanel.getByTestId('affine-note-shadow-film').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
@@ -327,10 +328,11 @@ test.describe('edgeless note element toolbar', () => {
},
});
await toolbar.getByRole('button', { name: 'Border style' }).click();
await toolbar.locator('.mode-solid').click();
await toolbar.getByRole('button', { name: 'Border style' }).click();
await toolbar.locator('affine-slider').getByLabel('8').click();
const borderStylePanel = noteStylePanel.getByTestId(
'affine-note-border-style-panel'
);
await borderStylePanel.locator('.mode-solid').click();
await borderStylePanel.locator('affine-slider').getByLabel('8').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
@@ -341,12 +343,10 @@ test.describe('edgeless note element toolbar', () => {
},
});
await toolbar.getByRole('button', { name: 'Corners' }).click();
// TODO(@fundon): delete duplicate components
await toolbar
.locator('affine-size-dropdown-menu')
.getByText('Large')
.click();
const cornerPanel = noteStylePanel.getByTestId(
'affine-note-corner-radius-panel'
);
await cornerPanel.locator('affine-slider').getByLabel('24').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
@@ -357,6 +357,8 @@ test.describe('edgeless note element toolbar', () => {
},
});
await pressEscape(page);
const headerToolbar = page.getByTestId('edgeless-page-block-header');
const toggleButton = headerToolbar.getByTestId(
'edgeless-note-toggle-button'
@@ -301,12 +301,12 @@ test('should focus on input of popover on toolbar', async ({ page }) => {
const scaleValue = await scaleInput.inputValue();
expect(scaleValue).toBe('150');
const cornersMenu = toolbar.locator('.corners-menu');
const cornersInput = cornersMenu.locator('input');
const cornersButton = cornersMenu.getByLabel('Corners');
const stylePanelButton = toolbar.locator('edgeless-note-style-panel');
const cornersInput = stylePanelButton.locator(
'.edgeless-note-corner-radius-panel input'
);
await expect(cornersInput).toBeHidden();
await cornersButton.click();
await stylePanelButton.click();
await cornersInput.click();
await expect(cornersInput).toBeFocused();
+1 -1
View File
@@ -499,7 +499,7 @@ test(scoped`paste note block with background`, async ({ page }) => {
await switchEditorMode(page);
await selectNoteInEdgeless(page, ids.noteId);
await triggerComponentToolbarAction(page, 'changeNoteColor');
await triggerComponentToolbarAction(page, 'changeNoteStyle');
await changeEdgelessNoteBackground(page, 'White');
await assertEdgelessNoteBackground(
page,
@@ -191,7 +191,7 @@ test.describe('auto-complete', () => {
await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height / 2);
await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'changeNoteColor');
await triggerComponentToolbarAction(page, 'changeNoteStyle');
await changeEdgelessNoteBackground(page, 'Red');
// move to arrow icon
@@ -57,7 +57,7 @@ test('tooltip should be hidden after clicking on button', async ({ page }) => {
await expect(page.locator('note-display-mode-panel')).toBeVisible();
const colorBtn = toolbar.getByRole('button', {
name: 'Background',
name: 'Note Style',
});
await colorBtn.hover();
@@ -317,7 +317,7 @@ test('change note color', async ({ page }) => {
);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'changeNoteColor');
await triggerComponentToolbarAction(page, 'changeNoteStyle');
await changeEdgelessNoteBackground(page, 'Green');
await assertEdgelessNoteBackground(
page,
@@ -1057,7 +1057,7 @@ type Action =
| 'sendBackward'
| 'sendToBack'
| 'copyAsPng'
| 'changeNoteColor'
| 'changeNoteStyle'
| 'changeShapeStyle'
| 'changeShapeColor'
| 'changeShapeFillColor'
@@ -1253,10 +1253,10 @@ export async function triggerComponentToolbarAction(
await button.click();
break;
}
case 'changeNoteColor': {
const button = locatorComponentToolbar(page).getByRole('button', {
name: 'Background',
});
case 'changeNoteStyle': {
const button = locatorComponentToolbar(page).locator(
'edgeless-note-style-panel'
);
await button.click();
break;
}
@@ -1373,7 +1373,7 @@ export async function triggerComponentToolbarAction(
export async function changeEdgelessNoteBackground(page: Page, label: string) {
const colorButton = page
.locator('edgeless-color-picker-button')
.locator('edgeless-note-style-panel')
.locator('edgeless-color-panel')
.locator(`.color-unit[aria-label="${label}"]`);
await colorButton.click();