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.
Files changed (45) hide show
  1. package/PROJECT_SUMMARY.md +482 -0
  2. package/QUICKSTART.md +223 -0
  3. package/README.md +365 -0
  4. package/STATUS.md +315 -0
  5. package/USAGE_GUIDE.md +547 -0
  6. package/dist/chunk-CMGEAPA5.js +157 -0
  7. package/dist/chunk-CMGEAPA5.js.map +1 -0
  8. package/dist/chunk-QZHBI6ZI.js +5281 -0
  9. package/dist/chunk-QZHBI6ZI.js.map +1 -0
  10. package/dist/chunk-SEGVTWSK.js +44 -0
  11. package/dist/chunk-SEGVTWSK.js.map +1 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +248012 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/stdio-FWYJXSU7.js +101 -0
  16. package/dist/stdio-FWYJXSU7.js.map +1 -0
  17. package/dist/template-JDMAVVX7.js +9 -0
  18. package/dist/template-JDMAVVX7.js.map +1 -0
  19. package/examples/claude_desktop_config.example.json +12 -0
  20. package/examples/example-complex-editor.tsx +107 -0
  21. package/examples/example-dashboard.tsx +65 -0
  22. package/examples/example-ide-layout.tsx +53 -0
  23. package/examples/test-generator.ts +37 -0
  24. package/examples/test-parser.ts +121 -0
  25. package/examples/test-scenarios.md +496 -0
  26. package/package.json +42 -0
  27. package/src/index.ts +16 -0
  28. package/src/server.ts +314 -0
  29. package/src/tools/analyze-layout.ts +193 -0
  30. package/src/tools/apply-template.ts +125 -0
  31. package/src/tools/generate-layout.ts +235 -0
  32. package/src/tools/interactive-builder.ts +100 -0
  33. package/src/tools/validate-layout.ts +113 -0
  34. package/src/types/layout.ts +48 -0
  35. package/src/types/template.ts +181 -0
  36. package/src/utils/ast-parser.ts +264 -0
  37. package/src/utils/code-generator.ts +123 -0
  38. package/src/utils/layout-analyzer.ts +105 -0
  39. package/src/utils/layout-builder.ts +127 -0
  40. package/src/utils/validator.ts +263 -0
  41. package/stderr.log +1 -0
  42. package/stdout.log +0 -0
  43. package/test-mcp.js +27 -0
  44. package/tsconfig.json +29 -0
  45. 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
+ */