initial commit

This commit is contained in:
Fedor Katurov 2023-04-24 19:13:40 +06:00
commit 9a4eb6ef58
23 changed files with 2490 additions and 0 deletions

14
.eslintrc.cjs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
);

View file

@ -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 };
};

View 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"
/>
);
};

View file

@ -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;
};

View file

@ -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 />;
};

View file

@ -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%;
}
}

View file

@ -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];
};

View file

@ -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>
);
};

View file

@ -0,0 +1,6 @@
.editor {
height: 100%;
padding: 16px;
overflow: scroll;
box-sizing: border-box;
}

View file

@ -0,0 +1,4 @@
export interface GridLayoutComponentProps {
id: string;
title: string;
}

View 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
View 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
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

30
tsconfig.json Normal file
View 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
View 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
View 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") }],
},
});

2068
yarn.lock Normal file

File diff suppressed because it is too large Load diff