@vemjs/renderer-vecto 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +115 -0
  3. package/dist/CommandBar.d.ts +13 -0
  4. package/dist/CommandBar.d.ts.map +1 -0
  5. package/dist/CommandBar.js +60 -0
  6. package/dist/CommandBar.js.map +1 -0
  7. package/dist/FileSystemHandler.d.ts +9 -0
  8. package/dist/FileSystemHandler.d.ts.map +1 -0
  9. package/dist/FileSystemHandler.js +51 -0
  10. package/dist/FileSystemHandler.js.map +1 -0
  11. package/dist/VemEditorEntity.d.ts +16 -0
  12. package/dist/VemEditorEntity.d.ts.map +1 -0
  13. package/dist/VemEditorEntity.js +209 -0
  14. package/dist/VemEditorEntity.js.map +1 -0
  15. package/dist/Workspace.d.ts +13 -0
  16. package/dist/Workspace.d.ts.map +1 -0
  17. package/dist/Workspace.js +55 -0
  18. package/dist/Workspace.js.map +1 -0
  19. package/dist/WorkspaceExplorer.d.ts +18 -0
  20. package/dist/WorkspaceExplorer.d.ts.map +1 -0
  21. package/dist/WorkspaceExplorer.js +102 -0
  22. package/dist/WorkspaceExplorer.js.map +1 -0
  23. package/dist/WorkspaceLayout.d.ts +38 -0
  24. package/dist/WorkspaceLayout.d.ts.map +1 -0
  25. package/dist/WorkspaceLayout.js +165 -0
  26. package/dist/WorkspaceLayout.js.map +1 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +41 -0
  30. package/dist/index.js.map +1 -0
  31. package/package.json +19 -0
  32. package/src/CommandBar.ts +69 -0
  33. package/src/FileSystemHandler.test.ts +51 -0
  34. package/src/FileSystemHandler.ts +60 -0
  35. package/src/VemEditorEntity.ts +235 -0
  36. package/src/Workspace.ts +65 -0
  37. package/src/WorkspaceExplorer.ts +124 -0
  38. package/src/WorkspaceLayout.ts +191 -0
  39. package/src/index.ts +48 -0
  40. package/tsconfig.json +13 -0
@@ -0,0 +1,235 @@
1
+ import { UIComponent, Text, RichText } from '@vectojs/ui';
2
+ import type { IRenderer } from '@vectojs/core';
3
+ import type { VemEditorState } from '@vemjs/core';
4
+ import { CommandBar } from './CommandBar';
5
+
6
+ export class VemEditorEntity extends UIComponent {
7
+ private editorState: VemEditorState;
8
+ private gutterText: Text;
9
+ private bodyText: RichText;
10
+ private commandBar: CommandBar;
11
+
12
+ private charWidth = 8.4;
13
+ private lineHeight = 20;
14
+ private scrollY = 0; // scroll offset in lines
15
+
16
+ constructor(editorState: VemEditorState) {
17
+ super();
18
+ this.editorState = editorState;
19
+ this.width = 800;
20
+ this.height = 600;
21
+ this.clipChildren = true;
22
+
23
+ this.gutterText = new Text('', {
24
+ font: '14px monospace',
25
+ color: '#64748b', // slate-500
26
+ lineHeight: this.lineHeight,
27
+ });
28
+
29
+ // Editor body text
30
+ this.bodyText = new RichText([], {
31
+ font: '14px monospace',
32
+ color: '#e2e8f0', // slate-200
33
+ });
34
+
35
+ this.commandBar = new CommandBar(editorState, this.width);
36
+ this.commandBar.setPosition(0, this.height - 30);
37
+
38
+ this.add(this.gutterText);
39
+ this.add(this.bodyText);
40
+
41
+ // Try to measure the exact monospace char width if running in browser
42
+ if (typeof document !== 'undefined') {
43
+ const canvas = document.createElement('canvas');
44
+ const ctx = canvas.getContext('2d');
45
+ if (ctx) {
46
+ ctx.font = '14px monospace';
47
+ this.charWidth = ctx.measureText('A').width;
48
+ }
49
+ }
50
+
51
+ this.updateFromState();
52
+ }
53
+
54
+ public updateFromState(): void {
55
+ const buffer = this.editorState.getBuffer();
56
+ const cursor = this.editorState.getCursor();
57
+ const lineCount = buffer.getLineCount();
58
+
59
+ // 1. Calculate gutter width dynamically
60
+ const maxLineDigits = Math.max(2, lineCount.toString().length);
61
+ const gutterWidth = maxLineDigits * this.charWidth + 15;
62
+
63
+ // 2. Set line numbers text
64
+ const lineNums: string[] = [];
65
+ for (let i = 1; i <= lineCount; i++) {
66
+ lineNums.push(i.toString().padStart(maxLineDigits, ' '));
67
+ }
68
+ this.gutterText.setText(lineNums.join('\n'));
69
+ this.gutterText.setPosition(5, 5);
70
+
71
+ // 3. Set editor body text
72
+ const spans = buffer.getLines().map((line, idx) => {
73
+ const suffix = idx === lineCount - 1 ? '' : '\n';
74
+ return { text: line + suffix };
75
+ });
76
+ this.bodyText.setSpans(spans);
77
+ this.bodyText.setPosition(gutterWidth + 5, 5);
78
+
79
+ // 4. Handle viewport scrolling to keep cursor visible
80
+ const visibleLines = Math.floor((this.height - 35) / this.lineHeight); // reserve 35px for status bar
81
+ if (cursor.line >= this.scrollY + visibleLines) {
82
+ this.scrollY = cursor.line - visibleLines + 1;
83
+ } else if (cursor.line < this.scrollY) {
84
+ this.scrollY = cursor.line;
85
+ }
86
+
87
+ // Update children scroll positions
88
+ const scrollOffsetY = -this.scrollY * this.lineHeight;
89
+ this.gutterText.setPosition(5, 5 + scrollOffsetY);
90
+ this.bodyText.setPosition(gutterWidth + 5, 5 + scrollOffsetY);
91
+
92
+ // 5. Handle CommandBar visibility
93
+ if (this.editorState.getMode() === 'COMMAND') {
94
+ if (!this.children.includes(this.commandBar)) {
95
+ this.add(this.commandBar);
96
+ }
97
+ this.commandBar.clear();
98
+ } else {
99
+ if (this.children.includes(this.commandBar)) {
100
+ this.remove(this.commandBar);
101
+ }
102
+ }
103
+ }
104
+
105
+ public render(r: IRenderer): void {
106
+ // 1. Draw editor background
107
+ r.beginPath();
108
+ r.moveTo(0, 0);
109
+ r.lineTo(this.width, 0);
110
+ r.lineTo(this.width, this.height);
111
+ r.lineTo(0, this.height);
112
+ r.closePath();
113
+ r.fill('#0f172a'); // slate-900
114
+
115
+ const lineCount = this.editorState.getBuffer().getLineCount();
116
+ const maxLineDigits = Math.max(2, lineCount.toString().length);
117
+ const gutterWidth = maxLineDigits * this.charWidth + 15;
118
+
119
+ // 2. Draw gutter background
120
+ r.beginPath();
121
+ r.moveTo(0, 0);
122
+ r.lineTo(gutterWidth, 0);
123
+ r.lineTo(gutterWidth, this.height);
124
+ r.lineTo(0, this.height);
125
+ r.closePath();
126
+ r.fill('#1e293b'); // slate-800
127
+
128
+ // Apply scrolling transformation for cursor and selections
129
+ r.save();
130
+ r.translate(0, -this.scrollY * this.lineHeight);
131
+
132
+ // 3. Draw Visual Mode selections
133
+ const selection = this.editorState.getVisualSelection();
134
+ if (selection) {
135
+ const type = selection.type;
136
+ let s = { ...selection.anchor };
137
+ let e = { ...selection.active };
138
+
139
+ if (s.line > e.line || (s.line === e.line && s.character > e.character)) {
140
+ const temp = s;
141
+ s = e;
142
+ e = temp;
143
+ }
144
+
145
+ const drawSelRect = (lineIdx: number, startChar: number, endChar: number) => {
146
+ const x = gutterWidth + 5 + startChar * this.charWidth;
147
+ const y = 5 + lineIdx * this.lineHeight;
148
+ const w = (endChar - startChar) * this.charWidth;
149
+ const h = this.lineHeight;
150
+
151
+ r.beginPath();
152
+ r.moveTo(x, y);
153
+ r.lineTo(x + w, y);
154
+ r.lineTo(x + w, y + h);
155
+ r.lineTo(x, y + h);
156
+ r.closePath();
157
+ r.fill('rgba(56, 189, 248, 0.3)'); // sky-400 opacity
158
+ };
159
+
160
+ if (type === 'line') {
161
+ for (let l = s.line; l <= e.line; l++) {
162
+ const lineText = this.editorState.getBuffer().getLine(l);
163
+ drawSelRect(l, 0, Math.max(1, lineText.length));
164
+ }
165
+ } else if (type === 'char') {
166
+ if (s.line === e.line) {
167
+ drawSelRect(s.line, s.character, e.character + 1);
168
+ } else {
169
+ const sLineText = this.editorState.getBuffer().getLine(s.line);
170
+ drawSelRect(s.line, s.character, Math.max(s.character + 1, sLineText.length + 1));
171
+ for (let l = s.line + 1; l < e.line; l++) {
172
+ const lText = this.editorState.getBuffer().getLine(l);
173
+ drawSelRect(l, 0, lText.length + 1);
174
+ }
175
+ drawSelRect(e.line, 0, e.character + 1);
176
+ }
177
+ } else if (type === 'block') {
178
+ const minCol = Math.min(selection.anchor.character, selection.active.character);
179
+ const maxCol = Math.max(selection.anchor.character, selection.active.character);
180
+ for (let l = s.line; l <= e.line; l++) {
181
+ drawSelRect(l, minCol, maxCol + 1);
182
+ }
183
+ }
184
+ }
185
+
186
+ // 4. Draw Vim cursor
187
+ const cursor = this.editorState.getCursor();
188
+ const cursorX = gutterWidth + 5 + cursor.character * this.charWidth;
189
+ const cursorY = 5 + cursor.line * this.lineHeight;
190
+ const mode = this.editorState.getMode();
191
+
192
+ r.beginPath();
193
+ if (mode === 'INSERT') {
194
+ r.moveTo(cursorX, cursorY);
195
+ r.lineTo(cursorX + 2, cursorY);
196
+ r.lineTo(cursorX + 2, cursorY + this.lineHeight);
197
+ r.lineTo(cursorX, cursorY + this.lineHeight);
198
+ r.closePath();
199
+ r.fill('#f43f5e'); // rose-500
200
+ } else {
201
+ r.moveTo(cursorX, cursorY);
202
+ r.lineTo(cursorX + this.charWidth, cursorY);
203
+ r.lineTo(cursorX + this.charWidth, cursorY + this.lineHeight);
204
+ r.lineTo(cursorX, cursorY + this.lineHeight);
205
+ r.closePath();
206
+ r.fill('rgba(56, 189, 248, 0.7)'); // sky-400 opacity
207
+ }
208
+
209
+ r.restore(); // Restore scroll transform
210
+
211
+ // 5. Draw status bar at the bottom
212
+ const statusBarHeight = 30;
213
+ const statusY = this.height - statusBarHeight;
214
+ r.beginPath();
215
+ r.moveTo(0, statusY);
216
+ r.lineTo(this.width, statusY);
217
+ r.lineTo(this.width, this.height);
218
+ r.lineTo(0, this.height);
219
+ r.closePath();
220
+ r.fill('#1e293b'); // slate-800
221
+
222
+ if (mode !== 'COMMAND') {
223
+ const modeText = `-- ${mode} --`;
224
+ const posText = `${cursor.line + 1}:${cursor.character + 1}`;
225
+ const pendingKeys = this.editorState.getPendingKeys();
226
+ const pendingText = pendingKeys.length > 0 ? pendingKeys.join('') : '';
227
+
228
+ r.fillText(modeText, 10, statusY + 18, 'bold 12px monospace', '#38bdf8');
229
+ if (pendingText) {
230
+ r.fillText(pendingText, 120, statusY + 18, '12px monospace', '#e2e8f0');
231
+ }
232
+ r.fillText(posText, this.width - 60, statusY + 18, '12px monospace', '#94a3b8');
233
+ }
234
+ }
235
+ }
@@ -0,0 +1,65 @@
1
+ import { UIComponent, Tabs } from '@vectojs/ui';
2
+ import type { IRenderer } from '@vectojs/core';
3
+ import { WorkspaceLayout } from './WorkspaceLayout';
4
+
5
+ export class VemWorkspace extends UIComponent {
6
+ private tabsComponent: Tabs | null = null;
7
+ private layouts: WorkspaceLayout[] = [];
8
+
9
+ constructor(width: number, height: number, initialText?: string) {
10
+ super();
11
+ this.width = width;
12
+ this.height = height;
13
+
14
+ const initialLayout = new WorkspaceLayout(width, height - 30, initialText || '');
15
+ this.layouts.push(initialLayout);
16
+
17
+ this.tabsComponent = new Tabs({
18
+ width: this.width,
19
+ height: this.height,
20
+ tabHeight: 30,
21
+ tabs: [{ id: 'tab-1', label: 'Tab 1', content: initialLayout }],
22
+ value: 'tab-1',
23
+ });
24
+
25
+ this.add(this.tabsComponent);
26
+ }
27
+
28
+ public addTab(initialText?: string): void {
29
+ const nextIndex = this.layouts.length + 1;
30
+ const newLayout = new WorkspaceLayout(this.width, this.height - 30, initialText || '');
31
+ this.layouts.push(newLayout);
32
+
33
+ const updatedTabs = this.layouts.map((layout, idx) => ({
34
+ id: `tab-${idx + 1}`,
35
+ label: `Tab ${idx + 1}`,
36
+ content: layout,
37
+ }));
38
+
39
+ this.tabsComponent!.tabs = updatedTabs;
40
+ this.tabsComponent!.value = `tab-${nextIndex}`;
41
+ }
42
+
43
+ public update(dt: number, time: number): void {
44
+ super.update(dt, time);
45
+ if (this.tabsComponent) {
46
+ this.tabsComponent.width = this.width;
47
+ this.tabsComponent.height = this.height;
48
+ }
49
+ }
50
+
51
+ public getActiveLayout(): WorkspaceLayout | null {
52
+ if (!this.tabsComponent) return null;
53
+ const activeId = this.tabsComponent.value;
54
+ const match = activeId.match(/tab-(\d+)/);
55
+ if (match) {
56
+ const idx = parseInt(match[1], 10) - 1;
57
+ return this.layouts[idx] || null;
58
+ }
59
+ return null;
60
+ }
61
+
62
+ public render(_r: IRenderer): void {
63
+ // Handled by tabsComponent
64
+ }
65
+ }
@@ -0,0 +1,124 @@
1
+ import { UIComponent, PanelGroup, Panel, TreeView, Button } from '@vectojs/ui';
2
+ import type { IRenderer } from '@vectojs/core';
3
+ import { VemWorkspace } from './Workspace';
4
+ import { FileSystemHandler } from './FileSystemHandler';
5
+
6
+ export class WorkspaceExplorer extends UIComponent {
7
+ private panelGroup: PanelGroup;
8
+ private leftPanel: Panel;
9
+ private rightPanel: Panel;
10
+ private workspace: VemWorkspace;
11
+ private treeView: TreeView | null = null;
12
+ private openBtn: Button;
13
+ private fsHandler: FileSystemHandler;
14
+
15
+ constructor(width: number, height: number, initialText?: string) {
16
+ super();
17
+ this.width = width;
18
+ this.height = height;
19
+ this.fsHandler = new FileSystemHandler();
20
+
21
+ this.panelGroup = new PanelGroup({
22
+ direction: 'horizontal',
23
+ width: width,
24
+ height: height,
25
+ });
26
+
27
+ this.leftPanel = new Panel({ minSize: 150, defaultSize: 0.2 });
28
+ this.rightPanel = new Panel({ minSize: 300 });
29
+
30
+ this.workspace = new VemWorkspace(width * 0.8, height, initialText);
31
+
32
+ this.openBtn = new Button('Open Folder', {
33
+ onClick: () => this.handleOpenFolder(),
34
+ bg: '#1e293b', // slate-800
35
+ hoverBg: '#334155',
36
+ font: '14px monospace',
37
+ color: '#e2e8f0',
38
+ });
39
+ this.openBtn.width = 120;
40
+ this.openBtn.height = 35;
41
+ this.openBtn.setPosition(15, 15);
42
+
43
+ this.leftPanel.add(this.openBtn);
44
+ this.rightPanel.add(this.workspace);
45
+
46
+ this.panelGroup.addPanel(this.leftPanel);
47
+ this.panelGroup.addPanel(this.rightPanel);
48
+
49
+ this.add(this.panelGroup);
50
+ }
51
+
52
+ public getWorkspace(): VemWorkspace {
53
+ return this.workspace;
54
+ }
55
+
56
+ private async handleOpenFolder(): Promise<void> {
57
+ if (typeof window === 'undefined' || !(window as any).showDirectoryPicker) {
58
+ console.warn('File System Access API is not supported in this environment.');
59
+ return;
60
+ }
61
+
62
+ try {
63
+ const rootHandle = await (window as any).showDirectoryPicker();
64
+ const nodes = await this.fsHandler.readDirectory(rootHandle);
65
+
66
+ this.treeView = new TreeView({
67
+ nodes,
68
+ width: this.leftPanel.width,
69
+ height: this.height - 10,
70
+ font: '13px monospace',
71
+ color: '#cbd5e1',
72
+ selectedColor: 'rgba(56, 189, 248, 0.2)',
73
+ hoverColor: 'rgba(255, 255, 255, 0.05)',
74
+ onSelect: async (node) => {
75
+ const fileHandle = this.fsHandler.getFileHandle(node.id);
76
+ if (fileHandle) {
77
+ const content = await this.fsHandler.readFile(fileHandle);
78
+ this.workspace.addTab(content);
79
+ }
80
+ },
81
+ });
82
+
83
+ this.leftPanel.remove(this.openBtn);
84
+ this.leftPanel.add(this.treeView);
85
+ } catch (err) {
86
+ console.error('Error selecting directory:', err);
87
+ }
88
+ }
89
+
90
+ public update(dt: number, time: number): void {
91
+ super.update(dt, time);
92
+
93
+ if (this.panelGroup.width !== this.width || this.panelGroup.height !== this.height) {
94
+ this.panelGroup.width = this.width;
95
+ this.panelGroup.height = this.height;
96
+ }
97
+
98
+ if (
99
+ this.treeView &&
100
+ (this.treeView.width !== this.leftPanel.width || this.treeView.height !== this.height)
101
+ ) {
102
+ this.treeView.width = this.leftPanel.width;
103
+ this.treeView.height = this.height;
104
+ }
105
+
106
+ if (
107
+ this.workspace.width !== this.rightPanel.width ||
108
+ this.workspace.height !== this.rightPanel.height
109
+ ) {
110
+ this.workspace.width = this.rightPanel.width;
111
+ this.workspace.height = this.rightPanel.height;
112
+ }
113
+ }
114
+
115
+ public render(_r: IRenderer): void {
116
+ _r.beginPath();
117
+ _r.moveTo(0, 0);
118
+ _r.lineTo(this.leftPanel.width, 0);
119
+ _r.lineTo(this.leftPanel.width, this.height);
120
+ _r.lineTo(0, this.height);
121
+ _r.closePath();
122
+ _r.fill('#090d16'); // deep slate sidebar background
123
+ }
124
+ }
@@ -0,0 +1,191 @@
1
+ import { UIComponent, PanelGroup, Panel } from '@vectojs/ui';
2
+ import type { Entity, IRenderer } from '@vectojs/core';
3
+ import { VemEditorState } from '@vemjs/core';
4
+ import { VemEditorEntity } from './VemEditorEntity';
5
+
6
+ export type PaneNode =
7
+ | { type: 'leaf'; id: string; state: VemEditorState }
8
+ | { type: 'split'; id: string; direction: 'horizontal' | 'vertical'; children: PaneNode[] };
9
+
10
+ export class EditorPane extends Panel {
11
+ public editorState: VemEditorState;
12
+ public editorEntity: VemEditorEntity;
13
+
14
+ constructor(state: VemEditorState) {
15
+ super();
16
+ this.editorState = state;
17
+ this.editorEntity = new VemEditorEntity(state);
18
+ this.add(this.editorEntity);
19
+ }
20
+
21
+ public update(dt: number, time: number): void {
22
+ super.update(dt, time);
23
+ if (this.editorEntity.width !== this.width || this.editorEntity.height !== this.height) {
24
+ this.editorEntity.width = this.width;
25
+ this.editorEntity.height = this.height;
26
+ this.editorEntity.updateFromState();
27
+ }
28
+ }
29
+ }
30
+
31
+ export class WorkspaceLayout extends UIComponent {
32
+ private rootNode: PaneNode;
33
+ private activePaneId: string;
34
+ private layoutRoot: Entity | null = null;
35
+ private paneMap = new Map<string, PaneNode>();
36
+
37
+ constructor(width: number, height: number, initialText?: string) {
38
+ super();
39
+ this.width = width;
40
+ this.height = height;
41
+
42
+ const initialId = 'pane-1';
43
+ const initialState = new VemEditorState(initialText);
44
+
45
+ this.rootNode = {
46
+ type: 'leaf',
47
+ id: initialId,
48
+ state: initialState,
49
+ };
50
+
51
+ this.activePaneId = initialId;
52
+ this.paneMap.set(initialId, this.rootNode);
53
+
54
+ this.bindStateEvents(initialId, initialState);
55
+ this.rebuildLayout();
56
+ }
57
+
58
+ private bindStateEvents(id: string, state: VemEditorState): void {
59
+ state.onSplit((dir) => {
60
+ this.splitPane(id, dir);
61
+ });
62
+ state.onQuit(() => {
63
+ this.closePane(id);
64
+ });
65
+ }
66
+
67
+ public getActiveState(): VemEditorState | null {
68
+ const pane = this.paneMap.get(this.activePaneId);
69
+ if (pane && pane.type === 'leaf') {
70
+ return pane.state;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ public splitPane(paneId: string, direction: 'horizontal' | 'vertical'): void {
76
+ const targetNode = this.paneMap.get(paneId);
77
+ if (!targetNode || targetNode.type !== 'leaf') return;
78
+
79
+ const newId = `pane-${Date.now()}`;
80
+ const newState = new VemEditorState(targetNode.state.getBuffer().getText());
81
+ const newLeaf: PaneNode = {
82
+ type: 'leaf',
83
+ id: newId,
84
+ state: newState,
85
+ };
86
+
87
+ this.paneMap.set(newId, newLeaf);
88
+ this.bindStateEvents(newId, newState);
89
+
90
+ const oldLeafCopy: PaneNode = { ...targetNode };
91
+
92
+ const splitNode = targetNode as any;
93
+ splitNode.type = 'split';
94
+ splitNode.direction = direction;
95
+ splitNode.children = [oldLeafCopy, newLeaf];
96
+
97
+ this.activePaneId = newId;
98
+ this.rebuildLayout();
99
+ }
100
+
101
+ public closePane(paneId: string): void {
102
+ if (this.rootNode.type === 'leaf') return;
103
+
104
+ const parentNode = this.findParentNode(this.rootNode, paneId);
105
+ if (!parentNode || parentNode.type !== 'split') return;
106
+
107
+ const sibling = parentNode.children.find((c) => c.id !== paneId);
108
+ if (!sibling) return;
109
+
110
+ const grandparent = this.findParentNode(this.rootNode, parentNode.id);
111
+ if (grandparent && grandparent.type === 'split') {
112
+ const idx = grandparent.children.findIndex((c) => c.id === parentNode.id);
113
+ grandparent.children[idx] = sibling;
114
+ } else {
115
+ this.rootNode = sibling;
116
+ }
117
+
118
+ this.paneMap.delete(paneId);
119
+
120
+ const remainingLeaf = this.findFirstLeaf(this.rootNode);
121
+ if (remainingLeaf) {
122
+ this.activePaneId = remainingLeaf.id;
123
+ }
124
+
125
+ this.rebuildLayout();
126
+ }
127
+
128
+ private findParentNode(current: PaneNode, childId: string): PaneNode | null {
129
+ if (current.type === 'leaf') return null;
130
+ for (const child of current.children) {
131
+ if (child.id === childId) {
132
+ return current;
133
+ }
134
+ const parent = this.findParentNode(child, childId);
135
+ if (parent) return parent;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ private findFirstLeaf(current: PaneNode): PaneNode | null {
141
+ if (current.type === 'leaf') return current;
142
+ return this.findFirstLeaf(current.children[0]);
143
+ }
144
+
145
+ public rebuildLayout(): void {
146
+ if (this.layoutRoot) {
147
+ this.remove(this.layoutRoot);
148
+ }
149
+
150
+ this.layoutRoot = this.buildNode(this.rootNode);
151
+ this.add(this.layoutRoot);
152
+ }
153
+
154
+ private buildNode(node: PaneNode): Entity {
155
+ if (node.type === 'leaf') {
156
+ const pane = new EditorPane(node.state);
157
+ pane.id = node.id;
158
+ return pane;
159
+ } else {
160
+ const group = new PanelGroup({
161
+ direction: node.direction,
162
+ width: this.width,
163
+ height: this.height,
164
+ });
165
+
166
+ for (const child of node.children) {
167
+ const childEntity = this.buildNode(child);
168
+ if (childEntity instanceof Panel) {
169
+ group.addPanel(childEntity);
170
+ } else {
171
+ const p = new Panel();
172
+ p.add(childEntity);
173
+ group.addPanel(p);
174
+ }
175
+ }
176
+ return group;
177
+ }
178
+ }
179
+
180
+ public update(dt: number, time: number): void {
181
+ super.update(dt, time);
182
+ if (this.layoutRoot) {
183
+ this.layoutRoot.width = this.width;
184
+ this.layoutRoot.height = this.height;
185
+ }
186
+ }
187
+
188
+ public render(_r: IRenderer): void {
189
+ // Handled by children
190
+ }
191
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { Scene } from '@vectojs/core';
2
+ import type { VemEditorState } from '@vemjs/core';
3
+ import { VemEditorEntity } from './VemEditorEntity';
4
+
5
+ export { VemEditorEntity } from './VemEditorEntity';
6
+ export { CommandBar } from './CommandBar';
7
+ export { WorkspaceLayout, EditorPane, type PaneNode } from './WorkspaceLayout';
8
+ export { VemWorkspace } from './Workspace';
9
+ export { WorkspaceExplorer } from './WorkspaceExplorer';
10
+
11
+ export class VectoRenderer {
12
+ private editorState: VemEditorState;
13
+ private scene: Scene | null = null;
14
+ private editorEntity: VemEditorEntity | null = null;
15
+
16
+ constructor(editorState: VemEditorState) {
17
+ this.editorState = editorState;
18
+ }
19
+
20
+ public attach(canvas: HTMLCanvasElement): void {
21
+ this.scene = new Scene(canvas);
22
+ this.editorEntity = new VemEditorEntity(this.editorState);
23
+ this.scene.add(this.editorEntity);
24
+ this.scene.start();
25
+
26
+ // Listen to changes in the editor state to update rendering properties
27
+ this.editorState.onChange(() => {
28
+ if (this.editorEntity) {
29
+ this.editorEntity.updateFromState();
30
+ }
31
+
32
+ if (this.editorState.getMode() === 'COMMAND') {
33
+ setTimeout(() => {
34
+ const inputEl = this.scene?.getA11yElement('vem-command-input');
35
+ if (inputEl) {
36
+ (inputEl as HTMLElement).focus();
37
+ }
38
+ }, 10);
39
+ }
40
+ });
41
+ }
42
+
43
+ public render(): void {
44
+ if (this.editorEntity) {
45
+ this.editorEntity.updateFromState();
46
+ }
47
+ }
48
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "sourceMap": true,
8
+ "rootDir": "./src",
9
+ "outDir": "./dist"
10
+ },
11
+ "include": ["src/**/*"],
12
+ "exclude": ["src/**/*.test.ts"]
13
+ }