diff --git a/.changeset/pre.json b/.changeset/pre.json
index c3d74ed0..11189d01 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -21,7 +21,7 @@
"@difizen/libro-shared-model": "0.0.1",
"@difizen/libro-toc": "0.0.1",
"@difizen/libro-widget": "0.0.1",
- "@difizen/mana-docs": "0.0.1"
+ "@difizen/libro-docs": "0.0.1"
},
"changesets": ["shaggy-lemons-prove"]
}
diff --git a/README.md b/README.md
index c4cda30e..f8fc64b3 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,15 @@
# libro
-[![Code: CI](https://github.com/difizen/libro/actions/workflows/ci.yml/badge.svg)](https://github.com/difizen/libro/actions/workflows/ci.yml)
-[![codecov](https://codecov.io/gh/difizen/libro/graph/badge.svg?token=8LWLNZK78Z)](https://codecov.io/gh/difizen/libro)
+
+
![Editor](https://mdn.alipayobjects.com/huamei_hdnzbp/afts/img/A*cngiQYmKficAAAAAAAAAAAAADjOxAQ/original)
+
+
notebook 产品前端解决方案。
+
+
+[![Code: CI](https://github.com/difizen/libro/actions/workflows/ci.yml/badge.svg)](https://github.com/difizen/libro/actions/workflows/ci.yml)
+[![codecov](https://codecov.io/gh/difizen/libro/graph/badge.svg?token=8LWLNZK78Z)](https://codecov.io/gh/difizen/libro)
- 优雅的交互和丰富的功能
- 方便扩展和二次开发
diff --git a/apps/docs/CHANGELOG.md b/apps/docs/CHANGELOG.md
index 8f55a852..64d57915 100644
--- a/apps/docs/CHANGELOG.md
+++ b/apps/docs/CHANGELOG.md
@@ -1,4 +1,4 @@
-# @difizen/mana-docs
+# @difizen/libro-docs
## 0.0.2-alpha.0
diff --git a/apps/docs/docs/examples/index.md b/apps/docs/docs/examples/index.md
index 15851874..b9d2c19a 100644
--- a/apps/docs/docs/examples/index.md
+++ b/apps/docs/docs/examples/index.md
@@ -3,6 +3,6 @@ title: 输出示例
order: 0
---
-# Libro 输出示例
+# libro 输出示例
diff --git a/apps/docs/docs/examples/lab.md b/apps/docs/docs/examples/lab.md
new file mode 100644
index 00000000..d83e51f6
--- /dev/null
+++ b/apps/docs/docs/examples/lab.md
@@ -0,0 +1,10 @@
+---
+title: Lab
+order: 1
+---
+
+# libro lab
+
+本地在某个文件目录下,起一个`jupyterlab`服务,该文件目录中包含以 `.ipynb` 为后缀名的文件。
+
+
diff --git a/apps/docs/docs/examples/workbench.md b/apps/docs/docs/examples/workbench.md
index b846a0e0..34285c1c 100644
--- a/apps/docs/docs/examples/workbench.md
+++ b/apps/docs/docs/examples/workbench.md
@@ -3,7 +3,7 @@ title: 工作台
order: 1
---
-# Libro 工作台
+# libro 工作台
本地在某个文件目录下,起一个`jupyterlab`服务,该文件目录中包含以 `.ipynb` 为后缀名的文件。
diff --git a/apps/docs/package.json b/apps/docs/package.json
index b8236a32..c396a2d0 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -1,5 +1,5 @@
{
- "name": "@difizen/mana-docs",
+ "name": "@difizen/libro-docs",
"version": "0.0.2-alpha.0",
"private": true,
"license": "MIT",
@@ -22,6 +22,7 @@
"@difizen/mana-app": "latest",
"@difizen/mana-react": "latest",
"@difizen/libro-jupyter": "^0.0.2-alpha.0",
+ "@difizen/libro-lab": "^0.0.2-alpha.0",
"@difizen/libro-core": "^0.0.2-alpha.0",
"@ant-design/icons": "^5.1.0",
"react": "^18.2.0",
diff --git a/apps/docs/src/lab/app.ts b/apps/docs/src/lab/app.ts
new file mode 100644
index 00000000..aaee60d1
--- /dev/null
+++ b/apps/docs/src/lab/app.ts
@@ -0,0 +1,22 @@
+import { ServerConnection, ServerManager } from '@difizen/libro-jupyter';
+import { ConfigurationService } from '@difizen/mana-app';
+import { SlotViewManager } from '@difizen/mana-app';
+import { ApplicationContribution, ViewManager } from '@difizen/mana-app';
+import { inject, singleton } from '@difizen/mana-app';
+
+@singleton({ contrib: ApplicationContribution })
+export class LibroApp implements ApplicationContribution {
+ @inject(ServerConnection) serverConnection: ServerConnection;
+ @inject(ServerManager) serverManager: ServerManager;
+ @inject(ViewManager) viewManager: ViewManager;
+ @inject(SlotViewManager) slotViewManager: SlotViewManager;
+ @inject(ConfigurationService) configurationService: ConfigurationService;
+
+ async onStart() {
+ this.serverConnection.updateSettings({
+ baseUrl: 'http://localhost:8888/',
+ wsUrl: 'ws://localhost:8888/',
+ });
+ this.serverManager.launch();
+ }
+}
diff --git a/apps/docs/src/lab/index.less b/apps/docs/src/lab/index.less
new file mode 100644
index 00000000..b6b1c821
--- /dev/null
+++ b/apps/docs/src/lab/index.less
@@ -0,0 +1,9 @@
+.libro-workbench-app {
+ width: 100%;
+ height: calc(100vh - 61px);
+}
+
+.dumi-default-doc-layout > main {
+ margin: unset;
+ max-width: unset;
+}
diff --git a/apps/docs/src/lab/index.tsx b/apps/docs/src/lab/index.tsx
new file mode 100644
index 00000000..4a25a3b7
--- /dev/null
+++ b/apps/docs/src/lab/index.tsx
@@ -0,0 +1,21 @@
+import { LibroLabModule } from '@difizen/libro-lab';
+import { ManaAppPreset, ManaComponents, ManaModule } from '@difizen/mana-app';
+
+import { LibroApp } from './app.js';
+import './index.less';
+
+const BaseModule = ManaModule.create().register(LibroApp);
+
+const App = (): JSX.Element => {
+ return (
+
+
+
+ );
+};
+
+export default App;
diff --git a/apps/docs/src/workbench/index.tsx b/apps/docs/src/workbench/index.tsx
index d3618d81..e1e8c323 100644
--- a/apps/docs/src/workbench/index.tsx
+++ b/apps/docs/src/workbench/index.tsx
@@ -29,11 +29,6 @@ const BaseModule = ManaModule.create().register(
view: FileTreeView,
slot: LibroWorkbenchSlots.Left,
}),
- // createViewPreference({
- // autoCreate: true,
- // view: FileTreeView,
- // slot: LibroWorkbenchSlots.Left,
- // }),
);
const App = (): JSX.Element => {
diff --git a/package.json b/package.json
index 504c6efb..1a0592d1 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"test": "nx run-many --target=test",
"ci": "nx run-many --target=lint:prettier,lint,test,test:jest",
"ci:affected": "nx affected --target=lint:prettier,lint,test:jest,coverage:jest",
- "docs": "nx run @difizen/mana-docs:start",
+ "docs": "nx run @difizen/libro-docs:start",
"changeset": "changeset",
"clean": "git clean -fX ."
},
diff --git a/packages/libro-jupyter/src/file/file-command.tsx b/packages/libro-jupyter/src/file/file-command.tsx
new file mode 100644
index 00000000..a792680e
--- /dev/null
+++ b/packages/libro-jupyter/src/file/file-command.tsx
@@ -0,0 +1,320 @@
+import pathUtil from 'path';
+
+import { ReloadOutlined } from '@ant-design/icons';
+import type {
+ CommandRegistry,
+ MenuPath,
+ MenuRegistry,
+ ToolbarRegistry,
+} from '@difizen/mana-app';
+import { ViewManager } from '@difizen/mana-app';
+import {
+ CommandContribution,
+ FileStatNode,
+ FileTreeCommand,
+ inject,
+ MenuContribution,
+ ModalService,
+ OpenerService,
+ singleton,
+ ToolbarContribution,
+ URI,
+} from '@difizen/mana-app';
+import { message, Modal } from 'antd';
+
+import { FileCreateModal } from './file-create-modal.js';
+import { FileDirCreateModal } from './file-createdir-modal.js';
+import { FileRenameModal } from './file-rename-modal.js';
+import { JupyterFileService } from './file-service.js';
+import { FileView } from './file-view/index.js';
+import { copy2clipboard } from './utils.js';
+
+const FileCommands = {
+ OPEN_FILE: {
+ id: 'fileTree.command.openfile',
+ label: '打开',
+ },
+ COPY: {
+ id: 'fileTree.command.copy',
+ label: '复制',
+ },
+ PASTE: {
+ id: 'fileTree.command.paste',
+ label: '粘贴',
+ },
+ CUT: {
+ id: 'fileTree.command.cut',
+ label: '剪切',
+ },
+ RENAME: {
+ id: 'fileTree.command.rename',
+ label: '重命名',
+ },
+ COPY_PATH: {
+ id: 'fileTree.command.copyPath',
+ label: '复制路径',
+ },
+ COPY_RELATIVE_PATH: {
+ id: 'fileTree.command.copyRelativePath',
+ label: '复制相对路径',
+ },
+ CREATE_FILE: {
+ id: 'fileTree.command.createfile',
+ label: '新建文件',
+ },
+ CREATE_DIR: {
+ id: 'fileTree.command.createdir',
+ label: '新建文件夹',
+ },
+ REFRESH: {
+ id: 'fileTree.command.refresh',
+ label: '刷新',
+ },
+};
+export const FileTreeContextMenuPath: MenuPath = ['file-tree-context-menu'];
+
+@singleton({
+ contrib: [CommandContribution, MenuContribution, ToolbarContribution],
+})
+export class FileCommandContribution
+ implements CommandContribution, MenuContribution, ToolbarContribution
+{
+ protected viewManager: ViewManager;
+ @inject(JupyterFileService) fileService: JupyterFileService;
+ @inject(ModalService) modalService: ModalService;
+ @inject(OpenerService) protected openService: OpenerService;
+ fileView: FileView;
+ lastAction: 'COPY' | 'CUT';
+ lastActionNode: FileStatNode;
+
+ constructor(@inject(ViewManager) viewManager: ViewManager) {
+ this.viewManager = viewManager;
+ this.viewManager
+ .getOrCreateView(FileView)
+ .then((view) => {
+ this.fileView = view;
+ return;
+ })
+ .catch(() => {
+ //
+ });
+ }
+
+ registerMenus(menu: MenuRegistry) {
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.CREATE_FILE.id,
+ command: FileCommands.CREATE_FILE.id,
+ order: 'a',
+ });
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.CREATE_DIR.id,
+ command: FileCommands.CREATE_DIR.id,
+ order: 'a',
+ });
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.OPEN_FILE.id,
+ command: FileCommands.OPEN_FILE.id,
+ order: 'a',
+ });
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.COPY.id,
+ command: FileCommands.COPY.id,
+ order: 'b',
+ });
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.PASTE.id,
+ command: FileCommands.PASTE.id,
+ order: 'c',
+ });
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.CUT.id,
+ command: FileCommands.CUT.id,
+ order: 'd',
+ });
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.RENAME.id,
+ command: FileCommands.RENAME.id,
+ order: 'e',
+ });
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.COPY_PATH.id,
+ command: FileCommands.COPY_PATH.id,
+ order: 'g',
+ });
+ menu.registerMenuAction(FileTreeContextMenuPath, {
+ id: FileCommands.COPY_RELATIVE_PATH.id,
+ command: FileCommands.COPY_RELATIVE_PATH.id,
+ order: 'g',
+ });
+ }
+ registerCommands(command: CommandRegistry): void {
+ command.registerCommand(FileCommands.OPEN_FILE, {
+ execute: (node) => {
+ try {
+ if (node.fileStat.isFile) {
+ this.openService
+ .getOpener(node.uri)
+ .then((opener) => {
+ if (opener) {
+ opener.open(node.uri, {
+ viewOptions: {
+ name: node.fileStat.name,
+ },
+ });
+ }
+ return;
+ })
+ .catch(() => {
+ throw Error();
+ });
+ }
+ } catch {
+ message.error('文件打开失败');
+ }
+ },
+ isVisible: (node) => {
+ return FileStatNode.is(node) && node.fileStat.isFile;
+ },
+ });
+ command.registerHandler(FileTreeCommand.REMOVE.id, {
+ execute: (node) => {
+ if (FileStatNode.is(node)) {
+ const filePath = node.uri.path.toString();
+ Modal.confirm({
+ title: '确认删除一下文件或文件夹?',
+ content: filePath,
+ onOk: async () => {
+ try {
+ await this.fileService.delete(node.uri);
+ } catch {
+ message.error('删除文件失败!');
+ }
+ this.fileView.model.refresh();
+ },
+ });
+ }
+ },
+ isVisible: (node) => {
+ return FileStatNode.is(node);
+ },
+ });
+ command.registerCommand(FileCommands.COPY, {
+ execute: (node) => {
+ this.lastAction = 'COPY';
+ this.lastActionNode = node;
+ },
+ isVisible: (node) => {
+ return FileStatNode.is(node) && node.fileStat.isFile;
+ },
+ });
+ command.registerCommand(FileCommands.CUT, {
+ execute: (node) => {
+ this.lastAction = 'CUT';
+ this.lastActionNode = node;
+ },
+ isVisible: (node) => {
+ return FileStatNode.is(node) && node.fileStat.isFile;
+ },
+ });
+ command.registerCommand(FileCommands.PASTE, {
+ execute: async (data) => {
+ try {
+ if (FileStatNode.is(data)) {
+ const targetUri = data.fileStat.isDirectory ? data.uri : data.uri.parent;
+ await this.fileService.copy(this.lastActionNode.uri, targetUri);
+ } else if (data instanceof FileView) {
+ const targetPath = '/';
+ await this.fileService.copy(this.lastActionNode.uri, new URI(targetPath));
+ }
+ if (this.lastAction === 'CUT') {
+ await this.fileService.delete(this.lastActionNode.uri);
+ }
+ this.fileView.model.refresh();
+ return;
+ } catch {
+ message.error('粘贴失败!');
+ }
+ },
+ isVisible: () => {
+ return this.lastAction === 'CUT' || this.lastAction === 'COPY';
+ },
+ });
+ command.registerCommand(FileCommands.RENAME, {
+ execute: async (node) => {
+ this.modalService.openModal(FileRenameModal, {
+ resource: node.uri,
+ fileName: node.uri.path.base,
+ });
+ },
+ isVisible: (node) => {
+ return FileStatNode.is(node);
+ },
+ });
+ command.registerCommand(FileCommands.CREATE_FILE, {
+ execute: async (data) => {
+ let path = '/workspace';
+ if (FileStatNode.is(data)) {
+ path = data.fileStat.isDirectory
+ ? data.uri.path.toString()
+ : data.uri.path.dir.toString();
+ }
+ this.modalService.openModal(FileCreateModal, {
+ path,
+ });
+ },
+ });
+ command.registerCommand(FileCommands.CREATE_DIR, {
+ execute: async (data) => {
+ let path = '/workspace';
+ if (FileStatNode.is(data)) {
+ path = data.fileStat.isDirectory
+ ? data.uri.path.toString()
+ : data.uri.path.dir.toString();
+ }
+ this.modalService.openModal(FileDirCreateModal, {
+ path,
+ });
+ },
+ });
+
+ command.registerCommand(FileCommands.COPY_PATH, {
+ execute: async (data) => {
+ let path = '/workspace';
+ if (FileStatNode.is(data)) {
+ path = data.uri.path.toString();
+ }
+ copy2clipboard(path);
+ },
+ });
+
+ command.registerCommand(FileCommands.COPY_RELATIVE_PATH, {
+ execute: async (data) => {
+ let relative = '';
+ if (FileStatNode.is(data)) {
+ relative = pathUtil.relative('/workspace', data.uri.path.toString());
+ }
+ copy2clipboard(relative);
+ },
+ });
+
+ command.registerCommand(FileCommands.REFRESH, {
+ execute: async (view) => {
+ if (view instanceof FileView) {
+ view.model.refresh();
+ }
+ },
+ isVisible: (view) => {
+ return view instanceof FileView;
+ },
+ });
+ }
+
+ registerToolbarItems(toolbarRegistry: ToolbarRegistry): void {
+ toolbarRegistry.registerItem({
+ id: FileCommands.REFRESH.id,
+ command: FileCommands.REFRESH.id,
+ icon: ,
+ tooltip: '刷新',
+ });
+ }
+}
diff --git a/packages/libro-jupyter/src/file/file-create-modal-contribution.ts b/packages/libro-jupyter/src/file/file-create-modal-contribution.ts
new file mode 100644
index 00000000..cb2eb5a7
--- /dev/null
+++ b/packages/libro-jupyter/src/file/file-create-modal-contribution.ts
@@ -0,0 +1,10 @@
+import { ModalContribution, singleton } from '@difizen/mana-app';
+
+import { FileCreateModal } from './file-create-modal.js';
+
+@singleton({ contrib: ModalContribution })
+export class FileCreateModalContribution implements ModalContribution {
+ registerModal() {
+ return FileCreateModal;
+ }
+}
diff --git a/packages/libro-jupyter/src/file/file-create-modal.tsx b/packages/libro-jupyter/src/file/file-create-modal.tsx
new file mode 100644
index 00000000..6732c25a
--- /dev/null
+++ b/packages/libro-jupyter/src/file/file-create-modal.tsx
@@ -0,0 +1,73 @@
+import type { ModalItem, ModalItemProps } from '@difizen/mana-app';
+import { URI, useInject, ViewManager } from '@difizen/mana-app';
+import type { InputRef } from 'antd';
+import { Input, Modal } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+
+import { FileView, JupyterFileService } from './index.js';
+
+export interface ModalItemType {
+ path: string;
+}
+
+export const FileCreateModalComponent: React.FC> = ({
+ visible,
+ close,
+ data,
+}: ModalItemProps) => {
+ const fileService = useInject(JupyterFileService);
+ const viewManager = useInject(ViewManager);
+ const [newFileName, setNewFileName] = useState('');
+ const [fileView, setFileView] = useState();
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ viewManager
+ .getOrCreateView(FileView)
+ .then((view) => {
+ setFileView(view);
+ return;
+ })
+ .catch(() => {
+ //
+ });
+ inputRef.current?.focus();
+ });
+ return (
+ {
+ await fileService.newFile(newFileName, new URI(data.path));
+ if (fileView) {
+ fileView.model.refresh();
+ }
+ close();
+ }}
+ keyboard={true}
+ >
+ {
+ setNewFileName(e.target.value);
+ }}
+ ref={inputRef}
+ onKeyDown={async (e) => {
+ if (e.keyCode === 13) {
+ await fileService.newFile(newFileName, new URI(data.path));
+ if (fileView) {
+ fileView.model.refresh();
+ }
+ close();
+ }
+ }}
+ />
+
+ );
+};
+
+export const FileCreateModal: ModalItem = {
+ id: 'file.create.modal',
+ component: FileCreateModalComponent,
+};
diff --git a/packages/libro-jupyter/src/file/file-createdir-modal-contribution.ts b/packages/libro-jupyter/src/file/file-createdir-modal-contribution.ts
new file mode 100644
index 00000000..2c4034da
--- /dev/null
+++ b/packages/libro-jupyter/src/file/file-createdir-modal-contribution.ts
@@ -0,0 +1,10 @@
+import { ModalContribution, singleton } from '@difizen/mana-app';
+
+import { FileDirCreateModal } from './file-createdir-modal.js';
+
+@singleton({ contrib: ModalContribution })
+export class FileCreateDirModalContribution implements ModalContribution {
+ registerModal() {
+ return FileDirCreateModal;
+ }
+}
diff --git a/packages/libro-jupyter/src/file/file-createdir-modal.tsx b/packages/libro-jupyter/src/file/file-createdir-modal.tsx
new file mode 100644
index 00000000..b861622a
--- /dev/null
+++ b/packages/libro-jupyter/src/file/file-createdir-modal.tsx
@@ -0,0 +1,76 @@
+import type { ModalItem, ModalItemProps } from '@difizen/mana-app';
+import { URI } from '@difizen/mana-app';
+import { ViewManager } from '@difizen/mana-app';
+import { useInject } from '@difizen/mana-app';
+import type { InputRef } from 'antd';
+import { Input } from 'antd';
+import { Modal } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+
+import { JupyterFileService } from './file-service.js';
+import { FileView } from './file-view/index.js';
+
+export interface ModalItemType {
+ path: string;
+}
+
+export const FileCreateDirModalComponent: React.FC> = ({
+ visible,
+ close,
+ data,
+}: ModalItemProps) => {
+ const fileService = useInject(JupyterFileService);
+ const viewManager = useInject(ViewManager);
+ const inputRef = useRef(null);
+ const [dirName, setDirName] = useState('');
+ const [fileView, setFileView] = useState();
+ useEffect(() => {
+ viewManager
+ .getOrCreateView(FileView)
+ .then((view) => {
+ setFileView(view);
+ return;
+ })
+ .catch(() => {
+ //
+ });
+ inputRef.current?.focus();
+ });
+ return (
+ {
+ await fileService.newFileDir(dirName, new URI(data.path));
+ if (fileView) {
+ fileView.model.refresh();
+ }
+ close();
+ }}
+ keyboard={true}
+ >
+ {
+ setDirName(e.target.value);
+ }}
+ ref={inputRef}
+ onKeyDown={async (e) => {
+ if (e.keyCode === 13) {
+ await fileService.newFileDir(dirName, new URI(data.path));
+ if (fileView) {
+ fileView.model.refresh();
+ }
+ close();
+ }
+ }}
+ />
+
+ );
+};
+
+export const FileDirCreateModal: ModalItem = {
+ id: 'file.createdir.modal',
+ component: FileCreateDirModalComponent,
+};
diff --git a/packages/libro-jupyter/src/file/file-protocol.ts b/packages/libro-jupyter/src/file/file-protocol.ts
index b420bab7..248ab0e8 100644
--- a/packages/libro-jupyter/src/file/file-protocol.ts
+++ b/packages/libro-jupyter/src/file/file-protocol.ts
@@ -5,3 +5,20 @@ export enum FileType {
SymbolicLink = 64,
}
export type DirItem = [string, FileType];
+
+export interface EditorView {
+ dirty: boolean;
+}
+
+export const EditorView = {
+ is: (data?: Record): data is EditorView => {
+ return (
+ !!data &&
+ typeof data === 'object' &&
+ 'id' in data &&
+ 'view' in data &&
+ 'dirty' in data &&
+ typeof data['view'] === 'function'
+ );
+ },
+};
diff --git a/packages/libro-jupyter/src/file/file-rename-modal-contribution.ts b/packages/libro-jupyter/src/file/file-rename-modal-contribution.ts
new file mode 100644
index 00000000..a58aa549
--- /dev/null
+++ b/packages/libro-jupyter/src/file/file-rename-modal-contribution.ts
@@ -0,0 +1,10 @@
+import { ModalContribution, singleton } from '@difizen/mana-app';
+
+import { FileRenameModal } from './file-rename-modal.js';
+
+@singleton({ contrib: ModalContribution })
+export class FileRenameModalContribution implements ModalContribution {
+ registerModal() {
+ return FileRenameModal;
+ }
+}
diff --git a/packages/libro-jupyter/src/file/file-rename-modal.tsx b/packages/libro-jupyter/src/file/file-rename-modal.tsx
new file mode 100644
index 00000000..6f9ae6d5
--- /dev/null
+++ b/packages/libro-jupyter/src/file/file-rename-modal.tsx
@@ -0,0 +1,64 @@
+import type { ModalItem, ModalItemProps, URI } from '@difizen/mana-app';
+import { useInject, ViewManager } from '@difizen/mana-app';
+import type { InputRef } from 'antd';
+import { Input, Modal } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+
+import { JupyterFileService } from './file-service.js';
+import { FileView } from './file-view/index.js';
+
+export interface ModalItemType {
+ resource: URI;
+ fileName: string;
+}
+
+export const FileRenameModalComponent: React.FC> = ({
+ visible,
+ close,
+ data,
+}: ModalItemProps) => {
+ const fileService = useInject(JupyterFileService);
+ const viewManager = useInject(ViewManager);
+ const [newFileName, setNewFileName] = useState(data.fileName);
+ const inputRef = useRef(null);
+ const [fileView, setFileView] = useState();
+ useEffect(() => {
+ viewManager
+ .getOrCreateView(FileView)
+ .then((view) => {
+ setFileView(view);
+ return;
+ })
+ .catch(() => {
+ //
+ });
+ inputRef.current?.focus();
+ });
+ return (
+ {
+ await fileService.rename(data.resource, newFileName);
+ if (fileView) {
+ fileView.model.refresh();
+ }
+ close();
+ }}
+ >
+ {
+ setNewFileName(e.target.value);
+ }}
+ ref={inputRef}
+ />
+
+ );
+};
+
+export const FileRenameModal: ModalItem = {
+ id: 'file.rename.modal',
+ component: FileRenameModalComponent,
+};
diff --git a/packages/libro-jupyter/src/file/file-service.ts b/packages/libro-jupyter/src/file/file-service.ts
index a94a0a0f..b56e7388 100644
--- a/packages/libro-jupyter/src/file/file-service.ts
+++ b/packages/libro-jupyter/src/file/file-service.ts
@@ -1,3 +1,5 @@
+import pathUtil from 'path';
+
import type { IContentsModel } from '@difizen/libro-kernel';
import { ContentsManager } from '@difizen/libro-kernel';
import type {
@@ -7,6 +9,7 @@ import type {
ResolveFileOptions,
} from '@difizen/mana-app';
import { FileService, URI, inject, singleton } from '@difizen/mana-app';
+import { message } from 'antd';
import { FileNameAlias } from './file-name-alias.js';
import type { DirItem } from './file-protocol.js';
@@ -50,41 +53,29 @@ export class JupyterFileService extends FileService {
}
async write(filePath: string, content: string): Promise {
- try {
- await this.contentsManager.save(filePath, {
- content,
- });
- return filePath;
- } catch (_e) {
- //
- }
- return undefined;
+ await this.contentsManager.save(filePath, {
+ content,
+ });
+ return filePath;
}
async readDir(dirPath: string): Promise {
let children: DirItem[] = [];
- try {
- const res = await this.contentsManager.get(dirPath, { type: 'directory' });
- if (res && this.isDirectory(res)) {
- const content = res.content;
- children = content.map((item) => {
- return [item.path, this.isDirectory(item) ? 2 : 1];
- });
- }
- } catch (_e) {
- //
+ const res = await this.contentsManager.get(dirPath, { type: 'directory' });
+ if (res && this.isDirectory(res)) {
+ const content = res.content;
+ children = content.map((item) => {
+ return [item.path, this.isDirectory(item) ? 2 : 1];
+ });
}
return children;
}
+
async read(filePath: string): Promise {
let content: string | undefined = undefined;
- try {
- const res = await this.contentsManager.get(filePath);
- if (res && !this.isDirectory(res)) {
- content = res.content as string;
- }
- } catch (_e) {
- //
+ const res = await this.contentsManager.get(filePath);
+ if (res && !this.isDirectory(res)) {
+ content = res.content as string;
}
return content;
}
@@ -94,7 +85,7 @@ export class JupyterFileService extends FileService {
try {
const res = await this.contentsManager.get(filePath);
stat = this.toFileMeta(res);
- } catch (_e) {
+ } catch {
//
}
return stat;
@@ -135,6 +126,7 @@ export class JupyterFileService extends FileService {
_target: URI,
_options?: CopyFileOptions,
): Promise {
+ await this.contentsManager.copy(source.path.toString(), _target.path.toString());
return this.resolve(source);
}
override async move(
@@ -147,7 +139,6 @@ export class JupyterFileService extends FileService {
toFileStatMeta(meta: FileMeta): FileStatWithMetadata {
const uri = URI.withScheme(new URI(meta.resource), 'file');
-
return {
...meta,
resource: uri,
@@ -176,4 +167,49 @@ export class JupyterFileService extends FileService {
isSymbolicLink: false,
};
}
+
+ async delete(
+ resource: URI,
+ _options?: ResolveFileOptions | undefined,
+ ): Promise {
+ await this.contentsManager.delete(resource.path.toString());
+ return this.resolve(resource);
+ }
+
+ async rename(resource: URI, newName: string): Promise {
+ const newPath = pathUtil.join(resource.path.dir.toString(), newName);
+ await this.contentsManager.rename(resource.path.toString(), newPath);
+
+ return this.resolve(resource);
+ }
+
+ async newFile(fileName: string, target: URI): Promise {
+ const targetFileUri = new URI(pathUtil.join(target.path.toString(), fileName));
+ if ((await this.resolve(targetFileUri)).isFile) {
+ message.error('文件名重复');
+ return this.resolve(target);
+ }
+ const fileNameArr = fileName.split('.');
+ const ext = fileNameArr[fileNameArr.length - 1];
+ const res = await this.contentsManager.newUntitled({
+ path: target.path.toString(),
+ ext,
+ });
+ await this.rename(new URI(res.path.toString()), fileName);
+ return this.resolve(target);
+ }
+
+ async newFileDir(dirName: string, target: URI): Promise {
+ const targetFileUri = new URI(pathUtil.join(target.path.toString(), dirName));
+ if ((await this.resolve(targetFileUri)).isDirectory) {
+ message.error('文件夹重复');
+ return this.resolve(target);
+ }
+ const res = await this.contentsManager.newUntitled({
+ path: target.path.toString(),
+ type: 'directory',
+ });
+ await this.rename(new URI(res.path.toString()), dirName);
+ return this.resolve(target);
+ }
}
diff --git a/packages/libro-jupyter/src/file/file-view/index.tsx b/packages/libro-jupyter/src/file/file-view/index.tsx
index f32b5a59..1a20db0d 100644
--- a/packages/libro-jupyter/src/file/file-view/index.tsx
+++ b/packages/libro-jupyter/src/file/file-view/index.tsx
@@ -1,3 +1,4 @@
+import { FolderFilled } from '@ant-design/icons';
import type { TreeNode, ViewOpenHandler } from '@difizen/mana-app';
import { FileTreeViewFactory } from '@difizen/mana-app';
import {
@@ -19,7 +20,7 @@ import {
inject,
singleton,
} from '@difizen/mana-app';
-import type React from 'react';
+import React from 'react';
import type { LibroNavigatableView } from '../navigatable-view.js';
@@ -53,6 +54,8 @@ export class FileView extends FileTreeView {
labelProvider,
decoratorService,
);
+ this.title.label = '文件导航';
+ this.title.icon = ;
this.toDispose.push(this.model.onOpenNode(this.openNode));
}
diff --git a/packages/libro-jupyter/src/file/module.ts b/packages/libro-jupyter/src/file/module.ts
index 9e8a9fe6..6f99eea0 100644
--- a/packages/libro-jupyter/src/file/module.ts
+++ b/packages/libro-jupyter/src/file/module.ts
@@ -1,6 +1,10 @@
import { FileTreeModule, ManaModule } from '@difizen/mana-app';
+import { FileCommandContribution } from './file-command.js';
+import { FileCreateModalContribution } from './file-create-modal-contribution.js';
+import { FileCreateDirModalContribution } from './file-createdir-modal-contribution.js';
import { FileNameAlias } from './file-name-alias.js';
+import { FileRenameModalContribution } from './file-rename-modal-contribution.js';
import { JupyterFileService } from './file-service.js';
import { FileTreeLabelProvider } from './file-tree-label-provider.js';
import { FileView } from './file-view/index.js';
@@ -15,5 +19,9 @@ export const LibroJupyterFileModule = ManaModule.create()
FileTreeLabelProvider,
LibroNavigatableView,
LibroJupyterOpenHandler,
+ FileCommandContribution,
+ FileCreateModalContribution,
+ FileCreateDirModalContribution,
+ FileRenameModalContribution,
)
.dependOn(FileTreeModule);
diff --git a/packages/libro-jupyter/src/file/navigatable-view.tsx b/packages/libro-jupyter/src/file/navigatable-view.tsx
index aea1ce00..c2c9be41 100644
--- a/packages/libro-jupyter/src/file/navigatable-view.tsx
+++ b/packages/libro-jupyter/src/file/navigatable-view.tsx
@@ -1,6 +1,7 @@
import type { LibroView } from '@difizen/libro-core';
import { LibroService } from '@difizen/libro-core';
import type { NavigatableView } from '@difizen/mana-app';
+import { CommandRegistry } from '@difizen/mana-app';
import {
BaseView,
inject,
@@ -19,6 +20,8 @@ import {
} from '@difizen/mana-app';
import { createRef, forwardRef } from 'react';
+import type { EditorView } from './file-protocol.js';
+
export const LibroEditorComponent = forwardRef(function LibroEditorComponent() {
const instance = useInject(ViewInstance);
@@ -32,15 +35,23 @@ export const LibroEditorComponent = forwardRef(function LibroEditorComponent() {
export const LibroNavigatableViewFactoryId = 'libro-navigatable-view-factory';
@transient()
@view(LibroNavigatableViewFactoryId)
-export class LibroNavigatableView extends BaseView implements NavigatableView {
+export class LibroNavigatableView
+ extends BaseView
+ implements NavigatableView, EditorView
+{
@inject(LibroService) protected libroService: LibroService;
+ @inject(CommandRegistry) commandRegistry: CommandRegistry;
+
override view = LibroEditorComponent;
codeRef = createRef();
@prop() filePath?: string;
+ @prop()
+ dirty: boolean;
+
@prop()
libroView?: LibroView;
@@ -56,6 +67,7 @@ export class LibroNavigatableView extends BaseView implements NavigatableView {
) {
super();
this.filePath = options.path;
+ this.dirty = false;
this.title.caption = options.path;
const uri = new URI(options.path);
const uriRef = URIIconReference.create('file', new VScodeURI(options.path));
@@ -77,7 +89,14 @@ export class LibroNavigatableView extends BaseView implements NavigatableView {
return;
}
this.libroView = libroView;
+ this.libroView.model.onContentChanged(() => {
+ this.dirty = true;
+ });
+ this.libroView.onSave(() => {
+ this.dirty = false;
+ });
await this.libroView.initialized;
+ this.libroView.focus();
this.defer.resolve();
}
diff --git a/packages/libro-jupyter/src/file/utils.ts b/packages/libro-jupyter/src/file/utils.ts
new file mode 100644
index 00000000..e1a93dff
--- /dev/null
+++ b/packages/libro-jupyter/src/file/utils.ts
@@ -0,0 +1,51 @@
+import { message } from 'antd';
+
+function copyFallback(string: string) {
+ function handler(event: ClipboardEvent) {
+ const clipboardData = event.clipboardData || (window as any).clipboardData;
+ clipboardData.setData('text/plain', string);
+ event.preventDefault();
+ document.removeEventListener('copy', handler, true);
+ }
+
+ document.addEventListener('copy', handler, true);
+ const successful = document.execCommand('copy');
+ if (successful) {
+ message.success('复制成功');
+ } else {
+ message.warning('复制失败');
+ }
+}
+
+// 复制到剪贴板
+export const copy2clipboard = (string: string) => {
+ navigator.permissions
+ .query({
+ name: 'clipboard-write' as any,
+ })
+ .then((result) => {
+ if (result.state === 'granted' || result.state === 'prompt') {
+ if (window.navigator && window.navigator.clipboard) {
+ window.navigator.clipboard
+ .writeText(string)
+ .then(() => {
+ message.success('复制成功');
+ return;
+ })
+ .catch((err) => {
+ message.warning('复制失败');
+ console.error('Could not copy text: ', err);
+ });
+ } else {
+ console.warn('navigator is not exist');
+ }
+ } else {
+ console.warn('浏览器权限不允许复制');
+ copyFallback(string);
+ }
+ return;
+ })
+ .catch(() => {
+ //
+ });
+};
diff --git a/packages/libro-jupyter/src/index.ts b/packages/libro-jupyter/src/index.ts
index 333f4826..20e59da3 100644
--- a/packages/libro-jupyter/src/index.ts
+++ b/packages/libro-jupyter/src/index.ts
@@ -26,6 +26,7 @@ export * from './theme/index.js';
export * from './toolbar/index.js';
export * from './libro-jupyter-protocol.js';
export * from './libro-jupyter-model.js';
+export * from './libro-jupyter-view.js';
export * from './libro-jupyter-file-service.js';
export * from './libro-jupyter-server-launch-manager.js';
export * from './file/index.js';
diff --git a/packages/libro-lab/package.json b/packages/libro-lab/package.json
index c10c62c1..b29a0bb9 100644
--- a/packages/libro-lab/package.json
+++ b/packages/libro-lab/package.json
@@ -48,10 +48,14 @@
"dependencies": {
"@difizen/mana-app": "latest",
"@difizen/mana-react": "latest",
- "@difizen/libro-jupyter": "0.0.2-alpha.0"
+ "@difizen/libro-jupyter": "0.0.2-alpha.0",
+ "@difizen/libro-toc": "0.0.2-alpha.0",
+ "@ant-design/icons": "^5.1.0",
+ "classnames": "^2.2.6"
},
"peerDependencies": {
- "react": "^18.2.0"
+ "react": "^18.2.0",
+ "antd": "^5.8.6"
},
"devDependencies": {
"@types/react": "^18.2.25"
diff --git a/packages/libro-lab/src/github-link/index.tsx b/packages/libro-lab/src/github-link/index.tsx
new file mode 100644
index 00000000..67b23761
--- /dev/null
+++ b/packages/libro-lab/src/github-link/index.tsx
@@ -0,0 +1,27 @@
+import { GithubFilled } from '@ant-design/icons';
+import { singleton, view } from '@difizen/mana-app';
+import { BaseView } from '@difizen/mana-app';
+import { forwardRef } from 'react';
+
+export const GithubLinkComponent = forwardRef(function GithubLinkComponent() {
+ return (
+
+
+
+ );
+});
+
+@singleton()
+@view('github-link')
+export class GithubLinkView extends BaseView {
+ override view = GithubLinkComponent;
+ link = '';
+ constructor() {
+ super();
+ }
+}
diff --git a/packages/libro-lab/src/index.less b/packages/libro-lab/src/index.less
new file mode 100644
index 00000000..87fd8e3b
--- /dev/null
+++ b/packages/libro-lab/src/index.less
@@ -0,0 +1,69 @@
+#libro-lab-content-layout-left {
+ .mana-side-tab-view {
+ .mana-tabs-left {
+ .mana-tabs-nav {
+ background-color: #f0f2f51f;
+ min-width: 40px;
+ box-shadow:
+ 1px 0 0 0 rgba(0, 10, 26, 0.84%),
+ 2px 0 0 0 rgba(255, 255, 255, 4.8%);
+
+ .mana-tabs-ink-bar {
+ display: none;
+ }
+
+ .mana-tabs-tab {
+ width: 40px;
+ height: 40px;
+
+ &-btn {
+ width: 24px;
+ height: 24px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .mana-tab-icon {
+ .anticon {
+ font-size: 16px;
+ }
+ }
+ }
+ }
+
+ .mana-tabs-tab-active {
+ .mana-tabs-tab-btn {
+ background-color: #000a1a1f;
+
+ .mana-tab-icon {
+ color: #1890ff;
+ }
+ }
+ }
+ }
+
+ .mana-tabs-content-holder {
+ background-image: linear-gradient(269deg, #c8c8cd1f 0%, #ffffff1f 100%);
+ border-left: none;
+ background-color: unset;
+
+ .mana-tab-side-pane-content {
+ height: calc(100% - 32px);
+ }
+ }
+ }
+ }
+}
+
+#libro-lab-content-layout-main {
+ background-color: #f8f8fb;
+
+ .mana-tabs-nav {
+ background-color: #f8f8fb;
+ }
+}
+
+#libro-lab-content-layout-main-container {
+ overflow: auto;
+}
diff --git a/packages/libro-lab/src/kernel-manager/index.tsx b/packages/libro-lab/src/kernel-manager/index.tsx
new file mode 100644
index 00000000..da602142
--- /dev/null
+++ b/packages/libro-lab/src/kernel-manager/index.tsx
@@ -0,0 +1,22 @@
+import { CodeFilled } from '@ant-design/icons';
+import { singleton, view } from '@difizen/mana-app';
+import { BaseView } from '@difizen/mana-app';
+import { forwardRef } from 'react';
+
+// import './index.less';
+
+export const KernelManagerComponent = forwardRef(function KernelManagerComponent() {
+ return 暂无文件;
+});
+
+@singleton()
+@view('kernel-manager')
+export class KernelManagerView extends BaseView {
+ override view = KernelManagerComponent;
+
+ constructor() {
+ super();
+ this.title.icon = ;
+ this.title.label = 'Kernel 管理';
+ }
+}
diff --git a/packages/libro-lab/src/lab-app.ts b/packages/libro-lab/src/lab-app.ts
index fe4d1f9b..9d4a2b44 100644
--- a/packages/libro-lab/src/lab-app.ts
+++ b/packages/libro-lab/src/lab-app.ts
@@ -15,7 +15,7 @@ import {
singleton,
} from '@difizen/mana-app';
-import { LibroLabContentSlots } from './layout/index.js';
+import { LibroLabLayoutSlots } from './layout/index.js';
@singleton({ contrib: ApplicationContribution })
export class LibroLabApp implements ApplicationContribution {
@@ -26,13 +26,9 @@ export class LibroLabApp implements ApplicationContribution {
@inject(ConfigurationService) configurationService: ConfigurationService;
async onStart() {
- this.serverConnection.updateSettings({
- baseUrl: 'http://localhost:8888/',
- wsUrl: 'ws://localhost:8888/',
- });
this.configurationService.set(
LibroJupyterConfiguration['OpenSlot'],
- LibroLabContentSlots.main,
+ LibroLabLayoutSlots.content,
);
await this.initialWorkspace();
}
diff --git a/packages/libro-lab/src/layout/brand/index.less b/packages/libro-lab/src/layout/brand/index.less
new file mode 100644
index 00000000..ca6cf6d2
--- /dev/null
+++ b/packages/libro-lab/src/layout/brand/index.less
@@ -0,0 +1,16 @@
+.libro-lab-brand {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ padding: 0 22px;
+
+ &-logo {
+ height: 16px;
+ }
+
+ label {
+ font-size: 16px;
+ margin-left: 8px;
+ }
+}
diff --git a/packages/libro-lab/src/layout/brand/index.tsx b/packages/libro-lab/src/layout/brand/index.tsx
new file mode 100644
index 00000000..395a0572
--- /dev/null
+++ b/packages/libro-lab/src/layout/brand/index.tsx
@@ -0,0 +1,22 @@
+import { BaseView, view, singleton } from '@difizen/mana-app';
+import * as React from 'react';
+
+import { Logo } from './logo.js';
+import './index.less';
+
+export const Brand: React.ForwardRefExoticComponent = React.forwardRef(
+ function Brand(_props, ref: React.ForwardedRef) {
+ return (
+
+
+
+
+ );
+ },
+);
+
+@singleton()
+@view('libro-brand-view')
+export class BrandView extends BaseView {
+ override view = Brand;
+}
diff --git a/packages/libro-lab/src/layout/brand/logo.tsx b/packages/libro-lab/src/layout/brand/logo.tsx
new file mode 100644
index 00000000..2fc6ff5f
--- /dev/null
+++ b/packages/libro-lab/src/layout/brand/logo.tsx
@@ -0,0 +1,39 @@
+export interface IProps {
+ className?: string;
+ width?: string;
+ height?: string;
+}
+export function Logo(props: IProps) {
+ const { className = '' } = props;
+ return (
+
+ );
+}
diff --git a/packages/libro-lab/src/layout/container.tsx b/packages/libro-lab/src/layout/container.tsx
new file mode 100644
index 00000000..73158a97
--- /dev/null
+++ b/packages/libro-lab/src/layout/container.tsx
@@ -0,0 +1,28 @@
+import { singleton, Slot, view } from '@difizen/mana-app';
+import { BaseView } from '@difizen/mana-app';
+import { BoxPanel } from '@difizen/mana-react';
+import { forwardRef } from 'react';
+
+import './index.less';
+import { LibroLabLayoutSlots } from './protocol.js';
+
+export const LibroLabLayoutContainerComponent = forwardRef(
+ function LibroLabLayoutContainerComponent() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ },
+);
+
+@singleton()
+@view('libro-lab-layout-container')
+export class LibroLabLayoutContainerView extends BaseView {
+ override view = LibroLabLayoutContainerComponent;
+}
diff --git a/packages/libro-lab/src/layout/editor-tab-view.tsx b/packages/libro-lab/src/layout/editor-tab-view.tsx
new file mode 100644
index 00000000..387b1533
--- /dev/null
+++ b/packages/libro-lab/src/layout/editor-tab-view.tsx
@@ -0,0 +1,67 @@
+import { CloseOutlined } from '@ant-design/icons';
+import { EditorView } from '@difizen/libro-jupyter';
+import type { View } from '@difizen/mana-app';
+import {
+ CardTabView,
+ MenuRender,
+ transient,
+ view,
+ ViewContext,
+} from '@difizen/mana-app';
+import { Dropdown } from '@difizen/mana-react';
+import { Badge } from 'antd';
+import classnames from 'classnames';
+
+@transient()
+@view('LibroLabEditorTab')
+export class EditorTabView extends CardTabView {
+ protected override renderTab(item: View) {
+ return (
+
+ }
+ >
+
+ {item.title.icon && (
+
+ {this.renderTitleIcon(item.title.icon)}
+
+ )}
+ {this.renderTitleLabel(item.title.label)}
+ {this.renderTail(item)}
+
+
+
+ );
+ }
+
+ protected renderTail(item: View) {
+ const isDirty = EditorView.is(item) && item.dirty;
+ return (
+
+ {isDirty ? (
+
+
+
+ ) : (
+ item.title.closable && (
+
{
+ e.stopPropagation();
+ this.close(item);
+ if (this.children.length > 0) {
+ this.active = this.children[this.children.length - 1];
+ }
+ }}
+ className="mana-tab-close"
+ />
+ )
+ )}
+
+ );
+ }
+}
diff --git a/packages/libro-lab/src/layout/footer/current-file-footer-view.tsx b/packages/libro-lab/src/layout/footer/current-file-footer-view.tsx
new file mode 100644
index 00000000..285de16b
--- /dev/null
+++ b/packages/libro-lab/src/layout/footer/current-file-footer-view.tsx
@@ -0,0 +1,44 @@
+import { LibroNavigatableView } from '@difizen/libro-jupyter';
+import {
+ BaseView,
+ inject,
+ singleton,
+ useInject,
+ view,
+ ViewInstance,
+} from '@difizen/mana-app';
+import * as React from 'react';
+
+import { LayoutService } from '../layout-service.js';
+import { LibroLabLayoutSlots } from '../protocol.js';
+import './index.less';
+
+const CurrentFileFooterComponent = React.forwardRef(function CurrentFileFooterComponent(
+ _props,
+ ref: React.ForwardedRef,
+) {
+ const currentFileFooterView = useInject(ViewInstance);
+
+ return (
+
+ {`当前文件:${
+ currentFileFooterView.libroNavigatableView?.title.label || ''
+ }`}
+
+ );
+});
+
+@singleton()
+@view('libro-lab-current-file-footer-view')
+export class LibroLabCurrentFileFooterView extends BaseView {
+ override view = CurrentFileFooterComponent;
+ @inject(LayoutService) protected layoutService: LayoutService;
+
+ get libroNavigatableView() {
+ const contentView = this.layoutService.getActiveView(LibroLabLayoutSlots.content);
+ if (contentView instanceof LibroNavigatableView) {
+ return contentView;
+ }
+ return undefined;
+ }
+}
diff --git a/packages/libro-lab/src/layout/footer/footer-view.tsx b/packages/libro-lab/src/layout/footer/footer-view.tsx
new file mode 100644
index 00000000..a8d28b10
--- /dev/null
+++ b/packages/libro-lab/src/layout/footer/footer-view.tsx
@@ -0,0 +1,30 @@
+import { DefaultSlotView, singleton, Slot, view } from '@difizen/mana-app';
+import { BoxPanel } from '@difizen/mana-react';
+import * as React from 'react';
+import './index.less';
+
+export enum FooterArea {
+ left = 'libro-lab-footer-left',
+ right = 'libro-lab-footer-right',
+}
+
+const FooterComponent: React.ForwardRefExoticComponent = React.forwardRef(
+ function FooterComponent(_props, ref: React.ForwardedRef) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ },
+);
+
+@singleton()
+@view('libro-lab-footer-view')
+export class LibroLabLayoutFooterView extends DefaultSlotView {
+ override view = FooterComponent;
+}
diff --git a/packages/libro-lab/src/layout/footer/index.less b/packages/libro-lab/src/layout/footer/index.less
new file mode 100644
index 00000000..1861f1da
--- /dev/null
+++ b/packages/libro-lab/src/layout/footer/index.less
@@ -0,0 +1,17 @@
+.libro-lab-footer {
+ .libro-lab-footer-left {
+ padding: 0 24px;
+ }
+}
+
+.libro-lab-current-file-footer {
+ height: 100%;
+ display: flex;
+ align-items: center;
+
+ span {
+ font-weight: 400;
+ font-size: 12px;
+ color: rgba(0, 10, 26, 68%);
+ }
+}
diff --git a/packages/libro-lab/src/layout/header.tsx b/packages/libro-lab/src/layout/header.tsx
deleted file mode 100644
index e69de29b..00000000
diff --git a/packages/libro-lab/src/layout/index.less b/packages/libro-lab/src/layout/index.less
index 19a38e3a..cbc686ad 100644
--- a/packages/libro-lab/src/layout/index.less
+++ b/packages/libro-lab/src/layout/index.less
@@ -1,17 +1,59 @@
-/* stylelint-disable color-function-notation */
+@ant-prefix: ant;
+
.libro-lab-layout {
width: 100%;
height: 100%;
- &-top {
- height: 48px;
+ &-header {
+ height: 56px;
+ min-height: 56px;
+ }
+
+ &-container {
+ overflow: auto;
}
&-main {
- height: calc(100% - 72px);
+ overflow: auto;
}
- &-bottom {
- height: 24px;
+ &-footer {
+ height: 28px;
+ min-height: 28px;
+ background-image: linear-gradient(270deg, #fafafa 0%, #fff 100%);
+ box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 12%);
+ }
+
+ .mana-header {
+ background: var(--mana-color-bg-container);
+ border-bottom: none;
+
+ .mana-header-left,
+ .mana-header-right {
+ flex: unset !important;
+ }
+
+ .mana-menubar {
+ height: 100%;
+ padding: unset;
+ }
+
+ .mana-menubar-item-text {
+ color: rgba(0, 10, 26, 89%);
+ font-weight: 400;
+ }
+ }
+}
+
+.libro-lab-editor-tab-tail {
+ width: 16px;
+
+ .libro-lab-editor-tab-dirty {
+ padding: 0 4px;
+
+ .ant-badge.ant-badge-status .ant-badge-status-dot {
+ height: 8px;
+ width: 8px;
+ }
}
}
diff --git a/packages/libro-lab/src/layout/index.tsx b/packages/libro-lab/src/layout/index.tsx
index cac0b311..c700d834 100644
--- a/packages/libro-lab/src/layout/index.tsx
+++ b/packages/libro-lab/src/layout/index.tsx
@@ -1,80 +1,4 @@
-import { singleton, Slot, view } from '@difizen/mana-app';
-import { BaseView } from '@difizen/mana-app';
-import { BoxPanel, SplitPanel } from '@difizen/mana-react';
-import { forwardRef } from 'react';
-
-import './index.less';
-
-export const LibroLabSlots = {
- top: 'libro-lab-top',
- bottom: 'libro-lab-bottom',
-};
-export const LibroLabContentSlots = {
- left: 'libro-lab-content-left',
- main: 'libro-lab-content-main',
- bottom: 'libro-lab-content-bottom',
-};
-
-export const LibroLabLayoutComponent = forwardRef(
- function LibroWorkbenchLayoutComponent() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-);
-
-@singleton()
-@view('libro-lab-layout')
-export class LibroLabLayoutView extends BaseView {
- override view = LibroLabLayoutComponent;
-}
+export * from './protocol.js';
+export * from './module.js';
+export * from './layout.js';
+export * from './footer/footer-view.js';
diff --git a/packages/libro-lab/src/layout/layout-service.ts b/packages/libro-lab/src/layout/layout-service.ts
new file mode 100644
index 00000000..544c9acf
--- /dev/null
+++ b/packages/libro-lab/src/layout/layout-service.ts
@@ -0,0 +1,64 @@
+import type { SlotView, View, ViewOpenHandlerOptions } from '@difizen/mana-app';
+import {
+ DefaultSlotView,
+ inject,
+ Notifier,
+ prop,
+ singleton,
+ SlotViewManager,
+} from '@difizen/mana-app';
+
+import type { LibroLabLayoutSlotsType } from './protocol.js';
+import { LibroLabLayoutSlots } from './protocol.js';
+
+export type VisibilityMap = Record;
+
+@singleton()
+export class LayoutService {
+ @inject(SlotViewManager) protected readonly slotViewManager: SlotViewManager;
+
+ @prop()
+ protected visibilityMap: VisibilityMap = {
+ [LibroLabLayoutSlots.header]: true,
+ [LibroLabLayoutSlots.container]: true,
+ [LibroLabLayoutSlots.main]: true,
+ [LibroLabLayoutSlots.footer]: true,
+ [LibroLabLayoutSlots.navigator]: true,
+ [LibroLabLayoutSlots.content]: true,
+ [LibroLabLayoutSlots.contentBottom]: false,
+ };
+
+ isAreaVisible(slot: LibroLabLayoutSlotsType): boolean {
+ return this.visibilityMap[slot];
+ }
+
+ setAreaVisible(slot: LibroLabLayoutSlotsType, visible: boolean) {
+ this.visibilityMap[slot] = visible;
+ }
+
+ async addView(view: View, option?: ViewOpenHandlerOptions): Promise {
+ const { slot = LibroLabLayoutSlots.main, ...viewOpenOption } = option || {};
+ const slotView = this.slotViewManager.getSlotView(slot) as SlotView;
+ await slotView.addView(view, viewOpenOption);
+ }
+
+ getActiveView(slot: LibroLabLayoutSlotsType) {
+ if (this.isAreaVisible(slot)) {
+ const slotView = this.slotViewManager.getSlotView(slot);
+ if (slotView instanceof DefaultSlotView) {
+ return slotView.active;
+ }
+ }
+ return undefined;
+ }
+
+ onSlotActiveChange(slot: LibroLabLayoutSlotsType, handler: () => void) {
+ if (this.isAreaVisible(slot)) {
+ const slotView = this.slotViewManager.getSlotView(slot);
+ if (slotView instanceof DefaultSlotView) {
+ return Notifier.find(slotView, 'active')?.onChange(() => handler());
+ }
+ }
+ return undefined;
+ }
+}
diff --git a/packages/libro-lab/src/layout/layout.tsx b/packages/libro-lab/src/layout/layout.tsx
new file mode 100644
index 00000000..e15518a3
--- /dev/null
+++ b/packages/libro-lab/src/layout/layout.tsx
@@ -0,0 +1,37 @@
+import { singleton, Slot, useInject, view } from '@difizen/mana-app';
+import { BaseView } from '@difizen/mana-app';
+import { BoxPanel } from '@difizen/mana-react';
+import { forwardRef } from 'react';
+
+import './index.less';
+import { LayoutService } from './layout-service.js';
+import { LibroLabLayoutSlots } from './protocol.js';
+
+export const LibroLabLayoutContainerComponent = forwardRef(
+ function LibroLabLayoutContainerComponent() {
+ const layoutService = useInject(LayoutService);
+
+ return (
+
+
+ {layoutService.isAreaVisible(LibroLabLayoutSlots.header) && (
+
+
+
+ )}
+ {layoutService.isAreaVisible(LibroLabLayoutSlots.container) && (
+
+
+
+ )}
+
+
+ );
+ },
+);
+
+@singleton()
+@view('libro-lab-layout')
+export class LibroLabLayoutView extends BaseView {
+ override view = LibroLabLayoutContainerComponent;
+}
diff --git a/packages/libro-lab/src/layout/main.tsx b/packages/libro-lab/src/layout/main.tsx
new file mode 100644
index 00000000..63d79c76
--- /dev/null
+++ b/packages/libro-lab/src/layout/main.tsx
@@ -0,0 +1,68 @@
+import { singleton, Slot, useInject, view } from '@difizen/mana-app';
+import { BaseView } from '@difizen/mana-app';
+import { SplitPanel } from '@difizen/mana-react';
+import { forwardRef } from 'react';
+
+import './index.less';
+import { LayoutService } from './layout-service.js';
+import { LibroLabLayoutSlots } from './protocol.js';
+
+export const LibroLabLayoutMainComponent = forwardRef(
+ function LibroLabLayoutMainComponent() {
+ const layoutService = useInject(LayoutService);
+
+ return (
+
+ {layoutService.isAreaVisible(LibroLabLayoutSlots.navigator) && (
+
+
+
+ )}
+
+
+ {layoutService.isAreaVisible(LibroLabLayoutSlots.content) && (
+
+
+
+ )}
+ {layoutService.isAreaVisible(LibroLabLayoutSlots.contentBottom) && (
+
+
+
+ )}
+
+
+
+ );
+ },
+);
+
+@singleton()
+@view('libro-lab-layout-main')
+export class LibroLabLayoutMainView extends BaseView {
+ override view = LibroLabLayoutMainComponent;
+}
diff --git a/packages/libro-lab/src/layout/module.ts b/packages/libro-lab/src/layout/module.ts
new file mode 100644
index 00000000..5c2a4ff3
--- /dev/null
+++ b/packages/libro-lab/src/layout/module.ts
@@ -0,0 +1,62 @@
+import {
+ createSlotPreference,
+ FlexSlotView,
+ HeaderArea,
+ HeaderView,
+ ManaModule,
+} from '@difizen/mana-app';
+
+import { BrandView } from './brand/index.js';
+import { LibroLabLayoutContainerView } from './container.js';
+import { EditorTabView } from './editor-tab-view.js';
+import { LibroLabCurrentFileFooterView } from './footer/current-file-footer-view.js';
+import { FooterArea, LibroLabLayoutFooterView } from './footer/footer-view.js';
+import { LayoutService } from './layout-service.js';
+import { LibroLabLayoutView } from './layout.js';
+import { LibroLabLayoutMainView } from './main.js';
+import { LibroLabLayoutSlots } from './protocol.js';
+
+export const LibroLabLayoutModule = ManaModule.create('LibroLabLayoutModule').register(
+ LibroLabLayoutView,
+ LibroLabLayoutContainerView,
+ LibroLabLayoutMainView,
+ BrandView,
+ EditorTabView,
+ LibroLabLayoutFooterView,
+ LibroLabCurrentFileFooterView,
+ LayoutService,
+ createSlotPreference({
+ slot: LibroLabLayoutSlots.header,
+ view: HeaderView,
+ }),
+ createSlotPreference({
+ slot: HeaderArea.left,
+ view: BrandView,
+ }),
+ createSlotPreference({
+ slot: LibroLabLayoutSlots.container,
+ view: LibroLabLayoutContainerView,
+ }),
+ createSlotPreference({
+ slot: LibroLabLayoutSlots.main,
+ view: LibroLabLayoutMainView,
+ }),
+ createSlotPreference({
+ slot: LibroLabLayoutSlots.footer,
+ view: LibroLabLayoutFooterView,
+ }),
+ createSlotPreference({
+ slot: FooterArea.right,
+ view: FlexSlotView,
+ options: { sort: true },
+ }),
+ createSlotPreference({
+ slot: FooterArea.left,
+ view: FlexSlotView,
+ options: { sort: true },
+ }),
+ createSlotPreference({
+ slot: FooterArea.left,
+ view: LibroLabCurrentFileFooterView,
+ }),
+);
diff --git a/packages/libro-lab/src/layout/protocol.tsx b/packages/libro-lab/src/layout/protocol.tsx
new file mode 100644
index 00000000..d6c408b7
--- /dev/null
+++ b/packages/libro-lab/src/layout/protocol.tsx
@@ -0,0 +1,12 @@
+export const LibroLabLayoutSlots = {
+ header: 'libro-lab-header',
+ container: 'libro-lab-container',
+ main: 'libro-lab-main',
+ footer: 'libro-lab-footer',
+ navigator: 'libro-lab-navigator',
+ content: 'libro-lab-content',
+ contentBottom: 'libro-lab-content-bottom',
+};
+
+export type LibroLabLayoutSlotsType =
+ (typeof LibroLabLayoutSlots)[keyof typeof LibroLabLayoutSlots];
diff --git a/packages/libro-lab/src/menu/menu-bar-view.tsx b/packages/libro-lab/src/menu/menu-bar-view.tsx
new file mode 100644
index 00000000..c4cad4e2
--- /dev/null
+++ b/packages/libro-lab/src/menu/menu-bar-view.tsx
@@ -0,0 +1,28 @@
+import { MacCommandOutlined } from '@ant-design/icons';
+import {
+ BaseView,
+ MAIN_MENU_BAR,
+ MenuBarRender,
+ prop,
+ singleton,
+ view,
+} from '@difizen/mana-app';
+import { forwardRef } from 'react';
+
+export const ManaMenubarComponent = forwardRef(function GithubLinkComponent() {
+ return ;
+});
+
+@singleton()
+@view('MenuBarView')
+export class MenuBarView extends BaseView {
+ override view = ManaMenubarComponent;
+ @prop()
+ count = 0;
+ constructor() {
+ super();
+ this.title.icon = MacCommandOutlined;
+ this.title.label = '菜单';
+ this.id = 'menu-bar';
+ }
+}
diff --git a/packages/libro-lab/src/menu/menu-command.ts b/packages/libro-lab/src/menu/menu-command.ts
new file mode 100644
index 00000000..74a77473
--- /dev/null
+++ b/packages/libro-lab/src/menu/menu-command.ts
@@ -0,0 +1,138 @@
+export const MenuCommands = {
+ About: {
+ id: 'libro-lab-header-menu-help-about',
+ label: '关于',
+ },
+ OpenTerminal: {
+ id: 'libro-lab-header-menu-terminal-open',
+ label: '新建终端',
+ },
+ Save: {
+ id: 'libro-lab-header-menu-file-save',
+ label: '保存',
+ },
+ CreateFile: {
+ id: 'libro-lab-header-menu-file-create',
+ label: '新建文件',
+ },
+ RedoCellAction: {
+ id: 'libro-lab-header-menu-edit-redo-cell-action',
+ label: `恢复单元格操作`,
+ },
+ UndoCellAction: {
+ id: 'libro-lab-header-menu-edit-undo-cell-action',
+ label: `撤销单元格操作`,
+ },
+ CutCell: {
+ id: 'libro-lab-header-menu-edit-cut-cell',
+ label: `剪切单元格`,
+ },
+ CopyCell: {
+ id: 'libro-lab-header-menu-edit-copy-cell',
+ label: `复制单元格`,
+ },
+ PasteCellAbove: {
+ id: 'libro-lab-header-menu-edit-paste-cell-above',
+ label: `在上方粘贴单元格`,
+ },
+ PasteCellBelow: {
+ id: 'libro-lab-header-menu-edit-paste-cell-below',
+ label: `在下方粘贴单元格`,
+ },
+ PasteAndReplaceCell: {
+ id: 'libro-lab-header-menu-edit-paste-and-replace-cell',
+ label: `粘贴单元格并替换`,
+ },
+ DeleteCell: {
+ id: 'libro-lab-header-menu-edit-delete-cell',
+ label: `删除单元格`,
+ },
+ SelectAll: {
+ id: 'libro-lab-header-menu-edit-select-all',
+ label: `选择所有单元格`,
+ },
+ DeselectAll: {
+ id: 'libro-lab-header-menu-edit-deselect-all',
+ label: `取消选择所有单元格`,
+ },
+ MoveCellUp: {
+ id: 'libro-lab-header-menu-edit-move-cell-up',
+ label: `上移单元格`,
+ },
+ MoveCellDown: {
+ id: 'libro-lab-header-menu-edit-move-cell-down',
+ label: `下移单元格`,
+ },
+ SplitCellAntCursor: {
+ id: 'libro-lab-header-menu-edit-split-cell-at-cursor',
+ label: `切分单元格`,
+ },
+ MergeCellAbove: {
+ id: 'libro-lab-header-menu-edit-merge-cell-above',
+ label: `合并上方单元格`,
+ },
+ MergeCellBelow: {
+ id: 'libro-lab-header-menu-edit-merge-cell-below',
+ label: `合并下方单元格`,
+ },
+ MergeCells: {
+ id: 'libro-lab-header-menu-edit-merge-cells',
+ label: `合并选中单元格`,
+ },
+ ClearCellOutput: {
+ id: 'libro-lab-header-menu-edit-clear-cell-outputs',
+ label: `清空输出`,
+ },
+ ClearAllCellOutput: {
+ id: 'libro-lab-header-menu-edit-clear-all-cell-outputs',
+ label: `清空所有输出`,
+ },
+ HideOrShowCellCode: {
+ id: 'libro-lab-header-menu-view-hide-or-show-cell-code',
+ label: `隐藏/显示所选单元格代码`,
+ },
+ HideOrShowOutputs: {
+ id: 'libro-lab-header-menu-view-hide-or-show-outputs',
+ label: `隐藏/显示所选单元格输出`,
+ },
+ EnableOutputScrolling: {
+ id: 'libro-lab-header-menu-view-enable-output-scrolling',
+ label: `固定输出高度`,
+ },
+ DisableOutputScrolling: {
+ id: 'libro-lab-header-menu-view-disable-output-scrolling',
+ label: `取消固定输出高度`,
+ },
+ RestartAndRunToSelected: {
+ id: 'libro-lab-header-menu-run-restart-and-run-to-selected',
+ label: '重启并执行至选中单元格',
+ },
+ RestartRunAll: {
+ id: 'libro-lab-header-menu-run-restart-run-all',
+ label: '重启并执行全部单元格',
+ },
+ RunAllAbove: {
+ id: 'libro-lab-header-menu-run-all-above',
+ label: `执行上方所有单元格`,
+ },
+ RunAllBelow: {
+ id: 'libro-lab-header-menu-run-all-below',
+ label: `执行下方所有单元格`,
+ },
+ RunAllCells: {
+ id: 'libro-lab-header-menu-run-all-cells',
+ label: `执行全部单元格`,
+ },
+ RunCell: {
+ id: 'libro-lab-header-menu-run-cell',
+ label: `执行选中单元格`,
+ },
+ RunCellAndInsertBelow: {
+ id: 'libro-lab-header-menu-run-cell-and-insert-below',
+ label: `执行选中并向下插入一个单元格`,
+ },
+ RunCellAndSelectNext: {
+ id: 'libro-lab-header-menu-run-cell-and-select-next',
+ label: `执行并选中下一个单元格`,
+ },
+};
diff --git a/packages/libro-lab/src/menu/menu-contribution.ts b/packages/libro-lab/src/menu/menu-contribution.ts
new file mode 100644
index 00000000..2edff016
--- /dev/null
+++ b/packages/libro-lab/src/menu/menu-contribution.ts
@@ -0,0 +1,658 @@
+import type { LibroJupyterModel } from '@difizen/libro-jupyter';
+import {
+ LibroJupyterView,
+ LibroService,
+ NotebookCommands,
+} from '@difizen/libro-jupyter';
+import type { MenuRegistry } from '@difizen/mana-app';
+import {
+ CommandContribution,
+ CommandRegistry,
+ inject,
+ MAIN_MENU_BAR,
+ MenuContribution,
+ singleton,
+} from '@difizen/mana-app';
+
+import { MenuCommands } from './menu-command.js';
+
+export namespace HeaderMenus {
+ export const FILE = [...MAIN_MENU_BAR, '1_file'];
+ export const EDIT = [...MAIN_MENU_BAR, '2_edit'];
+ export const VIEW = [...MAIN_MENU_BAR, '3_view'];
+ export const RUN = [...MAIN_MENU_BAR, '4_run'];
+ export const TERMINAL = [...MAIN_MENU_BAR, '5_terminal'];
+ export const HELP = [...MAIN_MENU_BAR, '6_help'];
+}
+
+@singleton({ contrib: [MenuContribution, CommandContribution] })
+export class HeaderMenu implements MenuContribution, CommandContribution {
+ @inject(CommandRegistry) protected commandRegistry: CommandRegistry;
+ @inject(LibroService) protected libroService: LibroService;
+
+ registerMenus(menu: MenuRegistry) {
+ menu.registerSubmenu(HeaderMenus.FILE, { label: '文件' });
+ menu.registerSubmenu(HeaderMenus.EDIT, { label: '编辑' });
+ menu.registerSubmenu(HeaderMenus.VIEW, { label: '视图' });
+ menu.registerSubmenu(HeaderMenus.RUN, { label: '运行' });
+ menu.registerSubmenu(HeaderMenus.TERMINAL, { label: '终端' });
+ menu.registerSubmenu(HeaderMenus.HELP, { label: '帮助' });
+ menu.registerMenuAction(HeaderMenus.TERMINAL, {
+ id: MenuCommands.OpenTerminal.id,
+ command: MenuCommands.OpenTerminal.id,
+ label: MenuCommands.OpenTerminal.label,
+ });
+ menu.registerMenuAction(HeaderMenus.HELP, {
+ id: MenuCommands.About.id,
+ command: MenuCommands.About.id,
+ label: MenuCommands.About.label,
+ });
+ menu.registerMenuAction(HeaderMenus.FILE, {
+ id: MenuCommands.Save.id,
+ command: MenuCommands.Save.id,
+ label: MenuCommands.Save.label,
+ });
+ menu.registerMenuAction(HeaderMenus.FILE, {
+ id: MenuCommands.CreateFile.id,
+ command: MenuCommands.CreateFile.id,
+ label: MenuCommands.CreateFile.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.UndoCellAction.id,
+ command: MenuCommands.UndoCellAction.id,
+ label: MenuCommands.UndoCellAction.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.RedoCellAction.id,
+ command: MenuCommands.RedoCellAction.id,
+ label: MenuCommands.RedoCellAction.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.CutCell.id,
+ command: MenuCommands.CutCell.id,
+ label: MenuCommands.CutCell.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.CopyCell.id,
+ command: MenuCommands.CopyCell.id,
+ label: MenuCommands.CopyCell.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.PasteCellBelow.id,
+ command: MenuCommands.PasteCellBelow.id,
+ label: MenuCommands.PasteCellBelow.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.PasteCellAbove.id,
+ command: MenuCommands.PasteCellAbove.id,
+ label: MenuCommands.PasteCellAbove.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.PasteAndReplaceCell.id,
+ command: MenuCommands.PasteAndReplaceCell.id,
+ label: MenuCommands.PasteAndReplaceCell.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.DeleteCell.id,
+ command: MenuCommands.DeleteCell.id,
+ label: MenuCommands.DeleteCell.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.SelectAll.id,
+ command: MenuCommands.SelectAll.id,
+ label: MenuCommands.SelectAll.label,
+ });
+ // menu.registerMenuAction(HeaderMenus.EDIT, {
+ // id: MenuCommands.DeselectAll.id,
+ // command: MenuCommands.DeselectAll.id,
+ // label: MenuCommands.DeselectAll.label,
+ // });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.MoveCellUp.id,
+ command: MenuCommands.MoveCellUp.id,
+ label: MenuCommands.MoveCellUp.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.MoveCellDown.id,
+ command: MenuCommands.MoveCellDown.id,
+ label: MenuCommands.MoveCellDown.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.SplitCellAntCursor.id,
+ command: MenuCommands.SplitCellAntCursor.id,
+ label: MenuCommands.SplitCellAntCursor.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.MergeCells.id,
+ command: MenuCommands.MergeCells.id,
+ label: MenuCommands.MergeCells.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.MergeCellAbove.id,
+ command: MenuCommands.MergeCellAbove.id,
+ label: MenuCommands.MergeCellAbove.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.MergeCellBelow.id,
+ command: MenuCommands.MergeCellBelow.id,
+ label: MenuCommands.MergeCellBelow.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.ClearCellOutput.id,
+ command: MenuCommands.ClearCellOutput.id,
+ label: MenuCommands.ClearCellOutput.label,
+ });
+ menu.registerMenuAction(HeaderMenus.EDIT, {
+ id: MenuCommands.ClearAllCellOutput.id,
+ command: MenuCommands.ClearAllCellOutput.id,
+ label: MenuCommands.ClearAllCellOutput.label,
+ });
+ menu.registerMenuAction(HeaderMenus.VIEW, {
+ id: MenuCommands.HideOrShowCellCode.id,
+ command: MenuCommands.HideOrShowCellCode.id,
+ label: MenuCommands.HideOrShowCellCode.label,
+ });
+ menu.registerMenuAction(HeaderMenus.VIEW, {
+ id: MenuCommands.HideOrShowOutputs.id,
+ command: MenuCommands.HideOrShowOutputs.id,
+ label: MenuCommands.HideOrShowOutputs.label,
+ });
+ menu.registerMenuAction(HeaderMenus.VIEW, {
+ id: MenuCommands.EnableOutputScrolling.id,
+ command: MenuCommands.EnableOutputScrolling.id,
+ label: MenuCommands.EnableOutputScrolling.label,
+ });
+ menu.registerMenuAction(HeaderMenus.VIEW, {
+ id: MenuCommands.DisableOutputScrolling.id,
+ command: MenuCommands.DisableOutputScrolling.id,
+ label: MenuCommands.DisableOutputScrolling.label,
+ });
+ menu.registerMenuAction(HeaderMenus.RUN, {
+ id: MenuCommands.RunCell.id,
+ command: MenuCommands.RunCell.id,
+ label: MenuCommands.RunCell.label,
+ });
+ menu.registerMenuAction(HeaderMenus.RUN, {
+ id: MenuCommands.RunAllAbove.id,
+ command: MenuCommands.RunAllAbove.id,
+ label: MenuCommands.RunAllAbove.label,
+ });
+ menu.registerMenuAction(HeaderMenus.RUN, {
+ id: MenuCommands.RunAllBelow.id,
+ command: MenuCommands.RunAllBelow.id,
+ label: MenuCommands.RunAllBelow.label,
+ });
+ menu.registerMenuAction(HeaderMenus.RUN, {
+ id: MenuCommands.RunAllCells.id,
+ command: MenuCommands.RunAllCells.id,
+ label: MenuCommands.RunAllCells.label,
+ });
+ menu.registerMenuAction(HeaderMenus.RUN, {
+ id: MenuCommands.RunCellAndSelectNext.id,
+ command: MenuCommands.RunCellAndSelectNext.id,
+ label: MenuCommands.RunCellAndSelectNext.label,
+ });
+ menu.registerMenuAction(HeaderMenus.RUN, {
+ id: MenuCommands.RunCellAndInsertBelow.id,
+ command: MenuCommands.RunCellAndInsertBelow.id,
+ label: MenuCommands.RunCellAndInsertBelow.label,
+ });
+ menu.registerMenuAction(HeaderMenus.RUN, {
+ id: MenuCommands.RestartRunAll.id,
+ command: MenuCommands.RestartRunAll.id,
+ label: MenuCommands.RestartRunAll.label,
+ });
+ menu.registerMenuAction(HeaderMenus.RUN, {
+ id: MenuCommands.RestartAndRunToSelected.id,
+ command: MenuCommands.RestartAndRunToSelected.id,
+ label: MenuCommands.RestartAndRunToSelected.label,
+ });
+ }
+ registerCommands(commands: CommandRegistry) {
+ commands.registerCommand(MenuCommands.OpenTerminal, {
+ execute: () => {
+ //TODO: 增加终端
+ },
+ });
+ commands.registerCommand(MenuCommands.About, {
+ execute: async () => {
+ //TODO: 关于
+ },
+ });
+ commands.registerCommand(MenuCommands.Save, {
+ execute: async () => {
+ //TODO: 保存
+ },
+ });
+ commands.registerCommand(MenuCommands.UndoCellAction, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['UndoCellAction'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.RedoCellAction, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['RedoCellAction'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.CutCell, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['CutCell'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.CopyCell, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['CopyCell'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.DeleteCell, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['DeleteCell'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.PasteCellBelow, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['PasteCellBelow'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.PasteCellAbove, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['PasteCellAbove'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.PasteAndReplaceCell, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['PasteAndReplaceCell'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.SelectAll, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['SelectAll'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ // commands.registerCommand(MenuCommands.DeselectAll, {
+ // execute: async () => {
+ // if (this.libroService.active)
+ // this.commandRegistry.executeCommand(
+ // NotebookCommands.DeselectAll.id,
+ // this.libroService.active.activeCell,
+ // this.libroService.active,
+ // );
+ // },
+ // });
+ commands.registerCommand(MenuCommands.MoveCellUp, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['MoveCellUp'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.MoveCellDown, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['MoveCellDown'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.SplitCellAntCursor, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['SplitCellAntCursor'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.MergeCells, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['MergeCells'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.MergeCellAbove, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['MergeCellAbove'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.MergeCellBelow, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['MergeCellBelow'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.ClearCellOutput, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['ClearCellOutput'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.ClearAllCellOutput, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['ClearAllCellOutput'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.HideOrShowCellCode, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['HideOrShowCellCode'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.HideOrShowOutputs, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['HideOrShowOutputs'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.EnableOutputScrolling, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['EnableOutputScrolling'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommand(MenuCommands.DisableOutputScrolling, {
+ execute: async () => {
+ if (this.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['DisableOutputScrolling'].id,
+ this.libroService.active.activeCell,
+ this.libroService.active,
+ );
+ }
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RunCell, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ ctx.commandRegistry.executeCommand(
+ NotebookCommands['RunCell'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RunAllAbove, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ ctx.commandRegistry.executeCommand(
+ NotebookCommands['RunAllAbove'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RunAllBelow, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ ctx.commandRegistry.executeCommand(
+ NotebookCommands['RunAllBelow'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RunAllCells, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ ctx.commandRegistry.executeCommand(
+ NotebookCommands['RunAllCells'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RunCellAndInsertBelow, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ ctx.commandRegistry.executeCommand(
+ NotebookCommands['RunCellAndInsertBelow'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RunCellAndSelectNext, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ ctx.commandRegistry.executeCommand(
+ NotebookCommands['RunCellAndSelectNext'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RestartRunAll, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['RestartRunAll'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RestartRunAll, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ ctx.commandRegistry.executeCommand(
+ NotebookCommands['RestartRunAll'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ commands.registerCommandWithContext(MenuCommands.RestartAndRunToSelected, this, {
+ execute: async (ctx) => {
+ if (ctx.libroService.active) {
+ this.commandRegistry.executeCommand(
+ NotebookCommands['RestartAndRunToSelected'].id,
+ ctx.libroService.active.activeCell,
+ ctx.libroService.active,
+ );
+ }
+ },
+ isEnabled: (ctx) => {
+ const libro = ctx.libroService.active;
+ if (!libro || !(libro instanceof LibroJupyterView)) {
+ return false;
+ }
+ return (
+ (libro.model as LibroJupyterModel).kernelConnection !== undefined &&
+ (libro.model as LibroJupyterModel).kernelConnecting === false
+ );
+ },
+ });
+ }
+}
diff --git a/packages/libro-lab/src/menu/module.ts b/packages/libro-lab/src/menu/module.ts
new file mode 100644
index 00000000..c3534dc0
--- /dev/null
+++ b/packages/libro-lab/src/menu/module.ts
@@ -0,0 +1,13 @@
+import { createSlotPreference, HeaderArea, ManaModule } from '@difizen/mana-app';
+
+import { MenuBarView } from './menu-bar-view.js';
+import { HeaderMenu } from './menu-contribution.js';
+
+export const LibroLabHeaderMenuModule = ManaModule.create().register(
+ HeaderMenu,
+ MenuBarView,
+ createSlotPreference({
+ slot: HeaderArea.middle,
+ view: MenuBarView,
+ }),
+);
diff --git a/packages/libro-lab/src/module.tsx b/packages/libro-lab/src/module.tsx
index ca0fce16..e4241136 100644
--- a/packages/libro-lab/src/module.tsx
+++ b/packages/libro-lab/src/module.tsx
@@ -1,43 +1,88 @@
+import { FileView, LibroJupyterModule } from '@difizen/libro-jupyter';
import {
ManaModule,
createSlotPreference,
RootSlotId,
- CardTabView,
SideTabView,
createViewPreference,
- HeaderView,
+ HeaderArea,
} from '@difizen/mana-app';
-import { FileTreeView } from '@difizen/mana-app';
+import { GithubLinkView } from './github-link/index.js';
+import { KernelManagerView } from './kernel-manager/index.js';
import { LibroLabApp } from './lab-app.js';
+import { EditorTabView } from './layout/editor-tab-view.js';
import {
+ LibroLabLayoutModule,
+ LibroLabLayoutSlots,
LibroLabLayoutView,
- LibroLabSlots,
- LibroLabContentSlots,
} from './layout/index.js';
+import './index.less';
+import { LibroLabHeaderMenuModule } from './menu/module.js';
+import { LibroLabTocModule } from './toc/module.js';
+import { WelcomeView } from './welcome/index.js';
-export const LibroLabModule = ManaModule.create().register(
- LibroLabApp,
- LibroLabLayoutView,
- createSlotPreference({
- view: LibroLabLayoutView,
- slot: RootSlotId,
- }),
- createSlotPreference({
- slot: LibroLabSlots.top,
- view: HeaderView,
- }),
- createSlotPreference({
- view: CardTabView,
- slot: LibroLabContentSlots.main,
- }),
- createSlotPreference({
- view: SideTabView,
- slot: LibroLabContentSlots.left,
- }),
- createViewPreference({
- view: FileTreeView,
- slot: LibroLabContentSlots.left,
- autoCreate: true,
- }),
-);
+export const LibroLabModule = ManaModule.create()
+ .register(
+ LibroLabApp,
+ LibroLabLayoutView,
+ GithubLinkView,
+ createViewPreference({
+ view: GithubLinkView,
+ slot: HeaderArea.right,
+ openOptions: {
+ order: 'github',
+ },
+ autoCreate: true,
+ }),
+ KernelManagerView,
+ createViewPreference({
+ view: KernelManagerView,
+ slot: LibroLabLayoutSlots.navigator,
+ openOptions: {
+ reveal: false,
+ order: 'kernel-manager',
+ },
+ autoCreate: true,
+ }),
+ createSlotPreference({
+ view: LibroLabLayoutView,
+ slot: RootSlotId,
+ }),
+ createSlotPreference({
+ view: EditorTabView,
+ slot: LibroLabLayoutSlots.content,
+ }),
+ createSlotPreference({
+ view: SideTabView,
+ slot: LibroLabLayoutSlots.navigator,
+ options: {
+ sort: true,
+ },
+ }),
+ createViewPreference({
+ view: FileView,
+ slot: LibroLabLayoutSlots.navigator,
+ autoCreate: true,
+ openOptions: {
+ reveal: true,
+ order: 'file-tree',
+ },
+ }),
+ WelcomeView,
+ createViewPreference({
+ view: WelcomeView,
+ slot: LibroLabLayoutSlots.content,
+ autoCreate: true,
+ openOptions: {
+ reveal: true,
+ order: 'welcome',
+ },
+ }),
+ )
+ .dependOn(
+ LibroJupyterModule,
+ LibroLabLayoutModule,
+ LibroLabHeaderMenuModule,
+ LibroLabTocModule,
+ );
diff --git a/packages/libro-lab/src/toc/index.less b/packages/libro-lab/src/toc/index.less
new file mode 100644
index 00000000..4f8fa4fc
--- /dev/null
+++ b/packages/libro-lab/src/toc/index.less
@@ -0,0 +1,25 @@
+.libro-lab-toc-panel {
+ height: 100%;
+
+ .markdown-toc-container-title {
+ display: none;
+ }
+
+ .mana-tab-side-pane-content {
+ height: calc(100% - 32px);
+ }
+
+ .markdown-toc-container {
+ width: unset;
+ padding: 12px;
+ }
+}
+
+.libro-lab-toc-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ margin: unset;
+}
diff --git a/packages/libro-lab/src/toc/libro-toc-icons.tsx b/packages/libro-lab/src/toc/libro-toc-icons.tsx
new file mode 100644
index 00000000..2e2e1772
--- /dev/null
+++ b/packages/libro-lab/src/toc/libro-toc-icons.tsx
@@ -0,0 +1,35 @@
+export interface IProps {
+ className?: string;
+ width?: string;
+ height?: string;
+}
+export function TocIcon(props: IProps) {
+ return (
+
+ );
+}
diff --git a/packages/libro-lab/src/toc/libro-toc-panel-view.tsx b/packages/libro-lab/src/toc/libro-toc-panel-view.tsx
new file mode 100644
index 00000000..7ea4bbe7
--- /dev/null
+++ b/packages/libro-lab/src/toc/libro-toc-panel-view.tsx
@@ -0,0 +1,87 @@
+import { LibroNavigatableView } from '@difizen/libro-jupyter';
+import { TOCView } from '@difizen/libro-toc';
+import {
+ BaseView,
+ inject,
+ prop,
+ singleton,
+ useInject,
+ view,
+ ViewInstance,
+ ViewManager,
+ ViewRender,
+} from '@difizen/mana-app';
+import { Empty } from 'antd';
+
+import { LayoutService } from '../layout/layout-service.js';
+import { LibroLabLayoutSlots } from '../layout/protocol.js';
+
+import { TocIcon } from './libro-toc-icons.js';
+import './index.less';
+
+const TocViewRender: React.FC = () => {
+ const tocPanelView = useInject(ViewInstance);
+ return (
+
+ {tocPanelView.libroTocView ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export const TocViewFactoryId = 'libro-lab-toc-view';
+@singleton()
+@view(TocViewFactoryId)
+export class TocPanelView extends BaseView {
+ override view = TocViewRender;
+ @inject(ViewManager) protected viewManager: ViewManager;
+ @inject(LayoutService) protected layoutService: LayoutService;
+ @prop() libroTocView: TOCView | undefined;
+
+ constructor() {
+ super();
+ this.title.icon = ;
+ this.title.label = '大纲';
+ }
+
+ override onViewMount(): void {
+ this.handleEditTabChange();
+ this.layoutService.onSlotActiveChange(
+ LibroLabLayoutSlots.content,
+ this.handleEditTabChange,
+ );
+ }
+
+ get libroNavigatableView() {
+ const contentView = this.layoutService.getActiveView(LibroLabLayoutSlots.content);
+ if (contentView instanceof LibroNavigatableView) {
+ return contentView;
+ }
+ return undefined;
+ }
+
+ handleEditTabChange = () => {
+ if (!this.libroNavigatableView) {
+ return;
+ }
+ this.viewManager
+ .getOrCreateView(TOCView, {
+ id: this.libroNavigatableView.filePath,
+ })
+ .then((libroTocView) => {
+ this.libroTocView = libroTocView;
+ this.libroTocView.parent = this.libroNavigatableView?.libroView;
+ return;
+ })
+ .catch(() => {
+ //
+ });
+ };
+}
diff --git a/packages/libro-lab/src/toc/module.ts b/packages/libro-lab/src/toc/module.ts
new file mode 100644
index 00000000..f66eb1e3
--- /dev/null
+++ b/packages/libro-lab/src/toc/module.ts
@@ -0,0 +1,21 @@
+import { LibroTOCModule } from '@difizen/libro-toc';
+import { createViewPreference, ManaModule } from '@difizen/mana-app';
+
+import { LibroLabLayoutSlots } from '../layout/protocol.js';
+
+import { TocPanelView } from './libro-toc-panel-view.js';
+
+export const LibroLabTocModule = ManaModule.create()
+ .register(
+ TocPanelView,
+ createViewPreference({
+ view: TocPanelView,
+ slot: LibroLabLayoutSlots.navigator,
+ autoCreate: true,
+ openOptions: {
+ reveal: true,
+ order: 'toc',
+ },
+ }),
+ )
+ .dependOn(LibroTOCModule);
diff --git a/packages/libro-lab/src/welcome/index.less b/packages/libro-lab/src/welcome/index.less
new file mode 100644
index 00000000..8d698503
--- /dev/null
+++ b/packages/libro-lab/src/welcome/index.less
@@ -0,0 +1,21 @@
+.libro-lab-welcome-page {
+ padding: 32px 54px;
+ height: 100%;
+ background: white;
+
+ .libro-lab-welcome-page-title {
+ /* stylelint-disable-next-line declaration-property-value-disallowed-list */
+ font-family: PingFangSC;
+ font-weight: 600;
+ font-size: 40px;
+ color: #000a1a;
+ letter-spacing: 0;
+ line-height: 56px;
+ }
+
+ .libro-lab-welcome-page-title-tip {
+ margin: 16px 0 30px;
+ font-size: 16px;
+ color: rgba(0, 10, 26, 78%);
+ }
+}
diff --git a/packages/libro-lab/src/welcome/index.tsx b/packages/libro-lab/src/welcome/index.tsx
new file mode 100644
index 00000000..efe3f4ea
--- /dev/null
+++ b/packages/libro-lab/src/welcome/index.tsx
@@ -0,0 +1,30 @@
+import { singleton, view } from '@difizen/mana-app';
+import { BaseView } from '@difizen/mana-app';
+import { forwardRef } from 'react';
+
+import { WelcomeIcon } from './welcome-icon.js';
+
+import './index.less';
+
+export const WelcomeComponent = forwardRef(function WelcomeComponent() {
+ return (
+
+
欢迎使用 Notebook 工作台 🎉🎉
+
+ 👋 你好,服务正在加载中,请稍后开启你的研发之旅吧~
+
+
+ );
+});
+
+@singleton()
+@view('welcome-view')
+export class WelcomeView extends BaseView {
+ override view = WelcomeComponent;
+
+ constructor() {
+ super();
+ this.title.icon = ;
+ this.title.label = '欢迎使用';
+ }
+}
diff --git a/packages/libro-lab/src/welcome/welcome-icon.tsx b/packages/libro-lab/src/welcome/welcome-icon.tsx
new file mode 100644
index 00000000..347e3dce
--- /dev/null
+++ b/packages/libro-lab/src/welcome/welcome-icon.tsx
@@ -0,0 +1,64 @@
+export interface IProps {
+ className?: string;
+}
+export function WelcomeIcon(props: IProps) {
+ return (
+
+ );
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index c27546ce..cd000633 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -16,7 +16,7 @@
"noFallthroughCasesInSwitch": false,
// "noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
- "noUnusedParameters": true,
+ "noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": true,
"noImplicitOverride": true,
"skipLibCheck": true,