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