feat: initial react support

This commit is contained in:
Morten Olsen
2024-12-10 22:40:29 +01:00
parent 7bebe30bf7
commit d833dc5643
42 changed files with 557 additions and 15 deletions

View File

@@ -0,0 +1,7 @@
{
"name": "@plainidx/configs",
"version": "1.0.0",
"files": [
"tsconfig.json"
]
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
}
}

View File

@@ -10,6 +10,7 @@
"build": "tsc --build"
},
"dependencies": {
"@plainidx/configs": "workspace:*",
"@plainidx/plainidx": "workspace:*"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},

View File

@@ -10,6 +10,7 @@
"build": "tsc --build"
},
"dependencies": {
"@plainidx/configs": "workspace:*",
"@plainidx/plainidx": "workspace:*",
"glob": "^11.0.0"
},

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},

View File

@@ -10,7 +10,9 @@
"build": "tsc --build"
},
"devDependencies": {
"@plainidx/configs": "workspace:*",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"typescript": "^5.7.2"
},
"dependencies": {

View File

@@ -0,0 +1,39 @@
import { EventEmitter } from '../utils/eventemitter.js';
type EditorPanel = {
name: string;
icon: string;
component: React.ComponentType;
};
type EditorPanelsEvents = {
change: () => void;
};
class EditorPanels extends EventEmitter<EditorPanelsEvents> {
#panels: Map<string, EditorPanel>;
constructor() {
super();
this.#panels = new Map();
}
public get panels() {
return this.#panels;
}
public add(id: string, panel: EditorPanel) {
this.#panels.set(id, panel);
this.emit('change');
return () => {
this.#panels.delete(id);
this.emit('change');
};
}
public get(id: string) {
return this.#panels.get(id);
}
}
export { EditorPanels };

View File

@@ -0,0 +1,32 @@
import { Document } from '../documents/documents.document.js';
import { EventEmitter } from '../utils/eventemitter.js';
type EditorRender = {
supports: (document: Document) => boolean;
name: string;
component: React.ComponentType;
};
type EditorRendersEvents = {
change: () => void;
};
class EditorRenders extends EventEmitter<EditorRendersEvents> {
#renders: EditorRender[] = [];
public add = (render: EditorRender) => {
this.#renders.push(render);
this.emit('change');
return () => {
this.#renders = this.#renders.filter((r) => r !== render);
this.emit('change');
};
};
public getByDocument = (document: Document) => {
return this.#renders.filter((r) => r.supports(document));
};
}
export { EditorRenders };

View File

@@ -0,0 +1,33 @@
import { EventEmitter } from '../utils/eventemitter.js';
import { EditorPanels } from './editor.panels.js';
import { EditorRenders } from './editor.renders.js';
import { EditorWorkspace } from './editor.workspace.js';
type EditorEvents = Record<string, never>;
class Editor extends EventEmitter<EditorEvents> {
#workspace: EditorWorkspace;
#renders: EditorRenders;
#panels: EditorPanels;
constructor() {
super();
this.#workspace = new EditorWorkspace();
this.#renders = new EditorRenders();
this.#panels = new EditorPanels();
}
public get workspace() {
return this.#workspace;
}
public get renders() {
return this.#renders;
}
public get panels() {
return this.#panels;
}
}
export { Editor };

View File

@@ -0,0 +1,26 @@
import { Document } from '../documents/documents.document.js';
import { EventEmitter } from '../utils/eventemitter.js';
type WorkspaceEvents = {
change: () => void;
};
class EditorWorkspace extends EventEmitter<WorkspaceEvents> {
#documents: Set<Document> = new Set<Document>();
public get documents() {
return [...this.#documents];
}
public openDocument(document: Document) {
this.#documents.add(document);
this.emit('change');
}
public closeDocument(document: Document) {
this.#documents.delete(document);
this.emit('change');
}
}
export { EditorWorkspace };

View File

@@ -8,3 +8,5 @@ export { Plugin } from './plugins/plugin/plugin.js';
export { Databases, DatabaseMigration } from './databases/databases.js';
export * from 'zod';
export { type Knex as Database } from 'knex';
export { Editor } from './editor/editor.js';
export { EditorWorkspace } from './editor/editor.workspace.js';

View File

@@ -4,6 +4,7 @@ import { EventEmitter } from '../../utils/eventemitter.js';
import { Plugins } from '../plugins.js';
import { z, ZodSchema } from 'zod';
import { PluginActionApi } from './plugin.api.js';
import { Editor } from '../../editor/editor.js';
type PluginOptions<TLocalConfig extends ZodSchema = ZodSchema, TSharedConfig extends ZodSchema = ZodSchema> = {
plugins: Plugins;
@@ -64,6 +65,7 @@ abstract class Plugin<
public onUnload?: () => Promise<void>;
public onLoaded?: () => Promise<void>;
public process?: (document: Document) => Promise<void>;
public setupUI?: (editor: Editor) => Promise<void>;
/*public getPlugin = async <T extends Plugin>(plugin: new (...args: any) => T): Promise<
T['api'] extends (...args: any[]) => infer R ? R : never
@@ -81,8 +83,8 @@ abstract class Plugin<
: undefined,
): Promise<
Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined> extends ZodSchema
? z.infer<Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined>>
: undefined
? z.infer<Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined>>
: undefined
> => {
const { plugins } = this.#options;
const instance = await plugins.get(plugin);

View File

@@ -3,6 +3,7 @@ import { Documents } from '../documents/documents.js';
import { Databases } from '../databases/databases.js';
import { Plugin, PluginConstructor } from './plugin/plugin.js';
import { z, ZodSchema } from 'zod';
import { Editor } from '../editor/editor.js';
type PluginsOptions = {
documents: Documents;
@@ -36,6 +37,12 @@ class Plugins {
await document.save();
};
public setupUI = (editor: Editor) => {
for (const plugin of this.#plugins.values()) {
plugin.setupUI?.(editor);
}
};
public get = async <T extends Plugin>(plugin: PluginConstructor<any, any, T>): Promise<T> => {
if (!this.#plugins.has(plugin)) {
await this.add([plugin]);
@@ -88,8 +95,8 @@ class Plugins {
: undefined,
): Promise<
Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined> extends ZodSchema
? z.infer<Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined>>
: undefined
? z.infer<Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined>>
: undefined
> => {
const instance = await this.get(plugin);
const { actions } = instance;

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},

2
packages/platform-react/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/node_modules/
/dist/

View File

@@ -0,0 +1,21 @@
{
"name": "@plainidx/react",
"version": "1.0.0",
"type": "module",
"main": "dist/exports.js",
"files": [
"dist"
],
"scripts": {
"build": "tsc --build"
},
"devDependencies": {
"@plainidx/configs": "workspace:*",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"typescript": "^5.7.2"
},
"dependencies": {
"@plainidx/plainidx": "workspace:*"
}
}

View File

@@ -0,0 +1,10 @@
import { Document } from '@plainidx/plainidx';
import React from 'react';
type DocumentContextType = {
document: Document;
};
const DocumentContext = React.createContext<DocumentContextType | undefined>(undefined);
export { DocumentContext };

View File

@@ -0,0 +1,57 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { DocumentContext } from './document.context.js';
import { usePlainDB } from '../plaindb/plaindb.hooks.js';
const useDocument = () => {
const context = useContext(DocumentContext);
if (!context) {
throw new Error('useDocument must be used within a DocumentProvider');
}
return context.document;
};
const useDocumentRenders = () => {
const { editor } = usePlainDB();
const document = useDocument();
const [current, setCurrent] = useState(editor.renders.getByDocument(document));
useEffect(() => {
const listen = () => {
setCurrent(editor.renders.getByDocument(document));
};
editor.renders.on('change', listen);
return () => {
editor.renders.off('change', listen);
};
}, [editor.renders, document, setCurrent]);
return current;
};
const useDocumentValue = () => {
const document = useDocument();
const [current, setCurrent] = useState(document.data);
const setValue = useCallback(
(newValue: Buffer) => {
document.data = newValue;
},
[document],
);
useEffect(() => {
const listen = () => {
setCurrent(document.data);
};
document.on('change', listen);
return () => {
document.off('change', listen);
};
}, [document, setCurrent]);
return [current, setValue] as const;
};
export { useDocument, useDocumentValue, useDocumentRenders };

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { Document } from '@plainidx/plainidx';
import { DocumentContext } from './document.context.js';
type DocumentProviderProps = {
document: Document;
children: React.ReactNode;
};
const DocumentProvider = ({ document, children }: DocumentProviderProps) => {
return <DocumentContext.Provider value={{ document }}>{children}</DocumentContext.Provider>;
};
export { DocumentProvider };

View File

View File

@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
import { usePlainDB } from '../plaindb/plaindb.hooks.js';
const usePanel = (id: string) => {
const { editor } = usePlainDB();
const [panel, setPanel] = useState(editor.panels.get(id));
useEffect(() => {
const update = () => {
setPanel(editor.panels.get(id));
};
editor.panels.on('change', update);
return () => {
editor.panels.off('change', update);
};
}, [editor, id]);
return panel;
};
export { usePanel };

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { usePanel } from './panels.hooks.js';
type PanelProps = {
id: string;
};
const Panel = ({ id }: PanelProps) => {
const panel = usePanel(id);
if (!panel) {
return null;
}
const Component = panel.component;
return <Component />;
};
export { Panel };

View File

@@ -0,0 +1,11 @@
import { Editor, PlainDB } from '@plainidx/plainidx';
import React from 'react';
type PlainDBContextType = {
db: PlainDB;
editor: Editor;
};
const PlainDBContext = React.createContext<PlainDBContextType | undefined>(undefined);
export { PlainDBContext };

View File

@@ -0,0 +1,12 @@
import { useContext } from 'react';
import { PlainDBContext } from './plaindb.context.js';
const usePlainDB = () => {
const context = useContext(PlainDBContext);
if (context === undefined) {
throw new Error('usePlainDB must be used within a PlainDBProvider');
}
return context;
};
export { usePlainDB };

View File

@@ -0,0 +1,19 @@
import React, { useMemo } from 'react';
import { Editor, PlainDB } from '@plainidx/plainidx';
import { PlainDBContext } from './plaindb.context.js';
type PlainDBProviderProps = {
children: React.ReactNode;
db: PlainDB;
};
const PlainDBProvider = ({ children, db }: PlainDBProviderProps) => {
const editor = useMemo(() => {
const next = new Editor();
db.plugins.setupUI(editor);
return next;
}, [db]);
return <PlainDBContext.Provider value={{ db, editor }}>{children}</PlainDBContext.Provider>;
};
export { PlainDBProvider };

View File

@@ -0,0 +1,35 @@
import { useCallback, useEffect, useState } from 'react';
import { usePlainDB } from '../plaindb/plaindb.hooks.js';
const useOpenDocument = () => {
const { editor, db } = usePlainDB();
const open = useCallback(
async (location: string) => {
const document = await db.documents.get(location);
editor.workspace.openDocument(document);
},
[editor, db],
);
return open;
};
const useDocuments = () => {
const { editor } = usePlainDB();
const [documents, setDocuments] = useState(editor.workspace.documents);
useEffect(() => {
const update = () => {
setDocuments(editor.workspace.documents);
};
editor.workspace.on('change', update);
return () => {
editor.workspace.off('change', update);
};
});
return documents;
};
export { useOpenDocument, useDocuments };

View File

@@ -0,0 +1,10 @@
{
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"jsx": "react"
},
"include": [
"src"
]
}

View File

@@ -10,6 +10,7 @@
"build": "tsc --build"
},
"dependencies": {
"@plainidx/configs": "workspace:*",
"@plainidx/plainidx": "workspace:*"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},

View File

@@ -26,6 +26,7 @@
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@plainidx/configs": "workspace:*",
"@types/lodash": "^4.17.13",
"@types/mdast": "^4.0.4",
"@types/node": "^22.10.1",

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},

View File

@@ -13,6 +13,7 @@
"vitest": "^2.1.8"
},
"devDependencies": {
"@plainidx/configs": "workspace:*",
"@pnpm/find-workspace-packages": "^6.0.9",
"@types/node": "^22.10.1",
"typescript": "^5.7.2"

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},

View File

@@ -12,10 +12,7 @@ if (!root) {
const packages = await findWorkspacePackages(root);
const alias = Object.fromEntries(
packages.map(({ dir, manifest }) => [
manifest.name!,
resolve(dir, 'src', 'exports.ts'),
]),
packages.map(({ dir, manifest }) => [manifest.name || '_unknown', resolve(dir, 'src', 'exports.ts')]),
);
export default defineConfig({

View File

@@ -21,6 +21,7 @@
"sqlite3": "^5.1.7"
},
"devDependencies": {
"@plainidx/configs": "workspace:*",
"@types/node": "^22.10.1",
"typescript": "^5.7.2"
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},