diff --git a/apps/core/src/bootstrap/plugins/setup.ts b/apps/core/src/bootstrap/plugins/setup.ts
index dd49e41a75..6da43b396c 100644
--- a/apps/core/src/bootstrap/plugins/setup.ts
+++ b/apps/core/src/bootstrap/plugins/setup.ts
@@ -81,6 +81,9 @@ export const createGlobalThis = () => {
TextDecoder: globalThis.TextDecoder,
Request: globalThis.Request,
Error: globalThis.Error,
+ // bookmark uses these
+ Blob: globalThis.Blob,
+ ClipboardItem: globalThis.ClipboardItem,
// fixme: use our own db api
indexedDB: globalThis.indexedDB,
diff --git a/apps/core/src/bootstrap/register-plugins.ts b/apps/core/src/bootstrap/register-plugins.ts
index 31713e56b1..d40970dc88 100644
--- a/apps/core/src/bootstrap/register-plugins.ts
+++ b/apps/core/src/bootstrap/register-plugins.ts
@@ -37,6 +37,7 @@ const builtinPluginUrl = new Set([
'/plugins/bookmark',
'/plugins/copilot',
'/plugins/hello-world',
+ '/plugins/image-preview',
]);
const logger = new DebugLogger('register-plugins');
diff --git a/apps/storybook/src/stories/image-preview-modal.stories.tsx b/apps/storybook/src/stories/image-preview-modal.stories.tsx
index 16835219d0..5bf5c0f936 100644
--- a/apps/storybook/src/stories/image-preview-modal.stories.tsx
+++ b/apps/storybook/src/stories/image-preview-modal.stories.tsx
@@ -1,11 +1,13 @@
import { BlockHubWrapper } from '@affine/component/block-hub';
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
-import { ImagePreviewModal } from '@affine/component/image-preview-modal';
import { initEmptyPage } from '@affine/env/blocksuite';
import { WorkspaceFlavour } from '@affine/env/workspace';
+// eslint-disable-next-line @typescript-eslint/no-restricted-imports
+import { ImagePreviewModal } from '@affine/image-preview-plugin/src/component';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { Meta } from '@storybook/react';
+import { createPortal } from 'react-dom';
export default {
title: 'Component/ImagePreviewModal',
@@ -52,6 +54,10 @@ export const Default = () => {
}}
>
+ {createPortal(
+ ,
+ document.body
+ )}
= await readFile(
)
.then(text => JSON.parse(text))
.then(async json => {
- const { success } = await packageJsonInputSchema.safeParseAsync(json);
- if (success) {
+ const result = await packageJsonInputSchema.safeParseAsync(json);
+ if (result.success) {
return json;
} else {
- throw new Error('invalid package.json');
+ throw new Error('invalid package.json', result.error);
}
});
diff --git a/packages/component/src/components/block-suite-editor/index.tsx b/packages/component/src/components/block-suite-editor/index.tsx
index 0d4cf2352e..c097378661 100644
--- a/packages/component/src/components/block-suite-editor/index.tsx
+++ b/packages/component/src/components/block-suite-editor/index.tsx
@@ -8,8 +8,7 @@ import { Skeleton } from '@mui/material';
import { use } from 'foxact/use';
import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactElement } from 'react';
-import { lazy, memo, Suspense, useCallback, useEffect, useRef } from 'react';
-import { createPortal } from 'react-dom';
+import { memo, Suspense, useCallback, useEffect, useRef } from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
@@ -41,12 +40,6 @@ declare global {
var currentEditor: EditorContainer | undefined;
}
-const ImagePreviewModal = lazy(() =>
- import('../image-preview-modal').then(module => ({
- default: module.ImagePreviewModal,
- }))
-);
-
const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
const { onLoad, page, mode, style } = props;
if (!page.loaded) {
@@ -191,17 +184,6 @@ export const BlockSuiteEditor = memo(function BlockSuiteEditor(
}>
- {props.page && (
-
- {createPortal(
- ,
- document.body
- )}
-
- )}
);
});
diff --git a/plugins/image-preview/package.json b/plugins/image-preview/package.json
new file mode 100644
index 0000000000..fb4c091fe7
--- /dev/null
+++ b/plugins/image-preview/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@affine/image-preview-plugin",
+ "version": "0.8.0-canary.0",
+ "description": "Image preview plugin",
+ "affinePlugin": {
+ "release": true,
+ "entry": {
+ "core": "./src/index.ts"
+ }
+ },
+ "dependencies": {
+ "@affine/component": "workspace:*",
+ "@blocksuite/icons": "^2.1.27",
+ "@toeverything/plugin-infra": "workspace:*",
+ "@toeverything/theme": "^0.7.9",
+ "clsx": "^2.0.0",
+ "react-error-boundary": "^4.0.10",
+ "swr": "2.1.5"
+ }
+}
diff --git a/plugins/image-preview/src/app.tsx b/plugins/image-preview/src/app.tsx
new file mode 100644
index 0000000000..e2fa1d9949
--- /dev/null
+++ b/plugins/image-preview/src/app.tsx
@@ -0,0 +1,11 @@
+import type { Page } from '@blocksuite/store';
+
+import { ImagePreviewModal } from './component';
+
+export type AppProps = {
+ page: Page;
+};
+
+export const App = ({ page }: AppProps) => {
+ return ;
+};
diff --git a/packages/component/src/components/image-preview-modal/hooks/use-zoom.tsx b/plugins/image-preview/src/component/hooks/use-zoom.tsx
similarity index 100%
rename from packages/component/src/components/image-preview-modal/hooks/use-zoom.tsx
rename to plugins/image-preview/src/component/hooks/use-zoom.tsx
diff --git a/packages/component/src/components/image-preview-modal/index.css.ts b/plugins/image-preview/src/component/index.css.ts
similarity index 100%
rename from packages/component/src/components/image-preview-modal/index.css.ts
rename to plugins/image-preview/src/component/index.css.ts
diff --git a/packages/component/src/components/image-preview-modal/index.jotai.ts b/plugins/image-preview/src/component/index.jotai.ts
similarity index 86%
rename from packages/component/src/components/image-preview-modal/index.jotai.ts
rename to plugins/image-preview/src/component/index.jotai.ts
index 353d3dc5c1..67aa51d95d 100644
--- a/packages/component/src/components/image-preview-modal/index.jotai.ts
+++ b/plugins/image-preview/src/component/index.jotai.ts
@@ -5,8 +5,8 @@ export const hasAnimationPlayedAtom = atom(true);
previewBlockIdAtom.onMount = set => {
const callback = (event: MouseEvent) => {
- const target = event.target;
- if (target instanceof HTMLImageElement) {
+ const target = event.target as HTMLElement | null;
+ if (target?.tagName === 'IMG') {
const imageBlock = target.closest('affine-image');
if (imageBlock) {
const blockId = imageBlock.getAttribute('data-block-id');
diff --git a/packages/component/src/components/image-preview-modal/index.tsx b/plugins/image-preview/src/component/index.tsx
similarity index 99%
rename from packages/component/src/components/image-preview-modal/index.tsx
rename to plugins/image-preview/src/component/index.tsx
index 62892cc3f5..6ae7fb4656 100644
--- a/packages/component/src/components/image-preview-modal/index.tsx
+++ b/plugins/image-preview/src/component/index.tsx
@@ -1,5 +1,4 @@
-import '@blocksuite/blocks';
-
+import { Button, IconButton, Tooltip } from '@affine/component';
import type { ImageBlockModel } from '@blocksuite/blocks';
import { assertExists } from '@blocksuite/global/utils';
import {
@@ -23,8 +22,6 @@ import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
import useSWR from 'swr';
-import { Button, IconButton } from '../../ui/button';
-import { Tooltip } from '../../ui/tooltip';
import { useZoomControls } from './hooks/use-zoom';
import {
buttonStyle,
diff --git a/packages/component/src/components/image-preview-modal/toast.ts b/plugins/image-preview/src/component/toast.ts
similarity index 100%
rename from packages/component/src/components/image-preview-modal/toast.ts
rename to plugins/image-preview/src/component/toast.ts
diff --git a/plugins/image-preview/src/index.ts b/plugins/image-preview/src/index.ts
new file mode 100644
index 0000000000..5a95f4b96a
--- /dev/null
+++ b/plugins/image-preview/src/index.ts
@@ -0,0 +1,18 @@
+import type { PluginContext } from '@toeverything/plugin-infra/entry';
+import { createElement } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import { App } from './app';
+
+export const entry = (context: PluginContext) => {
+ context.register('editor', (div, editor) => {
+ const root = createRoot(div);
+ root.render(createElement(App, { page: editor.page }));
+ return () => {
+ root.unmount();
+ };
+ });
+ return () => {
+ // do nothing
+ };
+};
diff --git a/plugins/image-preview/tsconfig.json b/plugins/image-preview/tsconfig.json
new file mode 100644
index 0000000000..f2db0bc490
--- /dev/null
+++ b/plugins/image-preview/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": ["./src"],
+ "compilerOptions": {
+ "noEmit": false,
+ "outDir": "lib",
+ "jsx": "preserve"
+ },
+ "references": [
+ {
+ "path": "../../packages/plugin-infra"
+ },
+ {
+ "path": "../../packages/component"
+ }
+ ]
+}
diff --git a/scripts/build-plugins.mjs b/scripts/build-plugins.mjs
index 9d0bc0dcf8..9549be72a7 100644
--- a/scripts/build-plugins.mjs
+++ b/scripts/build-plugins.mjs
@@ -1,16 +1,15 @@
-import { spawnSync } from 'node:child_process';
+import { spawn } from 'node:child_process';
-spawnSync('yarn', ['-T', 'run', 'dev-plugin', '--plugin', 'bookmark'], {
- stdio: 'inherit',
- shell: true,
-});
+const builtInPlugins = ['bookmark', 'hello-world', 'copilot', 'image-preview'];
-spawnSync('yarn', ['-T', 'run', 'dev-plugin', '--plugin', 'hello-world'], {
- stdio: 'inherit',
- shell: true,
-});
-
-spawnSync('yarn', ['-T', 'run', 'dev-plugin', '--plugin', 'copilot'], {
- stdio: 'inherit',
- shell: true,
-});
+for (const plugin of builtInPlugins) {
+ const cp = spawn('yarn', ['-T', 'run', 'dev-plugin', '--plugin', plugin], {
+ stdio: 'inherit',
+ shell: true,
+ });
+ cp.on('exit', code => {
+ if (code !== 0) {
+ process.exit(code);
+ }
+ });
+}
diff --git a/tests/affine-plugin/e2e/basic.spec.ts b/tests/affine-plugin/e2e/basic.spec.ts
index 98354a57f0..d299b50d9c 100644
--- a/tests/affine-plugin/e2e/basic.spec.ts
+++ b/tests/affine-plugin/e2e/basic.spec.ts
@@ -7,7 +7,7 @@ test('plugin should exist', async ({ page }) => {
await openHomePage(page);
await waitEditorLoad(page);
await page.route('**/plugins/**/package.json', route => route.fetch(), {
- times: 3,
+ times: 4,
});
await page.waitForTimeout(50);
const packageJson = await page.evaluate(() => {
@@ -17,14 +17,26 @@ test('plugin should exist', async ({ page }) => {
expect(packageJson).toEqual([
{
name: '@affine/bookmark-plugin',
+ version: expect.any(String),
+ description: expect.any(String),
affinePlugin: expect.anything(),
},
{
name: '@affine/copilot-plugin',
+ version: expect.any(String),
+ description: expect.any(String),
affinePlugin: expect.anything(),
},
{
name: '@affine/hello-world-plugin',
+ version: expect.any(String),
+ description: expect.any(String),
+ affinePlugin: expect.anything(),
+ },
+ {
+ name: '@affine/image-preview-plugin',
+ version: expect.any(String),
+ description: expect.any(String),
affinePlugin: expect.anything(),
},
]);
diff --git a/yarn.lock b/yarn.lock
index dd78429798..47259dc262 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -403,6 +403,20 @@ __metadata:
languageName: unknown
linkType: soft
+"@affine/image-preview-plugin@workspace:plugins/image-preview":
+ version: 0.0.0-use.local
+ resolution: "@affine/image-preview-plugin@workspace:plugins/image-preview"
+ dependencies:
+ "@affine/component": "workspace:*"
+ "@blocksuite/icons": ^2.1.27
+ "@toeverything/plugin-infra": "workspace:*"
+ "@toeverything/theme": ^0.7.9
+ clsx: ^2.0.0
+ react-error-boundary: ^4.0.10
+ swr: 2.1.5
+ languageName: unknown
+ linkType: soft
+
"@affine/jotai@workspace:*, @affine/jotai@workspace:packages/jotai":
version: 0.0.0-use.local
resolution: "@affine/jotai@workspace:packages/jotai"