mirror of
https://github.com/muerwre/markdown-home-tab.git
synced 2025-04-24 16:36:41 +07:00
initial commit
This commit is contained in:
commit
9a4eb6ef58
23 changed files with 2490 additions and 0 deletions
14
.eslintrc.cjs
Normal file
14
.eslintrc.cjs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'warn',
|
||||||
|
},
|
||||||
|
}
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
6
README.md
Normal file
6
README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Markdown Extension
|
||||||
|
|
||||||
|
I'm trying to create markdown new tab extension with [Milkdown](https://milkdown.dev/) and
|
||||||
|
[Dockview](https://dockview.dev).
|
||||||
|
|
||||||
|
Now its only experiment, but it already handles grid layout and data persistance.
|
13
index.html
Normal file
13
index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
41
package.json
Normal file
41
package.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "markdown-home-tab",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@milkdown/core": "^7.2.1",
|
||||||
|
"@milkdown/ctx": "^7.2.1",
|
||||||
|
"@milkdown/plugin-clipboard": "^7.2.1",
|
||||||
|
"@milkdown/plugin-listener": "^7.2.1",
|
||||||
|
"@milkdown/preset-commonmark": "^7.2.1",
|
||||||
|
"@milkdown/prose": "^7.2.1",
|
||||||
|
"@milkdown/react": "^7.2.1",
|
||||||
|
"@milkdown/theme-nord": "^7.2.1",
|
||||||
|
"@milkdown/transformer": "^7.2.1",
|
||||||
|
"dockview": "^1.7.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"sass": "^1.62.0",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||||
|
"@typescript-eslint/parser": "^5.57.1",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.3.0"
|
||||||
|
}
|
||||||
|
}
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
11
src/main.tsx
Normal file
11
src/main.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { Editor } from "~/pages/editor";
|
||||||
|
|
||||||
|
import "./styles/main.scss";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Editor />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { DockviewApi, DockviewReadyEvent } from "dockview";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { createDefaultLayout } from "../utils/createDefaultLayout";
|
||||||
|
|
||||||
|
export const useGridLayoutPersistance = () => {
|
||||||
|
const api = useRef<DockviewApi>();
|
||||||
|
|
||||||
|
const onReady = (event: DockviewReadyEvent) => {
|
||||||
|
api.current = event.api;
|
||||||
|
|
||||||
|
const layoutString = localStorage.getItem("dockview_persistance_layout");
|
||||||
|
|
||||||
|
if (!layoutString) {
|
||||||
|
createDefaultLayout(event.api);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const layout = JSON.parse(layoutString);
|
||||||
|
event.api.fromJSON(layout);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createDefaultLayout(event.api);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const disposable = api.current.onDidLayoutChange(() => {
|
||||||
|
if (!api.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!api.current.groups.length) {
|
||||||
|
createDefaultLayout(api.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = api.current.toJSON();
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
"dockview_persistance_layout",
|
||||||
|
JSON.stringify(layout)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposable.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { api, onReady };
|
||||||
|
};
|
32
src/modules/layout/components/GridLayout/index.tsx
Normal file
32
src/modules/layout/components/GridLayout/index.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { DockviewReact, IDockviewPanelProps } from "dockview";
|
||||||
|
import { useGridLayoutPersistance } from "./hooks/useGridLayoutPersistance";
|
||||||
|
import { FC, createElement, useMemo } from "react";
|
||||||
|
import { GridLayoutComponentProps } from "../../types";
|
||||||
|
|
||||||
|
export interface GridLayoutProps {
|
||||||
|
component: FC<GridLayoutComponentProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GridLayout: FC<GridLayoutProps> = ({ component }) => {
|
||||||
|
const { onReady } = useGridLayoutPersistance();
|
||||||
|
|
||||||
|
const components = useMemo(
|
||||||
|
() => ({
|
||||||
|
default: (props: IDockviewPanelProps<{ title: string }>) => {
|
||||||
|
return createElement(component, {
|
||||||
|
id: props.api.id,
|
||||||
|
title: props.params.title,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[component]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DockviewReact
|
||||||
|
components={components}
|
||||||
|
onReady={onReady}
|
||||||
|
className="dockview-theme-abyss"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { DockviewApi } from "dockview";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
|
export const createDefaultLayout = (api: DockviewApi) => {
|
||||||
|
api.addPanel({
|
||||||
|
id: v4(),
|
||||||
|
component: "default",
|
||||||
|
title: "New editor",
|
||||||
|
params: {
|
||||||
|
title: "Panel 1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// panel.group.locked = true;
|
||||||
|
// panel.group.header.hidden = true;
|
||||||
|
};
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { FC } from "react";
|
||||||
|
import {
|
||||||
|
Editor,
|
||||||
|
rootCtx,
|
||||||
|
defaultValueCtx,
|
||||||
|
editorViewOptionsCtx,
|
||||||
|
} from "@milkdown/core";
|
||||||
|
import { Milkdown, useEditor } from "@milkdown/react";
|
||||||
|
import { commonmark } from "@milkdown/preset-commonmark";
|
||||||
|
import { listener, listenerCtx } from "@milkdown/plugin-listener";
|
||||||
|
import { clipboard } from "@milkdown/plugin-clipboard";
|
||||||
|
|
||||||
|
import styles from "./styles.module.scss";
|
||||||
|
|
||||||
|
interface MarkdownEditorProps {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (val: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarkdownEditor: FC<MarkdownEditorProps> = ({
|
||||||
|
value = "",
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
useEditor((root) =>
|
||||||
|
Editor.make()
|
||||||
|
.config((ctx) => {
|
||||||
|
ctx.set(rootCtx, root);
|
||||||
|
ctx.set(defaultValueCtx, value);
|
||||||
|
ctx.get(listenerCtx).markdownUpdated((_, markdown) => {
|
||||||
|
onChange?.(markdown);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.update(editorViewOptionsCtx, (prev) => ({
|
||||||
|
...prev,
|
||||||
|
attributes: {
|
||||||
|
class: styles.editor,
|
||||||
|
spellcheck: "false",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.use(commonmark)
|
||||||
|
.use(listener)
|
||||||
|
.use(clipboard)
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Milkdown />;
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
.editor {
|
||||||
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& > :first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :last-child {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div[data-milkdown-root="true"] {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
:global(.milkdown) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const prefix = "VALUE__";
|
||||||
|
|
||||||
|
const safelyGetStringValue = (key: string) => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key) ?? "";
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const safelySetStringValue = (key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
return localStorage.setItem(key, value);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePersistedValue = (
|
||||||
|
id: string
|
||||||
|
): [string, (val: string) => void] => {
|
||||||
|
const key = `${prefix}${id}`;
|
||||||
|
const [value, setValue] = useState(safelyGetStringValue(key));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(safelyGetStringValue(key));
|
||||||
|
}, [id, key]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
safelySetStringValue(key, value);
|
||||||
|
}, [key, value]);
|
||||||
|
|
||||||
|
return [value, setValue];
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { MilkdownProvider } from "@milkdown/react";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { MarkdownEditor } from "../../components/MarkdownEditor";
|
||||||
|
import styles from "./styles.module.scss";
|
||||||
|
import { usePersistedValue } from "./hooks/usePersistedValue";
|
||||||
|
|
||||||
|
interface MarkdownEditorContainerProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarkdownEditorContainer: FC<MarkdownEditorContainerProps> = ({
|
||||||
|
id,
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = usePersistedValue(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.editor}>
|
||||||
|
<MilkdownProvider>
|
||||||
|
<MarkdownEditor value={value} onChange={setValue} />
|
||||||
|
</MilkdownProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
.editor {
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: scroll;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
4
src/modules/layout/types/index.ts
Normal file
4
src/modules/layout/types/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export interface GridLayoutComponentProps {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
7
src/pages/editor/index.tsx
Normal file
7
src/pages/editor/index.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { FC } from "react";
|
||||||
|
import { GridLayout } from "~/modules/layout/components/GridLayout";
|
||||||
|
import { MarkdownEditorContainer } from "~/modules/layout/editor/containers/MarkdownEditorContainer/index";
|
||||||
|
|
||||||
|
const Editor: FC = () => <GridLayout component={MarkdownEditorContainer} />;
|
||||||
|
|
||||||
|
export { Editor };
|
12
src/styles/main.scss
Normal file
12
src/styles/main.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@import "dockview/dist/styles/dockview.css";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Path aliases */
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
11
vite.config.ts
Normal file
11
vite.config.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: [{ find: "~", replacement: path.resolve("src") }],
|
||||||
|
},
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue