Compare commits

..

11 Commits

Author SHA1 Message Date
zzj3720 06b84330a9 feat: add icon picker functionality to callout block
- Add IconData type import to callout-model.ts
- Implement icon picker component in callout-block.ts
- Copy renderUniLit function to avoid external dependencies
- Integrate icon picker directly in renderBlock for testing
- Remove unused IconPickerServiceIdentifier import
2025-09-26 23:59:30 +08:00
zzj3720 5147e2c62d fix: remove unused ThemeExtensionIdentifier import 2025-09-26 18:41:42 +08:00
zzj3720 03e8e7143d Merge canary branch with callout background color feature 2025-09-26 15:02:08 +08:00
zzj3720 5e8691367d feat(callout): add formatbar with background color selection
- Add background property to CalloutBlockModel with default white color
- Implement dynamic background color rendering in CalloutBlockComponent
- Create toolbar configuration with color palette for background selection
- Register toolbar extension in CalloutViewExtension
- Support all note background colors with visual feedback for current selection
- Maintain consistency with other block formatbar implementations
2025-09-24 23:56:57 +08:00
3720 e3d88ab3f2 Merge branch 'canary' into fix/callout-delete-merge 2025-09-19 21:58:05 +08:00
zzj3720 61e40c7523 fix(callout): adjust callout styling and slash menu behavior
update callout block margins and spacing
add debug logs for slash menu disableWhen checks
remove slash menu disable test and update paragraph count assertions
2025-09-19 20:16:08 +08:00
zzj3720 cdb721d6a6 Merge branch 'fix/callout-delete-merge' of github.com:toeverything/AFFiNE into fix/callout-delete-merge 2025-09-17 19:43:06 +08:00
zzj3720 c89680cb55 refactor(callout): rename variable for clarity in callout keymap
The variable `calloutBlock` was being assigned directly from `std.store.getBlock`, which could be confusing. Renamed to `parentBlock` first to better reflect its purpose before assignment to `calloutBlock`.
2025-09-17 19:42:38 +08:00
3720 0256fdb2af Merge branch 'canary' into fix/callout-delete-merge 2025-09-17 19:18:45 +08:00
zzj3720 a4711aad61 fix: improve callout block functionality and slash menu configuration 2025-09-17 19:16:03 +08:00
zzj3720 6d97c5a393 fix(callout): fix text merging issue when deleting callout sub-blocks
- Fix text content disappearing after deleting callout sub-blocks
- Properly clone text content before deletion to prevent data loss
- Ensure text merges correctly to previous block with formatting preserved
- Improve cursor positioning after merge operation
2025-09-17 18:55:03 +08:00
293 changed files with 2344 additions and 5668 deletions
-2
View File
@@ -2,8 +2,6 @@ version: '3.8'
services:
app:
security_opt:
- no-new-privileges:true
image: mcr.microsoft.com/devcontainers/base:bookworm
volumes:
- ../..:/workspaces:cached
+2 -2
View File
@@ -684,7 +684,7 @@
},
"scenarios": {
"type": "object",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4-5@20250929\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"default": {
"override_enabled": false,
"scenarios": {
@@ -693,7 +693,7 @@
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
"coding": "claude-sonnet-4-5@20250929",
"coding": "claude-sonnet-4@20250514",
"complex_text_generation": "gpt-4o-2024-08-06",
"quick_decision_making": "gpt-5-mini",
"quick_text_generation": "gemini-2.5-flash",
+1 -1
View File
@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.25.2"
appVersion: "0.22.4"
+1 -1
View File
@@ -3,7 +3,7 @@ name: doc
description: AFFiNE doc server
type: application
version: 0.0.0
appVersion: "0.25.2"
appVersion: "0.22.4"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.25.2"
appVersion: "0.22.4"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
@@ -3,7 +3,7 @@ name: renderer
description: AFFiNE renderer server
type: application
version: 0.0.0
appVersion: "0.25.2"
appVersion: "0.22.4"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
+1 -1
View File
@@ -3,7 +3,7 @@ name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.25.2"
appVersion: "0.22.4"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
Generated
+3 -46
View File
@@ -161,9 +161,7 @@ dependencies = [
"affine_common",
"chrono",
"file-format",
"infer",
"mimalloc",
"mp4parse",
"napi",
"napi-build",
"napi-derive",
@@ -575,15 +573,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bitreader"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "886559b1e163d56c765bc3a985febb4eee8009f625244511d8ee3c432e08c066"
dependencies = [
"cfg-if",
]
[[package]]
name = "bitvec"
version = "1.0.1"
@@ -1493,15 +1482,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fallible_collections"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd"
dependencies = [
"hashbrown 0.13.2",
]
[[package]]
name = "fancy-regex"
version = "0.13.0"
@@ -1524,9 +1504,9 @@ dependencies = [
[[package]]
name = "file-format"
version = "0.28.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eab8aa2fba5f39f494000a22f44bf3c755b7d7f8ffad3f36c6d507893074159"
checksum = "e7ef3d5e8ae27277c8285ac43ed153158178ef0f79567f32024ca8140a0c7cd8"
[[package]]
name = "flate2"
@@ -1820,15 +1800,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@@ -1942,7 +1913,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.2",
"windows-core 0.57.0",
]
[[package]]
@@ -2522,20 +2493,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "mp4parse"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63a35203d3c6ce92d5251c77520acb2e57108c88728695aa883f70023624c570"
dependencies = [
"bitreader",
"byteorder",
"fallible_collections",
"log",
"num-traits",
"static_assertions",
]
[[package]]
name = "nanoid"
version = "0.4.0"
+1 -2
View File
@@ -39,7 +39,7 @@ crossbeam-channel = "0.5"
dispatch2 = "0.3"
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
dotenvy = "0.15"
file-format = { version = "0.28", features = ["reader"] }
file-format = { version = "0.26", features = ["reader"] }
homedir = "0.3"
infer = { version = "0.19.0" }
lasso = { version = "0.7", features = ["multi-threaded"] }
@@ -48,7 +48,6 @@ libc = "0.2"
log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
mimalloc = "0.1"
mp4parse = "0.17"
nanoid = "0.4"
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
+1 -1
View File
@@ -6,7 +6,7 @@
<br>
</h1>
<a href="https://affine.pro/download">
<img alt="affine logo" src="https://cdn.affine.pro/Github_hero_image2.png" style="width: 100%">
<img alt="affine logo" src="https://cdn.affine.pro/Github_hero_image1.png" style="width: 100%">
</a>
<br/>
<p align="center">
+6 -4
View File
@@ -6,12 +6,12 @@ We recommend users to always use the latest major version. Security updates will
| Version | Supported |
| --------------- | ------------------ |
| 0.24.x (stable) | :white_check_mark: |
| < 0.24.x | :x: |
| 0.17.x (stable) | :white_check_mark: |
| < 0.17.x | :x: |
## Reporting a Vulnerability
We welcome you to provide us with bug reports via and email at [security@toeverything.info](mailto:security@toeverything.info) or submit directly on [GitHub](https://github.com/toeverything/AFFiNE/security), **we encourage you to submit the relevant information directly via GitHub**. We expect your report to contain at least the following for us to evaluate and reproduce:
We welcome you to provide us with bug reports via and email at [security@toeverything.info](mailto:security@toeverything.info). We expect your report to contain at least the following for us to evaluate and reproduce:
1. Using platform and version, for example:
@@ -22,6 +22,8 @@ We welcome you to provide us with bug reports via and email at [security@toevery
3. Your classification or analysis of the vulnerability (optional)
Since we are an open source project, we also welcome you to provide corresponding fix PRs, we will determine specific rewards based on the evaluation results.
Since we are an open source project, we also welcome you to provide corresponding fix PRs.
We will provide bounties for vulnerabilities involving user information leakage, permission leakage, and unauthorized code execution. For other types of vulnerabilities, we will determine specific rewards based on the evaluation results.
If the vulnerability is caused by a library we depend on, we encourage you to submit a security report to the corresponding dependent library at the same time to benefit more users.
+1 -1
View File
@@ -296,7 +296,7 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2",
"version": "0.22.4",
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"msw": "^2.8.4",
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -10,6 +10,7 @@
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-inline-preset": "workspace:*",
@@ -22,7 +23,6 @@
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emoji-mart/data": "^1.2.1",
"@emotion/css": "^11.13.5",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -1,56 +0,0 @@
import { css } from '@emotion/css';
export const calloutHostStyles = css({
display: 'block',
margin: '8px 0',
});
export const calloutBlockContainerStyles = css({
display: 'flex',
alignItems: 'flex-start',
padding: '5px 10px',
borderRadius: '8px',
});
export const calloutEmojiContainerStyles = css({
userSelect: 'none',
fontSize: '1.2em',
width: '24px',
height: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
// marginTop is dynamically set by JavaScript based on first child's height
marginBottom: '10px',
flexShrink: 0,
position: 'relative',
});
export const calloutEmojiStyles = css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
':hover': {
cursor: 'pointer',
opacity: 0.7,
},
});
export const calloutChildrenStyles = css({
flex: 1,
minWidth: 0,
paddingLeft: '10px',
});
export const iconPickerContainerStyles = css({
position: 'absolute',
top: '100%',
left: 0,
zIndex: 1000,
background: 'white',
border: '1px solid #ccc',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
width: '390px',
height: '400px',
});
@@ -1,13 +1,6 @@
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import {
createPopup,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
import {
type CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { type CalloutBlockModel, DefaultTheme } from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
@@ -15,24 +8,15 @@ import {
type IconData,
IconPickerServiceIdentifier,
IconType,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import * as icons from '@blocksuite/icons/lit';
import type { BlockComponent } from '@blocksuite/std';
import { type Signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import { type Signal, signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { html } from 'lit';
import { css, html } from 'lit';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import {
calloutBlockContainerStyles,
calloutChildrenStyles,
calloutEmojiContainerStyles,
calloutEmojiStyles,
calloutHostStyles,
} from './callout-block-styles.js';
import { IconPickerWrapper } from './icon-picker-wrapper.js';
// Copy of renderUniLit and UniLit from affine-data-view
export const renderUniLit = <Props, Expose extends NonNullable<unknown>>(
uni: UniComponent<Props, Expose> | undefined,
@@ -51,8 +35,9 @@ export const renderUniLit = <Props, Expose extends NonNullable<unknown>>(
></uni-lit>`;
};
const getIcon = (icon?: IconData) => {
console.log(icon);
if (!icon) {
return null;
return '💡';
}
if (icon.type === IconType.Emoji) {
return icon.unicode;
@@ -62,64 +47,85 @@ const getIcon = (icon?: IconData) => {
icons as Record<string, (props: { style: string }) => TemplateResult>
)[`${icon.name}Icon`]?.({ style: `color:${icon.color}` });
}
return null;
return '💡';
};
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
private _popupCloseHandler: (() => void) | null = null;
override connectedCallback() {
super.connectedCallback();
this.classList.add(calloutHostStyles);
}
private _getEmojiMarginTop(): string {
if (this.model.children.length === 0) {
return '10px';
static override styles = css`
:host {
display: block;
margin: 8px 0;
}
const firstChild = this.model.children[0];
const flavour = firstChild.flavour;
const marginTopMap: Record<string, string> = {
'affine:paragraph:h1': '23px',
'affine:paragraph:h2': '20px',
'affine:paragraph:h3': '16px',
'affine:paragraph:h4': '15px',
'affine:paragraph:h5': '14px',
'affine:paragraph:h6': '13px',
};
// For heading blocks, use the type to determine margin
if (flavour === 'affine:paragraph') {
const paragraph = firstChild as ParagraphBlockModel;
const type = paragraph.props.type$.value;
const key = `${flavour}:${type}`;
return marginTopMap[key] || '10px';
.affine-callout-block-container {
display: flex;
align-items: flex-start;
padding: 5px 10px;
border-radius: 8px;
}
// Default for all other block types
return '10px';
}
private _closeIconPicker() {
if (this._popupCloseHandler) {
this._popupCloseHandler();
this._popupCloseHandler = null;
.affine-callout-emoji-container {
user-select: none;
font-size: 1.2em;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
margin-bottom: 10px;
flex-shrink: 0;
position: relative;
}
.affine-callout-emoji {
display: flex;
align-items: center;
justify-content: center;
}
.affine-callout-emoji:hover {
cursor: pointer;
opacity: 0.7;
}
.affine-callout-children {
flex: 1;
min-width: 0;
padding-left: 10px;
}
.icon-picker-container {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 300px;
height: 400px;
}
`;
private readonly showIconPicker$ = signal(false);
private _closeEmojiMenu() {
this.showIconPicker$.value = false;
}
private _toggleIconPicker(event: MouseEvent) {
// If popup is already open, close it
if (this._popupCloseHandler) {
this._closeIconPicker();
return;
private _toggleIconPicker() {
this.showIconPicker$.value = !this.showIconPicker$.value;
}
private _renderIconPicker() {
if (!this.showIconPicker$.value) {
return html``;
}
// Get IconPickerService from the framework
const iconPickerService = this.std.getOptional(IconPickerServiceIdentifier);
if (!iconPickerService) {
console.warn('IconPickerService not found');
return;
return html``;
}
// Get the uni-component from the service
@@ -129,31 +135,23 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
const props = {
onSelect: (iconData?: IconData) => {
this.model.props.icon$.value = iconData;
this._closeIconPicker(); // Close the picker after selection
this._closeEmojiMenu(); // Close the picker after selection
},
onClose: () => {
this._closeIconPicker();
this._closeEmojiMenu();
},
};
// Create IconPickerWrapper instance
const wrapper = new IconPickerWrapper();
wrapper.iconPickerComponent = iconPickerComponent;
wrapper.props = props;
wrapper.style.position = 'absolute';
wrapper.style.backgroundColor = cssVarV2.layer.background.overlayPanel;
wrapper.style.boxShadow = 'var(--affine-menu-shadow)';
wrapper.style.borderRadius = '8px';
// Create popup target from the clicked element
const target = popupTargetFromElement(event.currentTarget as HTMLElement);
// Create popup
this._popupCloseHandler = createPopup(target, wrapper, {
onClose: () => {
this._popupCloseHandler = null;
},
});
return html`
<div
@click=${(e: MouseEvent) => {
e.stopPropagation();
}}
class="icon-picker-container"
>
${renderUniLit(iconPickerComponent, props)}
</div>
`;
}
private readonly _handleBlockClick = (event: MouseEvent) => {
@@ -166,13 +164,6 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
return;
}
// If there's no icon, open icon picker on click
const icon = this.model.props.icon$.value;
if (!icon) {
this._toggleIconPicker(event);
return;
}
// Only handle clicks when there are no children
if (this.model.children.length > 0) {
return;
@@ -215,38 +206,33 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
override renderBlock() {
const icon = this.model.props.icon$.value;
const backgroundColorName = this.model.props.backgroundColorName$.value;
const backgroundColor = (
cssVarV2.block.callout.background as Record<string, string>
)[backgroundColorName ?? ''];
const background = this.model.props.background$.value;
const iconContent = getIcon(icon);
const themeProvider = this.std.get(ThemeProvider);
const theme = themeProvider.theme$.value;
const backgroundColor = themeProvider.generateColorProperty(
background || DefaultTheme.NoteBackgroundColorMap.White,
DefaultTheme.NoteBackgroundColorMap.White,
theme
);
return html`
<div
class="${calloutBlockContainerStyles}"
class="affine-callout-block-container"
@click=${this._handleBlockClick}
style=${styleMap({
backgroundColor: backgroundColor ?? 'transparent',
backgroundColor: backgroundColor,
})}
>
${iconContent
? html`
<div
@click=${this._toggleIconPicker}
contenteditable="false"
class="${calloutEmojiContainerStyles}"
style=${styleMap({
marginTop: this._getEmojiMarginTop(),
})}
>
<span class="${calloutEmojiStyles}" data-testid="callout-emoji"
>${iconContent}</span
>
</div>
`
: ''}
<div class="${calloutChildrenStyles}">
<div
@click=${this._toggleIconPicker}
contenteditable="false"
class="affine-callout-emoji-container"
>
<span class="affine-callout-emoji">${getIcon(icon)}</span>
${this._renderIconPicker()}
</div>
<div class="affine-callout-children">
${this.renderChildren(this.model)}
</div>
</div>
@@ -1,28 +1,18 @@
import {
createPopup,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
import { CalloutBlockModel } from '@blocksuite/affine-model';
import { CalloutBlockModel, DefaultTheme } from '@blocksuite/affine-model';
import {
ActionPlacement,
type IconData,
IconPickerServiceIdentifier,
type ToolbarAction,
type ToolbarActionGroup,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
import { DeleteIcon, PaletteIcon, SmileIcon } from '@blocksuite/icons/lit';
import { PaletteIcon } from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { IconPickerWrapper } from '../icon-picker-wrapper.js';
const colors = [
'default',
'red',
@@ -48,7 +38,27 @@ const backgroundColorAction = {
if (!model) return null;
const updateBackground = (color: string) => {
ctx.store.updateBlock(model, { backgroundColorName: color });
// Map text highlight colors to note background colors
const colorMap: Record<
string,
keyof typeof DefaultTheme.NoteBackgroundColorMap | null
> = {
default: null,
red: 'Red',
orange: 'Orange',
yellow: 'Yellow',
green: 'Green',
teal: 'Green', // Map teal to green as it's not available in NoteBackgroundColorMap
blue: 'Blue',
purple: 'Purple',
grey: 'White', // Map grey to white as it's the closest available
};
const mappedColor = colorMap[color];
const backgroundValue = mappedColor
? DefaultTheme.NoteBackgroundColorMap[mappedColor]
: null;
ctx.store.updateBlock(model, { background: backgroundValue });
};
return html`
@@ -93,102 +103,12 @@ const backgroundColorAction = {
},
} satisfies ToolbarAction;
const iconPickerAction = {
id: 'icon-picker',
label: 'Icon Picker',
tooltip: 'Change icon',
icon: SmileIcon(),
run() {
// This will be handled by the content function
},
content(ctx) {
const model = ctx.getCurrentModelByType(CalloutBlockModel);
if (!model) return null;
const handleIconPickerClick = (event: MouseEvent) => {
// Get IconPickerService from the framework
const iconPickerService = ctx.std.getOptional(
IconPickerServiceIdentifier
);
if (!iconPickerService) {
console.warn('IconPickerService not found');
return;
}
// Get the uni-component from the service
const iconPickerComponent = iconPickerService.iconPickerComponent;
// Create props for the icon picker
const props = {
onSelect: (iconData?: IconData) => {
// When iconData is undefined (delete icon), set icon to undefined
ctx.store.updateBlock(model, { icon: iconData });
closeHandler(); // Close the picker after selection
},
onClose: () => {
closeHandler();
},
};
// Create IconPickerWrapper instance
const wrapper = new IconPickerWrapper();
wrapper.iconPickerComponent = iconPickerComponent;
wrapper.props = props;
wrapper.style.position = 'absolute';
wrapper.style.backgroundColor = cssVarV2.layer.background.overlayPanel;
wrapper.style.boxShadow = 'var(--affine-menu-shadow)';
wrapper.style.borderRadius = '8px';
// Create popup target from the clicked element
const target = popupTargetFromElement(event.currentTarget as HTMLElement);
// Create popup
const closeHandler = createPopup(target, wrapper, {
onClose: () => {
// Cleanup if needed
},
});
};
return html`
<editor-icon-button
aria-label="icon-picker"
.tooltip=${'Change Icon'}
@click=${handleIconPickerClick}
>
${SmileIcon()} ${EditorChevronDown}
</editor-icon-button>
`;
},
} satisfies ToolbarAction;
const builtinToolbarConfig = {
actions: [
{
id: 'style',
actions: [backgroundColorAction],
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'icon',
actions: [iconPickerAction],
} satisfies ToolbarActionGroup<ToolbarAction>,
{
placement: ActionPlacement.More,
id: 'c.delete',
label: 'Delete',
icon: DeleteIcon(),
variant: 'destructive',
run(ctx) {
const model = ctx.getCurrentModelByType(CalloutBlockModel);
if (!model) return;
ctx.store.deleteBlock(model);
// Clears
ctx.select('note');
ctx.reset();
},
} satisfies ToolbarAction,
],
} as const satisfies ToolbarModuleConfig;
@@ -1,14 +1,11 @@
import { CalloutBlockComponent } from './callout-block';
import { IconPickerWrapper } from './icon-picker-wrapper';
export function effects() {
customElements.define('affine-callout', CalloutBlockComponent);
customElements.define('icon-picker-wrapper', IconPickerWrapper);
}
declare global {
interface HTMLElementTagNameMap {
'affine-callout': CalloutBlockComponent;
'icon-picker-wrapper': IconPickerWrapper;
}
}
@@ -1,52 +0,0 @@
import type { IconData } from '@blocksuite/affine-shared/services';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import { ShadowlessElement } from '@blocksuite/std';
import { type Signal } from '@preact/signals-core';
import { html, type TemplateResult } from 'lit';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
// Copy of renderUniLit from callout-block.ts
const renderUniLit = <Props, Expose extends NonNullable<unknown>>(
uni: UniComponent<Props, Expose> | undefined,
props?: Props,
options?: {
ref?: Signal<Expose | undefined>;
style?: Readonly<StyleInfo>;
class?: string;
}
): TemplateResult => {
return html` <uni-lit
.uni="${uni}"
.props="${props}"
.ref="${options?.ref}"
style=${options?.style ? styleMap(options?.style) : ''}
></uni-lit>`;
};
export interface IconPickerWrapperProps {
onSelect?: (iconData?: IconData) => void;
onClose?: () => void;
}
export class IconPickerWrapper extends ShadowlessElement {
iconPickerComponent?: UniComponent<IconPickerWrapperProps, any>;
props?: IconPickerWrapperProps;
constructor() {
super();
}
override render() {
if (!this.iconPickerComponent) {
return html``;
}
return renderUniLit(this.iconPickerComponent, this.props);
}
}
declare global {
interface HTMLElementTagNameMap {
'icon-picker-wrapper': IconPickerWrapper;
}
}
+1 -1
View File
@@ -48,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -48,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -39,5 +39,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -49,5 +49,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -323,8 +323,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
private readonly _renderEmbedView = () => {
const linkedDoc = this.linkedDoc;
const trash = linkedDoc?.meta?.trash;
const isDeleted = trash || !linkedDoc;
const isDeleted = !linkedDoc;
const isLoading = this._loading;
const isError = this.isError;
const isEmpty = this._isDocEmpty() && this.isBannerEmpty;
@@ -522,6 +521,11 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
);
this._setDocUpdatedAt();
this.disposables.add(
this.store.workspace.slots.docListUpdated.subscribe(() => {
this._setDocUpdatedAt();
})
);
if (this._referenceToNode) {
this._linkedDocMode = this.model.props.params?.mode ?? 'page';
@@ -550,13 +554,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
})
);
this.disposables.add(
this.store.workspace.slots.docListUpdated.subscribe(() => {
this._setDocUpdatedAt();
this.refreshData();
})
);
this._trackCitationDeleteEvent();
}
@@ -357,14 +357,10 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
};
refreshData = () => {
this._load()
.then(() => {
this._isEmptySyncedDoc = isEmptyDoc(this.syncedDoc, this.editorMode);
})
.catch(e => {
console.error(e);
this._error = true;
});
this._load().catch(e => {
console.error(e);
this._error = true;
});
};
title$ = computed(() => {
@@ -449,8 +445,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
this._cycle = false;
const syncedDoc = this.syncedDoc;
const trash = syncedDoc?.meta?.trash;
if (trash || !syncedDoc) {
if (!syncedDoc) {
this._deleted = true;
this._loading = false;
return;
@@ -526,7 +521,6 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
this.disposables.add(
this.store.workspace.slots.docListUpdated.subscribe(() => {
this._setDocUpdatedAt();
this.refreshData();
})
);
+1 -1
View File
@@ -49,5 +49,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -1,5 +1,4 @@
import { updateBlockAlign } from '@blocksuite/affine-block-note';
import { ImageBlockModel, TextAlign } from '@blocksuite/affine-model';
import { ImageBlockModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
blockCommentToolbarButton,
@@ -13,9 +12,6 @@ import {
DeleteIcon,
DownloadIcon,
DuplicateIcon,
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
} from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
@@ -55,55 +51,7 @@ const builtinToolbarConfig = {
},
},
{
id: 'c.1.align-left',
tooltip: 'Align left',
icon: TextAlignLeftIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.chain
.pipe(updateBlockAlign, {
textAlign: TextAlign.Left,
selectedBlocks: [block],
})
.run();
}
},
},
{
id: 'c.2.align-center',
tooltip: 'Align center',
icon: TextAlignCenterIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.chain
.pipe(updateBlockAlign, {
textAlign: TextAlign.Center,
selectedBlocks: [block],
})
.run();
}
},
},
{
id: 'c.3.align-right',
tooltip: 'Align right',
icon: TextAlignRightIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.chain
.pipe(updateBlockAlign, {
textAlign: TextAlign.Right,
selectedBlocks: [block],
})
.run();
}
},
},
{
id: 'd.comment',
id: 'c.comment',
...blockCommentToolbarButton,
},
{
@@ -143,15 +143,6 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
width: '100%',
});
const alignItemsStyleMap = styleMap({
alignItems:
this.model.props.textAlign$.value === 'left'
? 'flex-start'
: this.model.props.textAlign$.value === 'right'
? 'flex-end'
: undefined,
});
const resovledState = this.resourceController.resolveStateWith({
loadingIcon: LoadingIcon({
strokeColor: cssVarV2('button/pureWhiteText'),
@@ -171,7 +162,6 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
html`<affine-page-image
.block=${this}
.state=${resovledState}
style="${alignItemsStyleMap}"
></affine-page-image>`,
() =>
html`<affine-image-fallback-card
+1 -1
View File
@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -150,10 +150,6 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
const listIcon = getListIcon(model, !collapsed, _onClickIcon);
const textAlignStyle = styleMap({
textAlign: this.model.props.textAlign$?.value,
});
const children = html`<div
class="affine-block-children-container"
style=${styleMap({
@@ -165,7 +161,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
</div>`;
return html`
<div class=${'affine-list-block-container'} style="${textAlignStyle}">
<div class=${'affine-list-block-container'}>
<div
class=${classMap({
'affine-list-rich-text-wrapper': true,
+1 -1
View File
@@ -49,5 +49,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -8,4 +8,3 @@ export { indentBlock } from './indent-block.js';
export { indentBlocks } from './indent-blocks.js';
export { selectBlock } from './select-block.js';
export { selectBlocksBetween } from './select-blocks-between.js';
export { updateBlockAlign } from './update-block-align.js';
@@ -1,53 +0,0 @@
import type { TextAlign } from '@blocksuite/affine-model';
import {
getBlockSelectionsCommand,
getImageSelectionsCommand,
getSelectedBlocksCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import {
type BlockComponent,
type Command,
TextSelection,
} from '@blocksuite/std';
type UpdateBlockAlignConfig = {
textAlign: TextAlign;
selectedBlocks?: BlockComponent[];
};
export const updateBlockAlign: Command<UpdateBlockAlignConfig> = (
ctx,
next
) => {
let { std, textAlign, selectedBlocks } = ctx;
if (!selectedBlocks) {
const [result, ctx] = std.command
.chain()
.tryAll(chain => [
chain.pipe(getTextSelectionCommand),
chain.pipe(getBlockSelectionsCommand),
chain.pipe(getImageSelectionsCommand),
])
.pipe(getSelectedBlocksCommand, { types: ['text', 'block', 'image'] })
.run();
if (result) {
selectedBlocks = ctx.selectedBlocks;
}
}
if (!selectedBlocks || selectedBlocks.length === 0) return false;
selectedBlocks.forEach(block => {
std.store.updateBlock(block.model, { textAlign });
});
const selectionManager = std.host.selection;
const textSelection = selectionManager.find(TextSelection);
if (!textSelection) {
return false;
}
selectionManager.setGroup('note', [textSelection]);
return next();
};
@@ -4,15 +4,9 @@ import {
textFormatConfigs,
} from '@blocksuite/affine-inline-preset';
import {
type TextAlignConfig,
textAlignConfigs,
type TextConversionConfig,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import {
getSelectedModelsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
import {
type SlashMenuActionItem,
@@ -23,7 +17,7 @@ import {
import { HeadingsIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { updateBlockAlign, updateBlockType } from '../commands';
import { updateBlockType } from '../commands';
import { tooltips } from './tooltips';
let basicIndex = 0;
@@ -66,10 +60,6 @@ const noteSlashMenuConfig: SlashMenuConfig = {
createConversionItem(config, `1_List@${index++}`)
),
...textAlignConfigs.map((config, index) =>
createAlignItem(config, `2_Align@${index++}`)
),
...textFormatConfigs
.filter(i => !['Code', 'Link'].includes(i.name))
.map((config, index) =>
@@ -99,26 +89,6 @@ function createConversionItem(
};
}
function createAlignItem(
config: TextAlignConfig,
group?: SlashMenuItem['group']
): SlashMenuActionItem {
const { textAlign, name, icon } = config;
return {
name,
group,
icon,
action: ({ std }) => {
std.command
.chain()
.pipe(getTextSelectionCommand)
.pipe(getSelectedModelsCommand, { types: ['text'] })
.pipe(updateBlockAlign, { textAlign })
.run();
},
};
}
function createTextFormatItem(
config: TextFormatConfig,
group?: SlashMenuItem['group']
@@ -5,10 +5,7 @@ import {
NoteBlockSchema,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import {
textAlignConfigs,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
import {
focusBlockEnd,
focusBlockStart,
@@ -39,7 +36,6 @@ import {
indentBlocks,
selectBlock,
selectBlocksBetween,
updateBlockAlign,
updateBlockType,
} from './commands';
import { moveBlockConfigs } from './move-block';
@@ -161,36 +157,6 @@ class NoteKeymap {
);
};
private readonly _bindTextAlignHotKey = () => {
return textAlignConfigs.reduce(
(acc, item) => {
const keymap = item.hotkey!.reduce(
(acc, key) => {
return {
...acc,
[key]: ctx => {
ctx.get('defaultState').event.preventDefault();
const [result] = this._std.command
.chain()
.pipe(updateBlockAlign, { textAlign: item.textAlign })
.run();
return result;
},
};
},
{} as Record<string, UIEventHandler>
);
return {
...acc,
...keymap,
};
},
{} as Record<string, UIEventHandler>
);
};
private _focusBlock: BlockComponent | null = null;
private readonly _getClosestNoteByBlockId = (blockId: string) => {
@@ -602,7 +568,6 @@ class NoteKeymap {
...this._bindMoveBlockHotKey(),
...this._bindQuickActionHotKey(),
...this._bindTextConversionHotKey(),
...this._bindTextAlignHotKey(),
Tab: ctx => {
const [success] = this.std.command.exec(indentBlocks);
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -264,10 +264,6 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
`;
}
const textAlignStyle = styleMap({
textAlign: this.model.props.textAlign$?.value,
});
const children = html`<div
class="affine-block-children-container"
style=${styleMap({
@@ -292,7 +288,6 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
'affine-paragraph-block-container': true,
'highlight-comment': this.isCommentHighlighted,
})}
style="${textAlignStyle}"
data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
>
<div
+1 -1
View File
@@ -67,5 +67,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -8,10 +8,7 @@ import {
notifyDocCreated,
promptDocTitle,
} from '@blocksuite/affine-block-embed';
import {
updateBlockAlign,
updateBlockType,
} from '@blocksuite/affine-block-note';
import { updateBlockType } from '@blocksuite/affine-block-note';
import type { HighlightType } from '@blocksuite/affine-components/highlight-dropdown-menu';
import { toast } from '@blocksuite/affine-components/toast';
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
@@ -26,12 +23,8 @@ import {
import {
EmbedLinkedDocBlockSchema,
EmbedSyncedDocBlockSchema,
type TextAlign,
} from '@blocksuite/affine-model';
import {
textAlignConfigs,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
import {
copySelectedModelsCommand,
deleteSelectedModelsCommand,
@@ -53,7 +46,6 @@ import {
ActionPlacement,
blockCommentToolbarButton,
} from '@blocksuite/affine-shared/services';
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
import {
CopyIcon,
@@ -138,64 +130,6 @@ const conversionsActionGroup = {
},
} as const satisfies ToolbarActionGenerator;
const alignActionGroup = {
id: 'b.align',
when: ({ chain }) => isFormatSupported(chain).run()[0],
generate({ chain }) {
const [ok, { selectedModels = [] }] = chain
.tryAll(chain => [
chain.pipe(getTextSelectionCommand),
chain.pipe(getBlockSelectionsCommand),
])
.pipe(getSelectedModelsCommand, { types: ['text', 'block'] })
.run();
if (!ok) return null;
const alignment =
textAlignConfigs.find(
({ textAlign }) =>
textAlign ===
getMostCommonValue(
selectedModels.map(
({ props }) => props as { textAlign?: TextAlign }
),
'textAlign'
)
) ?? textAlignConfigs[0];
const update = (textAlign: TextAlign) => {
chain.pipe(updateBlockAlign, { textAlign }).run();
};
return {
content: html`
<editor-menu-button
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button aria-label="Align" .tooltip="${'Align'}">
${alignment.icon} ${EditorChevronDown}
</editor-icon-button>
`}
>
<div data-size="large" data-orientation="vertical">
${repeat(
textAlignConfigs,
item => item.name,
({ textAlign, name, icon }) => html`
<editor-menu-action
aria-label=${name}
@click=${() => update(textAlign)}
>
${icon}<span class="label">${name}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`,
};
},
} as const satisfies ToolbarActionGenerator;
const inlineTextActionGroup = {
id: 'b.inline-text',
when: ({ chain }) => isFormatSupported(chain).run()[0],
@@ -357,7 +291,6 @@ const turnIntoLinkedDoc = {
export const builtinToolbarConfig = {
actions: [
conversionsActionGroup,
alignActionGroup,
inlineTextActionGroup,
highlightActionGroup,
turnIntoDatabase,
@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -144,16 +144,6 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
style=${styleMap({
paddingLeft: `${virtualPadding}px`,
paddingRight: `${virtualPadding}px`,
marginLeft:
!this.model.props.textAlign$.value ||
this.model.props.textAlign$?.value === 'left'
? undefined
: 'auto',
marginRight:
!this.model.props.textAlign$.value ||
this.model.props.textAlign$?.value === 'right'
? undefined
: 'auto',
width: 'max-content',
})}
>
+1 -1
View File
@@ -82,5 +82,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -26,5 +26,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -35,5 +35,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -40,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -19,16 +19,16 @@ const DOC_BLOCK_CHILD_PADDING = 24;
export class DocTitle extends WithDisposable(ShadowlessElement) {
static override styles = css`
.doc-title-container {
font-size: 40px;
line-height: 50px;
font-weight: 700;
}
.doc-icon-container,
.doc-title-container {
box-sizing: border-box;
font-family: var(--affine-font-family);
font-size: var(--affine-font-base);
line-height: var(--affine-line-height);
color: var(--affine-text-primary-color);
font-size: 40px;
line-height: 50px;
font-weight: 700;
outline: none;
resize: none;
border: 0;
@@ -47,10 +47,6 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
${DOC_BLOCK_CHILD_PADDING}px
);
}
.doc-icon-container + * .doc-title-container {
/* when doc icon exists, remove the top padding */
padding-top: 0;
}
/* Extra small devices (phones, 640px and down) */
@container viewport (width <= 640px) {
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -51,5 +51,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -25,5 +25,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.21.0"
}
@@ -47,5 +47,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -50,5 +50,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -56,5 +56,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
+1 -1
View File
@@ -30,5 +30,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -6,20 +6,22 @@ import {
type Text,
} from '@blocksuite/store';
import type { Color } from '../../themes/index.js';
import { DefaultTheme } from '../../themes/index.js';
import type { BlockMeta } from '../../utils/types';
export type CalloutProps = {
icon?: IconData;
text: Text;
backgroundColorName?: string;
background: Color;
} & BlockMeta;
export const CalloutBlockSchema = defineBlockSchema({
flavour: 'affine:callout',
props: (internal): CalloutProps => ({
icon: { type: 'emoji', unicode: '💡' } as IconData,
icon: undefined,
text: internal.Text(),
backgroundColorName: 'grey',
background: DefaultTheme.NoteBackgroundColorMap.White,
'meta:createdAt': undefined,
'meta:updatedAt': undefined,
'meta:createdBy': undefined,
@@ -9,7 +9,6 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types.js';
import { ImageBlockTransformer } from './image-transformer.js';
@@ -21,7 +20,6 @@ export type ImageBlockProps = {
rotate: number;
size?: number;
comments?: Record<string, boolean>;
textAlign?: TextAlign;
} & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta;
@@ -36,7 +34,6 @@ const defaultImageProps: ImageBlockProps = {
rotate: 0,
size: -1,
comments: undefined,
textAlign: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,
@@ -5,7 +5,6 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
// `toggle` type has been deprecated, do not use it
@@ -14,7 +13,6 @@ export type ListType = 'bulleted' | 'numbered' | 'todo' | 'toggle';
export type ListProps = {
type: ListType;
text: Text;
textAlign?: TextAlign;
checked: boolean;
collapsed: boolean;
order: number | null;
@@ -27,7 +25,6 @@ export const ListBlockSchema = defineBlockSchema({
({
type: 'bulleted',
text: internal.Text(),
textAlign: undefined,
checked: false,
collapsed: false,
@@ -5,7 +5,6 @@ import {
type Text,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
export type ParagraphType =
@@ -20,7 +19,6 @@ export type ParagraphType =
export type ParagraphProps = {
type: ParagraphType;
textAlign?: TextAlign;
text: Text;
collapsed: boolean;
comments?: Record<string, boolean>;
@@ -31,7 +29,6 @@ export const ParagraphBlockSchema = defineBlockSchema({
props: (internal): ParagraphProps => ({
type: 'text',
text: internal.Text(),
textAlign: undefined,
collapsed: false,
comments: undefined,
'meta:createdAt': undefined,
@@ -5,7 +5,6 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
export type TableCell = {
@@ -31,7 +30,6 @@ export interface TableBlockProps extends BlockMeta {
// key = `${rowId}:${columnId}`
cells: Record<string, TableCell>;
comments?: Record<string, boolean>;
textAlign?: TextAlign;
}
export interface TableCellSerialized {
@@ -55,7 +53,6 @@ export const TableBlockSchema = defineBlockSchema({
columns: {},
cells: {},
comments: undefined,
textAlign: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,
+1 -1
View File
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
-35
View File
@@ -1,35 +0,0 @@
import { TextAlign } from '@blocksuite/affine-model';
import {
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
} from '@blocksuite/icons/lit';
import type { TemplateResult } from 'lit';
export interface TextAlignConfig {
textAlign: TextAlign;
name: string;
hotkey: string[] | null;
icon: TemplateResult<1>;
}
export const textAlignConfigs: TextAlignConfig[] = [
{
textAlign: TextAlign.Left,
name: 'Align left',
hotkey: [`Mod-Shift-L`],
icon: TextAlignLeftIcon(),
},
{
textAlign: TextAlign.Center,
name: 'Align center',
hotkey: [`Mod-Shift-E`],
icon: TextAlignCenterIcon(),
},
{
textAlign: TextAlign.Right,
name: 'Align right',
hotkey: [`Mod-Shift-R`],
icon: TextAlignRightIcon(),
},
];
-1
View File
@@ -1,4 +1,3 @@
export { type TextAlignConfig, textAlignConfigs } from './align';
export { type TextConversionConfig, textConversionConfigs } from './conversion';
export {
asyncGetRichText,
+1 -1
View File
@@ -75,5 +75,5 @@
"devDependencies": {
"vitest": "3.1.3"
},
"version": "0.25.2"
"version": "0.22.4"
}
@@ -1,5 +1,6 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import { createIdentifier } from '@blocksuite/global/di';
import type { TemplateResult } from 'lit';
export enum IconType {
Emoji = 'emoji',
AffineIcon = 'affine-icon',
@@ -21,8 +22,15 @@ export type IconData =
blob: Blob;
};
export interface IconPickerOptions {
onSelect?: (icon: IconData) => void;
onClose?: () => void;
currentIcon?: IconData;
}
export interface IconPickerService {
iconPickerComponent: UniComponent<{ onSelect?: (data?: IconData) => void }>;
renderIconPicker(options: IconPickerOptions): TemplateResult;
}
export const IconPickerServiceIdentifier =
@@ -45,5 +45,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -34,5 +34,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -36,5 +36,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -40,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -38,5 +38,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -36,5 +36,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -34,5 +34,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -55,5 +55,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}
@@ -134,7 +134,7 @@ export class ImportDoc extends WithDisposable(LitElement) {
);
return;
}
this._onImportSuccess(entryId ? [entryId] : [], {
this._onImportSuccess([entryId], {
isWorkspaceFile,
importedCount: pageIds.length,
});
@@ -76,16 +76,10 @@ export const linkedDocPopoverStyles = css`
border-top: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
}
.group icon-button svg,
.group icon-button .icon {
.group icon-button svg {
width: 20px;
height: 20px;
}
.group icon-button .icon {
display: flex;
align-items: center;
justify-content: center;
}
.linked-doc-popover .group {
display: flex;
@@ -21,28 +21,6 @@ type ImportNotionZipOptions = {
extensions: ExtensionType[];
};
type PageIcon = {
type: 'emoji' | 'image';
content: string; // emoji unicode or image URL/data
};
type FolderHierarchy = {
name: string;
path: string;
children: Map<string, FolderHierarchy>;
pageId?: string;
parentPath?: string;
icon?: PageIcon;
};
type ImportNotionZipResult = {
entryId: string | undefined;
pageIds: string[];
isWorkspaceFile: boolean;
hasMarkdown: boolean;
folderHierarchy?: FolderHierarchy;
};
function getProvider(extensions: ExtensionType[]) {
const container = new Container();
extensions.forEach(ext => {
@@ -51,197 +29,6 @@ function getProvider(extensions: ExtensionType[]) {
return container.provider();
}
function parseFolderPath(filePath: string): {
folderParts: string[];
fileName: string;
} {
const parts = filePath.split('/');
const fileName = parts.pop() || '';
return { folderParts: parts.filter(part => part.length > 0), fileName };
}
function extractPageIcon(doc: Document): PageIcon | undefined {
// Look for Notion page icon in the HTML
// Notion export format: <div class="page-header-icon undefined"><span class="icon">✅</span></div>
console.log('=== Extracting page icon ===');
// Check if there's a head section with title for debugging
const headTitle = doc.querySelector('head title');
if (headTitle) {
console.log('Page title from head:', headTitle.textContent);
}
// Look for the exact Notion export structure: .page-header-icon .icon
const notionIconSpan = doc.querySelector('.page-header-icon .icon');
if (notionIconSpan && notionIconSpan.textContent) {
const iconContent = notionIconSpan.textContent.trim();
console.log('Found Notion icon (.page-header-icon .icon):', iconContent);
if (/\p{Emoji}/u.test(iconContent)) {
return {
type: 'emoji',
content: iconContent,
};
}
}
// Look for page header area for debugging
const pageHeader = doc.querySelector('.page-header-icon');
if (pageHeader) {
console.log(
'Found .page-header-icon:',
pageHeader.outerHTML.substring(0, 300) + '...'
);
}
// Fallback: try to find emoji icons with older selectors
const emojiIcon = doc.querySelector('.page-header-icon .notion-emoji');
if (emojiIcon && emojiIcon.textContent) {
console.log(
'Found emoji icon (.page-header-icon .notion-emoji):',
emojiIcon.textContent
);
return {
type: 'emoji',
content: emojiIcon.textContent.trim(),
};
}
// Try alternative emoji selectors
const altEmojiIcon = doc.querySelector('[role="img"][aria-label]');
if (
altEmojiIcon &&
altEmojiIcon.textContent &&
/\p{Emoji}/u.test(altEmojiIcon.textContent)
) {
console.log(
'Found emoji icon ([role="img"][aria-label]):',
altEmojiIcon.textContent
);
return {
type: 'emoji',
content: altEmojiIcon.textContent.trim(),
};
}
// Look for image icons in the page header
const imageIcon = doc.querySelector('.page-header-icon img');
if (imageIcon) {
const src = imageIcon.getAttribute('src');
console.log('Found image icon (.page-header-icon img):', src);
if (src) {
return {
type: 'image',
content: src,
};
}
}
// Fallback: Look for any span with emoji class "icon" in page header area
const iconSpans = doc.querySelectorAll('span.icon');
for (const span of iconSpans) {
if (span.textContent && /\p{Emoji}/u.test(span.textContent.trim())) {
const parent = span.parentElement;
console.log(
'Found emoji in span.icon:',
span.textContent,
'parent classes:',
parent?.className
);
// Check if this is in a page header context
if (
parent &&
(parent.classList.contains('page-header-icon') ||
parent.closest('.page-header-icon'))
) {
console.log(
'Using emoji from span.icon in page header:',
span.textContent
);
return {
type: 'emoji',
content: span.textContent.trim(),
};
}
}
}
// Fallback: Try to find icons in the page title area that might contain emoji
const pageTitle = doc.querySelector('.page-title, h1');
if (pageTitle && pageTitle.textContent) {
console.log('Page title element found:', pageTitle.textContent);
const text = pageTitle.textContent.trim();
// Check if the title starts with an emoji
const emojiMatch = text.match(/^(\p{Emoji}+)/u);
if (emojiMatch) {
console.log('Found emoji in title:', emojiMatch[1]);
return {
type: 'emoji',
content: emojiMatch[1],
};
}
}
console.log('No page icon found');
return undefined;
}
function buildFolderHierarchy(
pagePaths: Array<{ path: string; pageId: string; icon?: PageIcon }>
): FolderHierarchy {
const root: FolderHierarchy = {
name: '',
path: '',
children: new Map(),
};
for (const { path, pageId, icon } of pagePaths) {
const { folderParts, fileName } = parseFolderPath(path);
let current = root;
let currentPath = '';
// Navigate/create folder structure
for (const folderName of folderParts) {
const parentPath = currentPath;
currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
if (!current.children.has(folderName)) {
current.children.set(folderName, {
name: folderName,
path: currentPath,
parentPath: parentPath || undefined,
children: new Map(),
});
}
current = current.children.get(folderName)!;
}
// If this is a page file, associate it with the current folder
if (fileName.endsWith('.html') && !fileName.startsWith('index.html')) {
const pageName = fileName.replace(/\.html$/, '');
if (!current.children.has(pageName)) {
current.children.set(pageName, {
name: pageName,
path: path,
parentPath: current.path || undefined,
children: new Map(),
pageId: pageId,
icon: icon,
});
} else {
// Update existing entry with pageId and icon
const existingPage = current.children.get(pageName)!;
existingPage.pageId = pageId;
if (icon) {
existingPage.icon = icon;
}
}
}
}
return root;
}
/**
* Imports a Notion zip file into the BlockSuite collection.
*
@@ -255,24 +42,18 @@ function buildFolderHierarchy(
* - pageIds: An array of imported page IDs.
* - isWorkspaceFile: Whether the imported file is a workspace file.
* - hasMarkdown: Whether the zip contains markdown files.
* - folderHierarchy: The parsed folder hierarchy from the Notion export.
*/
async function importNotionZip({
collection,
schema,
imported,
extensions,
}: ImportNotionZipOptions): Promise<ImportNotionZipResult> {
}: ImportNotionZipOptions) {
const provider = getProvider(extensions);
const pageIds: string[] = [];
let isWorkspaceFile = false;
let hasMarkdown = false;
let entryId: string | undefined;
const pagePathsWithIds: Array<{
path: string;
pageId: string;
icon?: PageIcon;
}> = [];
const parseZipFile = async (path: File | Blob) => {
const unzip = new Unzip();
await unzip.load(path);
@@ -299,8 +80,6 @@ async function importNotionZip({
isWorkspaceFile = true;
continue;
}
let pageIcon: PageIcon | undefined;
if (lastSplitIndex !== -1) {
const text = await content.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
@@ -309,10 +88,7 @@ async function importNotionZip({
// Skip empty pages
continue;
}
// Extract page icon from the HTML
pageIcon = extractPageIcon(doc);
}
const id = collection.idGenerator();
const splitPath = path.split('/');
while (splitPath.length > 0) {
@@ -320,7 +96,6 @@ async function importNotionZip({
splitPath.shift();
}
pagePaths.push(path);
pagePathsWithIds.push({ path, pageId: id, icon: pageIcon });
if (entryId === undefined && lastSplitIndex === -1) {
entryId = id;
}
@@ -391,14 +166,7 @@ async function importNotionZip({
const allPromises = await parseZipFile(imported);
await Promise.all(allPromises.flat());
entryId = entryId ?? pageIds[0];
// Build folder hierarchy from collected paths
const folderHierarchy =
pagePathsWithIds.length > 0
? buildFolderHierarchy(pagePathsWithIds)
: undefined;
return { entryId, pageIds, isWorkspaceFile, hasMarkdown, folderHierarchy };
return { entryId, pageIds, isWorkspaceFile, hasMarkdown };
}
export const NotionHtmlTransformer = {
@@ -37,5 +37,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.2"
"version": "0.22.4"
}

Some files were not shown because too many files have changed in this diff Show More