mcp-dndgrid 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.
- package/PROJECT_SUMMARY.md +482 -0
- package/QUICKSTART.md +223 -0
- package/README.md +365 -0
- package/STATUS.md +315 -0
- package/USAGE_GUIDE.md +547 -0
- package/dist/chunk-CMGEAPA5.js +157 -0
- package/dist/chunk-CMGEAPA5.js.map +1 -0
- package/dist/chunk-QZHBI6ZI.js +5281 -0
- package/dist/chunk-QZHBI6ZI.js.map +1 -0
- package/dist/chunk-SEGVTWSK.js +44 -0
- package/dist/chunk-SEGVTWSK.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +248012 -0
- package/dist/index.js.map +1 -0
- package/dist/stdio-FWYJXSU7.js +101 -0
- package/dist/stdio-FWYJXSU7.js.map +1 -0
- package/dist/template-JDMAVVX7.js +9 -0
- package/dist/template-JDMAVVX7.js.map +1 -0
- package/examples/claude_desktop_config.example.json +12 -0
- package/examples/example-complex-editor.tsx +107 -0
- package/examples/example-dashboard.tsx +65 -0
- package/examples/example-ide-layout.tsx +53 -0
- package/examples/test-generator.ts +37 -0
- package/examples/test-parser.ts +121 -0
- package/examples/test-scenarios.md +496 -0
- package/package.json +42 -0
- package/src/index.ts +16 -0
- package/src/server.ts +314 -0
- package/src/tools/analyze-layout.ts +193 -0
- package/src/tools/apply-template.ts +125 -0
- package/src/tools/generate-layout.ts +235 -0
- package/src/tools/interactive-builder.ts +100 -0
- package/src/tools/validate-layout.ts +113 -0
- package/src/types/layout.ts +48 -0
- package/src/types/template.ts +181 -0
- package/src/utils/ast-parser.ts +264 -0
- package/src/utils/code-generator.ts +123 -0
- package/src/utils/layout-analyzer.ts +105 -0
- package/src/utils/layout-builder.ts +127 -0
- package/src/utils/validator.ts +263 -0
- package/stderr.log +1 -0
- package/stdout.log +0 -0
- package/test-mcp.js +27 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { parse } from '@typescript-eslint/typescript-estree';
|
|
2
|
+
import type { TSESTree } from '@typescript-eslint/typescript-estree';
|
|
3
|
+
import type { LayoutTree, LayoutNode, DndSplitDirection } from '../types/layout.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses TypeScript/JSX code to extract DndGrid layout structure
|
|
7
|
+
*/
|
|
8
|
+
export class ASTParser {
|
|
9
|
+
/**
|
|
10
|
+
* Parse DndGrid code and extract LayoutTree
|
|
11
|
+
*/
|
|
12
|
+
parse(code: string): LayoutTree | null {
|
|
13
|
+
try {
|
|
14
|
+
const ast = parse(code, {
|
|
15
|
+
jsx: true,
|
|
16
|
+
loc: true,
|
|
17
|
+
range: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const container = this.findDndGridContainer(ast);
|
|
21
|
+
if (!container) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return this.buildLayoutTree(container);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Failed to parse code:', error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find DndGridContainer in AST
|
|
34
|
+
*/
|
|
35
|
+
private findDndGridContainer(ast: TSESTree.Program): TSESTree.JSXElement | null {
|
|
36
|
+
let container: TSESTree.JSXElement | null = null;
|
|
37
|
+
|
|
38
|
+
const visit = (node: TSESTree.Node) => {
|
|
39
|
+
if (node.type === 'JSXElement') {
|
|
40
|
+
const openingElement = node.openingElement;
|
|
41
|
+
if (this.isComponentName(openingElement.name, 'DndGridContainer')) {
|
|
42
|
+
container = node;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Recursively visit children
|
|
48
|
+
for (const key in node) {
|
|
49
|
+
const child = (node as any)[key];
|
|
50
|
+
if (child && typeof child === 'object') {
|
|
51
|
+
if (Array.isArray(child)) {
|
|
52
|
+
child.forEach(visit);
|
|
53
|
+
} else if (child.type) {
|
|
54
|
+
visit(child);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
visit(ast);
|
|
61
|
+
return container;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build LayoutTree from DndGridContainer element
|
|
66
|
+
*/
|
|
67
|
+
private buildLayoutTree(containerElement: TSESTree.JSXElement): LayoutTree | null {
|
|
68
|
+
const props = this.extractProps(containerElement.openingElement);
|
|
69
|
+
const width = this.getPropValue(props, 'width') as number;
|
|
70
|
+
const height = this.getPropValue(props, 'height') as number;
|
|
71
|
+
|
|
72
|
+
if (!width || !height) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find the child node
|
|
77
|
+
const child = this.findFirstElementChild(containerElement);
|
|
78
|
+
if (!child) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const childNode = this.buildLayoutNode(child);
|
|
83
|
+
if (!childNode) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
type: 'container',
|
|
89
|
+
width,
|
|
90
|
+
height,
|
|
91
|
+
child: childNode,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build LayoutNode from JSXElement
|
|
97
|
+
*/
|
|
98
|
+
private buildLayoutNode(element: TSESTree.JSXElement): LayoutNode | null {
|
|
99
|
+
const openingElement = element.openingElement;
|
|
100
|
+
|
|
101
|
+
// Check if it's a DndGridSplit
|
|
102
|
+
if (this.isComponentName(openingElement.name, 'DndGridSplit')) {
|
|
103
|
+
return this.buildSplitNode(element);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if it's a DndGridItem
|
|
107
|
+
if (this.isComponentName(openingElement.name, 'DndGridItem')) {
|
|
108
|
+
return this.buildItemNode(element);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build Split node
|
|
116
|
+
*/
|
|
117
|
+
private buildSplitNode(element: TSESTree.JSXElement): LayoutNode | null {
|
|
118
|
+
const props = this.extractProps(element.openingElement);
|
|
119
|
+
const direction = this.getPropValue(props, 'direction') as DndSplitDirection;
|
|
120
|
+
const ratio = this.getPropValue(props, 'ratio') as number;
|
|
121
|
+
|
|
122
|
+
if (!direction || typeof ratio !== 'number') {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get children (should be exactly 2)
|
|
127
|
+
const children = this.findElementChildren(element);
|
|
128
|
+
if (children.length !== 2) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const primary = this.buildLayoutNode(children[0]);
|
|
133
|
+
const secondary = this.buildLayoutNode(children[1]);
|
|
134
|
+
|
|
135
|
+
if (!primary || !secondary) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
type: 'split',
|
|
141
|
+
direction,
|
|
142
|
+
ratio,
|
|
143
|
+
primary,
|
|
144
|
+
secondary,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build Item node
|
|
150
|
+
*/
|
|
151
|
+
private buildItemNode(element: TSESTree.JSXElement): LayoutNode | null {
|
|
152
|
+
// Find the first child component
|
|
153
|
+
const child = this.findFirstElementChild(element);
|
|
154
|
+
if (!child) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const componentName = this.getComponentName(child.openingElement.name);
|
|
159
|
+
if (!componentName) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
type: 'item',
|
|
165
|
+
component: componentName,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract props from JSXOpeningElement
|
|
171
|
+
*/
|
|
172
|
+
private extractProps(
|
|
173
|
+
openingElement: TSESTree.JSXOpeningElement
|
|
174
|
+
): Map<string, TSESTree.Expression | TSESTree.Literal> {
|
|
175
|
+
const props = new Map<string, TSESTree.Expression | TSESTree.Literal>();
|
|
176
|
+
|
|
177
|
+
for (const attr of openingElement.attributes) {
|
|
178
|
+
if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier') {
|
|
179
|
+
const name = attr.name.name;
|
|
180
|
+
const value = attr.value;
|
|
181
|
+
|
|
182
|
+
if (value?.type === 'JSXExpressionContainer') {
|
|
183
|
+
props.set(name, value.expression as TSESTree.Expression);
|
|
184
|
+
} else if (value?.type === 'Literal') {
|
|
185
|
+
props.set(name, value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return props;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get prop value
|
|
195
|
+
*/
|
|
196
|
+
private getPropValue(
|
|
197
|
+
props: Map<string, TSESTree.Expression | TSESTree.Literal>,
|
|
198
|
+
name: string
|
|
199
|
+
): string | number | boolean | null {
|
|
200
|
+
const value = props.get(name);
|
|
201
|
+
if (!value) return null;
|
|
202
|
+
|
|
203
|
+
if (value.type === 'Literal') {
|
|
204
|
+
return value.value as string | number | boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Handle simple cases
|
|
208
|
+
if (value.type === 'Identifier') {
|
|
209
|
+
return value.name;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Find element children (excluding text nodes)
|
|
217
|
+
*/
|
|
218
|
+
private findElementChildren(element: TSESTree.JSXElement): TSESTree.JSXElement[] {
|
|
219
|
+
const children: TSESTree.JSXElement[] = [];
|
|
220
|
+
|
|
221
|
+
for (const child of element.children) {
|
|
222
|
+
if (child.type === 'JSXElement') {
|
|
223
|
+
children.push(child);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return children;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Find first element child
|
|
232
|
+
*/
|
|
233
|
+
private findFirstElementChild(element: TSESTree.JSXElement): TSESTree.JSXElement | null {
|
|
234
|
+
for (const child of element.children) {
|
|
235
|
+
if (child.type === 'JSXElement') {
|
|
236
|
+
return child;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Check if a JSX name matches a component name
|
|
244
|
+
*/
|
|
245
|
+
private isComponentName(
|
|
246
|
+
name: TSESTree.JSXTagNameExpression,
|
|
247
|
+
componentName: string
|
|
248
|
+
): boolean {
|
|
249
|
+
if (name.type === 'JSXIdentifier') {
|
|
250
|
+
return name.name === componentName;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get component name from JSX name
|
|
257
|
+
*/
|
|
258
|
+
private getComponentName(name: TSESTree.JSXTagNameExpression): string | null {
|
|
259
|
+
if (name.type === 'JSXIdentifier') {
|
|
260
|
+
return name.name;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { LayoutTree, LayoutNode, LayoutSplit, LayoutItem } from '../types/layout.js';
|
|
2
|
+
|
|
3
|
+
export interface GenerateOptions {
|
|
4
|
+
framework: 'react' | 'nextjs-app' | 'nextjs-pages';
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
componentPrefix?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generates TypeScript/JSX code from LayoutTree
|
|
12
|
+
*/
|
|
13
|
+
export class CodeGenerator {
|
|
14
|
+
private options: GenerateOptions;
|
|
15
|
+
|
|
16
|
+
constructor(options: GenerateOptions) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate complete component code
|
|
22
|
+
*/
|
|
23
|
+
generate(layout: LayoutTree): string {
|
|
24
|
+
const useClient = this.shouldAddUseClient();
|
|
25
|
+
const imports = this.generateImports();
|
|
26
|
+
const jsx = this.generateJSX(layout.child, 2);
|
|
27
|
+
|
|
28
|
+
const code = `${useClient}${imports}
|
|
29
|
+
|
|
30
|
+
export default function DndGridLayout() {
|
|
31
|
+
return (
|
|
32
|
+
<DndGridContainer width={${layout.width}} height={${layout.height}}>
|
|
33
|
+
${jsx}
|
|
34
|
+
</DndGridContainer>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
return this.formatCode(code);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if "use client" directive is needed
|
|
44
|
+
*/
|
|
45
|
+
private shouldAddUseClient(): string {
|
|
46
|
+
if (this.options.framework === 'nextjs-app') {
|
|
47
|
+
return '"use client";\n\n';
|
|
48
|
+
}
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate import statements
|
|
54
|
+
*/
|
|
55
|
+
private generateImports(): string {
|
|
56
|
+
const imports: string[] = [];
|
|
57
|
+
|
|
58
|
+
// DndGrid components import
|
|
59
|
+
imports.push(
|
|
60
|
+
`import { DndGridContainer, DndGridSplit, DndGridItem } from 'zerojin/components';`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return imports.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate JSX for a layout node
|
|
68
|
+
*/
|
|
69
|
+
private generateJSX(node: LayoutNode, indent: number): string {
|
|
70
|
+
const spaces = ' '.repeat(indent);
|
|
71
|
+
|
|
72
|
+
if (node.type === 'item') {
|
|
73
|
+
return this.generateItemJSX(node, indent);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return this.generateSplitJSX(node, indent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate JSX for a GridItem
|
|
81
|
+
*/
|
|
82
|
+
private generateItemJSX(node: LayoutItem, indent: number): string {
|
|
83
|
+
const spaces = ' '.repeat(indent);
|
|
84
|
+
const innerSpaces = ' '.repeat(indent + 2);
|
|
85
|
+
const componentName = this.options.componentPrefix
|
|
86
|
+
? `${this.options.componentPrefix}${node.component}`
|
|
87
|
+
: node.component;
|
|
88
|
+
|
|
89
|
+
return `${spaces}<DndGridItem>
|
|
90
|
+
${innerSpaces}<${componentName} />
|
|
91
|
+
${spaces}</DndGridItem>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate JSX for a GridSplit
|
|
96
|
+
*/
|
|
97
|
+
private generateSplitJSX(node: LayoutSplit, indent: number): string {
|
|
98
|
+
const spaces = ' '.repeat(indent);
|
|
99
|
+
const primaryJSX = this.generateJSX(node.primary, indent + 2);
|
|
100
|
+
const secondaryJSX = this.generateJSX(node.secondary, indent + 2);
|
|
101
|
+
|
|
102
|
+
return `${spaces}<DndGridSplit direction="${node.direction}" ratio={${node.ratio}}>
|
|
103
|
+
${primaryJSX}
|
|
104
|
+
${secondaryJSX}
|
|
105
|
+
${spaces}</DndGridSplit>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format generated code
|
|
110
|
+
*/
|
|
111
|
+
private formatCode(code: string): string {
|
|
112
|
+
// Basic formatting - can be enhanced with prettier later
|
|
113
|
+
return code.trim() + '\n';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Helper function to quickly generate code
|
|
119
|
+
*/
|
|
120
|
+
export function generateCode(layout: LayoutTree, options: GenerateOptions): string {
|
|
121
|
+
const generator = new CodeGenerator(options);
|
|
122
|
+
return generator.generate(layout);
|
|
123
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { LayoutTree, LayoutNode, LayoutMetadata } from '../types/layout.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analyzes layout tree and extracts metadata
|
|
5
|
+
*/
|
|
6
|
+
export class LayoutAnalyzer {
|
|
7
|
+
/**
|
|
8
|
+
* Calculate layout metadata
|
|
9
|
+
*/
|
|
10
|
+
static calculateMetadata(layout: LayoutTree): LayoutMetadata {
|
|
11
|
+
const stats = this.analyzeNode(layout.child);
|
|
12
|
+
|
|
13
|
+
const itemCount = stats.itemCount;
|
|
14
|
+
const maxDepth = stats.maxDepth;
|
|
15
|
+
|
|
16
|
+
// Estimate performance based on complexity
|
|
17
|
+
let estimatedPerformance: LayoutMetadata['estimatedPerformance'];
|
|
18
|
+
if (itemCount <= 10 && maxDepth <= 3) {
|
|
19
|
+
estimatedPerformance = 'excellent';
|
|
20
|
+
} else if (itemCount <= 20 && maxDepth <= 4) {
|
|
21
|
+
estimatedPerformance = 'good';
|
|
22
|
+
} else if (itemCount <= 50 && maxDepth <= 6) {
|
|
23
|
+
estimatedPerformance = 'fair';
|
|
24
|
+
} else {
|
|
25
|
+
estimatedPerformance = 'poor';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
splitCount: stats.splitCount,
|
|
30
|
+
itemCount: stats.itemCount,
|
|
31
|
+
maxDepth: stats.maxDepth,
|
|
32
|
+
estimatedPerformance,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Recursively analyze a node
|
|
38
|
+
*/
|
|
39
|
+
private static analyzeNode(
|
|
40
|
+
node: LayoutNode,
|
|
41
|
+
depth: number = 1
|
|
42
|
+
): {
|
|
43
|
+
splitCount: number;
|
|
44
|
+
itemCount: number;
|
|
45
|
+
maxDepth: number;
|
|
46
|
+
} {
|
|
47
|
+
if (node.type === 'item') {
|
|
48
|
+
return {
|
|
49
|
+
splitCount: 0,
|
|
50
|
+
itemCount: 1,
|
|
51
|
+
maxDepth: depth,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Split node
|
|
56
|
+
const primaryStats = this.analyzeNode(node.primary, depth + 1);
|
|
57
|
+
const secondaryStats = this.analyzeNode(node.secondary, depth + 1);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
splitCount: 1 + primaryStats.splitCount + secondaryStats.splitCount,
|
|
61
|
+
itemCount: primaryStats.itemCount + secondaryStats.itemCount,
|
|
62
|
+
maxDepth: Math.max(primaryStats.maxDepth, secondaryStats.maxDepth),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Count total items in tree
|
|
68
|
+
*/
|
|
69
|
+
static countItems(node: LayoutNode): number {
|
|
70
|
+
if (node.type === 'item') return 1;
|
|
71
|
+
return this.countItems(node.primary) + this.countItems(node.secondary);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Count total splits in tree
|
|
76
|
+
*/
|
|
77
|
+
static countSplits(node: LayoutNode): number {
|
|
78
|
+
if (node.type === 'item') return 0;
|
|
79
|
+
return 1 + this.countSplits(node.primary) + this.countSplits(node.secondary);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Calculate maximum depth
|
|
84
|
+
*/
|
|
85
|
+
static getMaxDepth(node: LayoutNode, currentDepth: number = 1): number {
|
|
86
|
+
if (node.type === 'item') return currentDepth;
|
|
87
|
+
return Math.max(
|
|
88
|
+
this.getMaxDepth(node.primary, currentDepth + 1),
|
|
89
|
+
this.getMaxDepth(node.secondary, currentDepth + 1)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Collect all component names
|
|
95
|
+
*/
|
|
96
|
+
static collectComponents(node: LayoutNode): string[] {
|
|
97
|
+
if (node.type === 'item') {
|
|
98
|
+
return [node.component];
|
|
99
|
+
}
|
|
100
|
+
return [
|
|
101
|
+
...this.collectComponents(node.primary),
|
|
102
|
+
...this.collectComponents(node.secondary),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LayoutTree,
|
|
3
|
+
LayoutNode,
|
|
4
|
+
LayoutSplit,
|
|
5
|
+
LayoutItem,
|
|
6
|
+
DndSplitDirection,
|
|
7
|
+
} from '../types/layout.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builder for constructing LayoutTree programmatically
|
|
11
|
+
*/
|
|
12
|
+
export class LayoutBuilder {
|
|
13
|
+
private width: number;
|
|
14
|
+
private height: number;
|
|
15
|
+
private root: LayoutNode | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(width: number, height: number) {
|
|
18
|
+
this.width = width;
|
|
19
|
+
this.height = height;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create an Item node
|
|
24
|
+
*/
|
|
25
|
+
static item(component: string): LayoutItem {
|
|
26
|
+
return {
|
|
27
|
+
type: 'item',
|
|
28
|
+
component,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a Split node
|
|
34
|
+
*/
|
|
35
|
+
static split(
|
|
36
|
+
direction: DndSplitDirection,
|
|
37
|
+
ratio: number,
|
|
38
|
+
primary: LayoutNode,
|
|
39
|
+
secondary: LayoutNode
|
|
40
|
+
): LayoutSplit {
|
|
41
|
+
// Validate ratio
|
|
42
|
+
if (ratio <= 0 || ratio >= 1) {
|
|
43
|
+
throw new Error(`Invalid ratio ${ratio}. Must be between 0 and 1 (exclusive)`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
type: 'split',
|
|
48
|
+
direction,
|
|
49
|
+
ratio,
|
|
50
|
+
primary,
|
|
51
|
+
secondary,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set the root node
|
|
57
|
+
*/
|
|
58
|
+
setRoot(node: LayoutNode): this {
|
|
59
|
+
this.root = node;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the final LayoutTree
|
|
65
|
+
*/
|
|
66
|
+
build(): LayoutTree {
|
|
67
|
+
if (!this.root) {
|
|
68
|
+
throw new Error('Root node not set. Call setRoot() first.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
type: 'container',
|
|
73
|
+
width: this.width,
|
|
74
|
+
height: this.height,
|
|
75
|
+
child: this.root,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Helper: Create horizontal split (top/bottom)
|
|
81
|
+
*/
|
|
82
|
+
static horizontalSplit(
|
|
83
|
+
ratio: number,
|
|
84
|
+
top: LayoutNode,
|
|
85
|
+
bottom: LayoutNode
|
|
86
|
+
): LayoutSplit {
|
|
87
|
+
return this.split('horizontal', ratio, top, bottom);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Helper: Create vertical split (left/right)
|
|
92
|
+
*/
|
|
93
|
+
static verticalSplit(
|
|
94
|
+
ratio: number,
|
|
95
|
+
left: LayoutNode,
|
|
96
|
+
right: LayoutNode
|
|
97
|
+
): LayoutSplit {
|
|
98
|
+
return this.split('vertical', ratio, left, right);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Quick builder functions
|
|
104
|
+
*/
|
|
105
|
+
export const L = {
|
|
106
|
+
item: LayoutBuilder.item,
|
|
107
|
+
split: LayoutBuilder.split,
|
|
108
|
+
h: LayoutBuilder.horizontalSplit,
|
|
109
|
+
v: LayoutBuilder.verticalSplit,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Example usage:
|
|
114
|
+
* ```ts
|
|
115
|
+
* const tree = new LayoutBuilder(1200, 800)
|
|
116
|
+
* .setRoot(
|
|
117
|
+
* L.v(0.2,
|
|
118
|
+
* L.item('Sidebar'),
|
|
119
|
+
* L.h(0.7,
|
|
120
|
+
* L.item('Editor'),
|
|
121
|
+
* L.item('Terminal')
|
|
122
|
+
* )
|
|
123
|
+
* )
|
|
124
|
+
* )
|
|
125
|
+
* .build();
|
|
126
|
+
* ```
|
|
127
|
+
*/
|