@elixpo/lixsketch 5.5.10 → 5.5.12

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 (32) hide show
  1. package/dist/react/{AIRenderer-46WIFF2B.js → AIRenderer-PWWLWI6U.js} +84 -16
  2. package/dist/react/AIRenderer-PWWLWI6U.js.map +7 -0
  3. package/dist/react/{MermaidFlowchartRenderer-A26B2SM3.js → MermaidFlowchartRenderer-4YEMAYQC.js} +18 -3
  4. package/dist/react/MermaidFlowchartRenderer-4YEMAYQC.js.map +7 -0
  5. package/dist/react/{MermaidSequenceRenderer-OF4JK77D.js → MermaidSequenceRenderer-KCUHO7UI.js} +106 -4
  6. package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +7 -0
  7. package/dist/react/MermaidStructuredRenderer-GGOOGON5.js +365 -0
  8. package/dist/react/MermaidStructuredRenderer-GGOOGON5.js.map +7 -0
  9. package/dist/react/{SketchEngine-BCRJ62CV.js → SketchEngine-NPF2XN6Z.js} +2 -2
  10. package/dist/react/index.js +61 -127
  11. package/dist/react/index.js.map +3 -3
  12. package/package.json +1 -1
  13. package/src/core/AIRenderer.js +95 -11
  14. package/src/core/MermaidFlowchartRenderer.js +18 -3
  15. package/src/core/MermaidSequenceRenderer.js +116 -7
  16. package/src/core/MermaidStructuredRenderer.js +363 -0
  17. package/src/react/LixSketchCanvas.jsx +1 -1
  18. package/src/react/components/canvas/FindBar.jsx +1 -1
  19. package/src/react/components/modals/CanvasPropertiesModal.jsx +1 -1
  20. package/src/react/components/modals/LixScriptModal.jsx +22 -96
  21. package/src/react/components/sidebars/ArrowSidebar.jsx +6 -6
  22. package/src/react/components/sidebars/CircleSidebar.jsx +3 -3
  23. package/src/react/components/sidebars/FrameSidebar.jsx +1 -1
  24. package/src/react/components/sidebars/LineSidebar.jsx +4 -4
  25. package/src/react/components/sidebars/PaintbrushSidebar.jsx +5 -5
  26. package/src/react/components/sidebars/RectangleSidebar.jsx +4 -4
  27. package/src/react/components/sidebars/TextSidebar.jsx +5 -5
  28. package/src/react/store/useUIStore.js +1 -1
  29. package/dist/react/AIRenderer-46WIFF2B.js.map +0 -7
  30. package/dist/react/MermaidFlowchartRenderer-A26B2SM3.js.map +0 -7
  31. package/dist/react/MermaidSequenceRenderer-OF4JK77D.js.map +0 -7
  32. /package/dist/react/{SketchEngine-BCRJ62CV.js.map → SketchEngine-NPF2XN6Z.js.map} +0 -0
@@ -0,0 +1,363 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Native renderers for Mermaid ER and chart diagrams.
4
+ *
5
+ * Preview is SVG, while canvas placement deliberately creates ordinary
6
+ * LixSketch shapes. That keeps entities, attributes, relationships, bars,
7
+ * points, and legend items independently selectable and editable.
8
+ */
9
+
10
+ const NS = 'http://www.w3.org/2000/svg';
11
+ const PALETTE = [
12
+ { fill: '#dfeee4', stroke: '#5f836c' },
13
+ { fill: '#f4e3d4', stroke: '#a97852' },
14
+ { fill: '#e9e1ef', stroke: '#7e6b91' },
15
+ { fill: '#f3edc9', stroke: '#9a8745' },
16
+ { fill: '#dcebed', stroke: '#5d7f82' },
17
+ { fill: '#f1dedc', stroke: '#9a6863' },
18
+ ];
19
+
20
+ function escapeXml(value) {
21
+ return String(value ?? '')
22
+ .replace(/&/g, '&')
23
+ .replace(/</g, '&lt;')
24
+ .replace(/>/g, '&gt;')
25
+ .replace(/"/g, '&quot;');
26
+ }
27
+
28
+ function isDark() {
29
+ return typeof document !== 'undefined' && document.body?.classList.contains('theme-dark');
30
+ }
31
+
32
+ function theme() {
33
+ return isDark()
34
+ ? { bg: '#20232a', text: '#f0eee8', line: '#aaa89f', frame: '#77766f', panel: '#292d34' }
35
+ : { bg: '#fbfaf6', text: '#343832', line: '#6e746c', frame: '#b9b7ac', panel: '#f5f2ea' };
36
+ }
37
+
38
+ function sourceLines(src) {
39
+ return src.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('%%'));
40
+ }
41
+
42
+ export function detectStructuredMermaid(src) {
43
+ const header = sourceLines(src)[0]?.toLowerCase() || '';
44
+ if (header === 'erdiagram') return 'er';
45
+ if (/^pie(?:\s|$)/.test(header) || /^xychart(?:-beta)?(?:\s|$)/.test(header)) return 'chart';
46
+ return null;
47
+ }
48
+
49
+ // ---------------------------------------------------------------------
50
+ // Entity relationship diagrams
51
+ // ---------------------------------------------------------------------
52
+
53
+ export function parseERDiagram(src) {
54
+ const lines = sourceLines(src);
55
+ if (lines[0]?.toLowerCase() !== 'erdiagram') return null;
56
+
57
+ const entityMap = new Map();
58
+ const relationships = [];
59
+ let current = null;
60
+
61
+ const ensureEntity = (name) => {
62
+ if (!entityMap.has(name)) entityMap.set(name, { name, attributes: [] });
63
+ return entityMap.get(name);
64
+ };
65
+
66
+ for (let index = 1; index < lines.length; index++) {
67
+ const line = lines[index];
68
+ const entityStart = line.match(/^([\w-]+)\s*\{$/);
69
+ if (entityStart) {
70
+ current = ensureEntity(entityStart[1]);
71
+ continue;
72
+ }
73
+ if (line === '}') {
74
+ current = null;
75
+ continue;
76
+ }
77
+ if (current) {
78
+ const attribute = line.match(/^(\S+)\s+(\S+)(?:\s+(.+))?$/);
79
+ if (attribute) {
80
+ current.attributes.push({
81
+ type: attribute[1],
82
+ name: attribute[2],
83
+ key: attribute[3] || '',
84
+ });
85
+ }
86
+ continue;
87
+ }
88
+
89
+ // CUSTOMER ||--o{ ORDER : places
90
+ const relation = line.match(/^([\w-]+)\s+([|o}{.]+)(?:--|\.\.)([|o}{.]+)\s+([\w-]+)\s*:\s*(.+)$/);
91
+ if (relation) {
92
+ ensureEntity(relation[1]);
93
+ ensureEntity(relation[4]);
94
+ relationships.push({
95
+ from: relation[1],
96
+ fromCardinality: relation[2],
97
+ toCardinality: relation[3],
98
+ to: relation[4],
99
+ label: relation[5],
100
+ });
101
+ }
102
+ }
103
+
104
+ if (entityMap.size === 0) return null;
105
+ return { type: 'erDiagram', title: 'Entity relationship diagram', entities: [...entityMap.values()], relationships };
106
+ }
107
+
108
+ function layoutER(diagram) {
109
+ const width = 230;
110
+ const headerHeight = 44;
111
+ const rowHeight = 30;
112
+ const columns = Math.max(1, Math.ceil(Math.sqrt(diagram.entities.length)));
113
+ return diagram.entities.map((entity, index) => ({
114
+ ...entity,
115
+ x: 60 + (index % columns) * 330,
116
+ y: 55 + Math.floor(index / columns) * 270,
117
+ width,
118
+ headerHeight,
119
+ rowHeight,
120
+ height: headerHeight + Math.max(1, entity.attributes.length) * rowHeight,
121
+ color: PALETTE[index % PALETTE.length],
122
+ }));
123
+ }
124
+
125
+ export function renderERPreviewSVG(diagram) {
126
+ if (!diagram?.entities?.length) return '';
127
+ const TK = theme();
128
+ const entities = layoutER(diagram);
129
+ const byName = new Map(entities.map(entity => [entity.name, entity]));
130
+ const maxX = Math.max(...entities.map(entity => entity.x + entity.width)) + 60;
131
+ const maxY = Math.max(...entities.map(entity => entity.y + entity.height)) + 55;
132
+ let content = `<rect width="${maxX}" height="${maxY}" rx="10" fill="${TK.bg}"/>`;
133
+ content += `<defs><marker id="er-arrow" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M1 1 L8 3.5 L1 6" fill="none" stroke="${TK.line}" stroke-width="1.4"/></marker></defs>`;
134
+
135
+ for (const relation of diagram.relationships) {
136
+ const from = byName.get(relation.from);
137
+ const to = byName.get(relation.to);
138
+ if (!from || !to) continue;
139
+ const x1 = from.x + from.width / 2;
140
+ const y1 = from.y + from.height / 2;
141
+ const x2 = to.x + to.width / 2;
142
+ const y2 = to.y + to.height / 2;
143
+ const mx = (x1 + x2) / 2;
144
+ const my = (y1 + y2) / 2;
145
+ content += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${TK.line}" stroke-width="1.6" marker-end="url(#er-arrow)"/>`;
146
+ content += `<text x="${mx}" y="${my - 8}" text-anchor="middle" fill="${TK.text}" font-size="12" font-family="lixFont">${escapeXml(`${relation.fromCardinality} ${relation.label} ${relation.toCardinality}`)}</text>`;
147
+ }
148
+
149
+ for (const entity of entities) {
150
+ content += `<g><rect x="${entity.x}" y="${entity.y}" width="${entity.width}" height="${entity.height}" rx="8" fill="${TK.panel}" stroke="${entity.color.stroke}" stroke-width="1.5"/>`;
151
+ content += `<rect x="${entity.x}" y="${entity.y}" width="${entity.width}" height="${entity.headerHeight}" rx="8" fill="${entity.color.fill}" stroke="${entity.color.stroke}" stroke-width="1.5"/>`;
152
+ content += `<text x="${entity.x + entity.width / 2}" y="${entity.y + 27}" text-anchor="middle" fill="#343832" font-size="14" font-weight="600" font-family="lixFont">${escapeXml(entity.name)}</text>`;
153
+ const attributes = entity.attributes.length ? entity.attributes : [{ type: '', name: 'No attributes', key: '' }];
154
+ attributes.forEach((attribute, row) => {
155
+ const y = entity.y + entity.headerHeight + row * entity.rowHeight;
156
+ content += `<line x1="${entity.x}" y1="${y}" x2="${entity.x + entity.width}" y2="${y}" stroke="${TK.frame}" stroke-width="0.8"/>`;
157
+ content += `<text x="${entity.x + 10}" y="${y + 20}" fill="${TK.text}" font-size="11" font-family="lixCode">${escapeXml([attribute.type, attribute.name, attribute.key].filter(Boolean).join(' '))}</text>`;
158
+ });
159
+ content += '</g>';
160
+ }
161
+ return `<svg xmlns="${NS}" width="600" height="450" viewBox="0 0 ${maxX} ${maxY}">${content}</svg>`;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------
165
+ // Pie and XY charts
166
+ // ---------------------------------------------------------------------
167
+
168
+ function numberList(value) {
169
+ return value.replace(/^\[|\]$/g, '').split(',').map(item => Number(item.trim())).filter(Number.isFinite);
170
+ }
171
+
172
+ export function parseChartDiagram(src) {
173
+ const lines = sourceLines(src);
174
+ const header = lines[0]?.toLowerCase() || '';
175
+ if (/^pie(?:\s|$)/.test(header)) {
176
+ let title = lines[0].replace(/^pie(?:\s+showData)?\s*/i, '').replace(/^title\s+/i, '').trim() || 'Pie chart';
177
+ const values = [];
178
+ for (let index = 1; index < lines.length; index++) {
179
+ const titleMatch = lines[index].match(/^title\s+(.+)$/i);
180
+ if (titleMatch) { title = titleMatch[1]; continue; }
181
+ const item = lines[index].match(/^"?(.+?)"?\s*:\s*(-?\d+(?:\.\d+)?)$/);
182
+ if (item) values.push({ label: item[1], value: Number(item[2]) });
183
+ }
184
+ return values.length ? { type: 'chart', kind: 'pie', title, categories: values.map(v => v.label), series: [{ name: title, values: values.map(v => v.value) }] } : null;
185
+ }
186
+ if (!/^xychart(?:-beta)?(?:\s|$)/.test(header)) return null;
187
+ let title = 'Chart';
188
+ let categories = [];
189
+ const series = [];
190
+ for (let index = 1; index < lines.length; index++) {
191
+ const titleMatch = lines[index].match(/^title\s+"?(.+?)"?$/i);
192
+ const axisMatch = lines[index].match(/^x-axis(?:\s+".*?")?\s+(\[.+\])/i);
193
+ const seriesMatch = lines[index].match(/^(bar|line)(?:\s+"?([^"\[]+)"?)?\s+(\[.+\])$/i);
194
+ if (titleMatch) title = titleMatch[1];
195
+ else if (axisMatch) categories = axisMatch[1].replace(/^\[|\]$/g, '').split(',').map(v => v.trim().replace(/^"|"$/g, ''));
196
+ else if (seriesMatch) series.push({ kind: seriesMatch[1].toLowerCase(), name: seriesMatch[2]?.trim() || seriesMatch[1], values: numberList(seriesMatch[3]) });
197
+ }
198
+ const maxItems = Math.max(0, ...series.map(item => item.values.length));
199
+ if (!categories.length) categories = Array.from({ length: maxItems }, (_, index) => String(index + 1));
200
+ return series.length ? { type: 'chart', kind: 'xy', title, categories, series } : null;
201
+ }
202
+
203
+ export function renderChartPreviewSVG(chart) {
204
+ if (!chart?.series?.length) return '';
205
+ const TK = theme();
206
+ const width = 720;
207
+ const height = 450;
208
+ const left = 70;
209
+ const top = 60;
210
+ const plotWidth = 580;
211
+ const plotHeight = 300;
212
+ const values = chart.series.flatMap(series => series.values);
213
+ const maximum = Math.max(1, ...values.map(Math.abs));
214
+ const categoryCount = Math.max(1, chart.categories.length);
215
+ const slot = plotWidth / categoryCount;
216
+ let content = `<rect width="${width}" height="${height}" rx="10" fill="${TK.bg}"/><text x="${width / 2}" y="32" text-anchor="middle" fill="${TK.text}" font-size="18" font-family="lixFont">${escapeXml(chart.title)}</text>`;
217
+ content += `<line x1="${left}" y1="${top}" x2="${left}" y2="${top + plotHeight}" stroke="${TK.line}"/><line x1="${left}" y1="${top + plotHeight}" x2="${left + plotWidth}" y2="${top + plotHeight}" stroke="${TK.line}"/>`;
218
+
219
+ chart.categories.forEach((category, index) => {
220
+ content += `<text x="${left + slot * (index + .5)}" y="${top + plotHeight + 24}" text-anchor="middle" fill="${TK.text}" font-size="11" font-family="lixFont">${escapeXml(category)}</text>`;
221
+ });
222
+
223
+ chart.series.forEach((series, seriesIndex) => {
224
+ const color = PALETTE[seriesIndex % PALETTE.length];
225
+ if (series.kind === 'line') {
226
+ const points = series.values.map((value, index) => `${left + slot * (index + .5)},${top + plotHeight - Math.abs(value) / maximum * plotHeight}`).join(' ');
227
+ content += `<polyline points="${points}" fill="none" stroke="${color.stroke}" stroke-width="3"/>`;
228
+ series.values.forEach((value, index) => content += `<circle cx="${left + slot * (index + .5)}" cy="${top + plotHeight - Math.abs(value) / maximum * plotHeight}" r="5" fill="${color.fill}" stroke="${color.stroke}" stroke-width="2"/>`);
229
+ } else {
230
+ const barWidth = Math.max(12, slot * .7 / chart.series.length);
231
+ series.values.forEach((value, index) => {
232
+ const barHeight = Math.abs(value) / maximum * plotHeight;
233
+ const x = left + slot * index + slot * .15 + seriesIndex * barWidth;
234
+ content += `<rect x="${x}" y="${top + plotHeight - barHeight}" width="${barWidth}" height="${barHeight}" rx="4" fill="${color.fill}" stroke="${color.stroke}" stroke-width="1.5"/><text x="${x + barWidth / 2}" y="${top + plotHeight - barHeight - 7}" text-anchor="middle" fill="${TK.text}" font-size="10" font-family="lixFont">${value}</text>`;
235
+ });
236
+ }
237
+ });
238
+ return `<svg xmlns="${NS}" width="600" height="450" viewBox="0 0 ${width} ${height}">${content}</svg>`;
239
+ }
240
+
241
+ // ---------------------------------------------------------------------
242
+ // Native canvas placement helpers
243
+ // ---------------------------------------------------------------------
244
+
245
+ function push(shape, frame) {
246
+ if (!shape) return null;
247
+ window.shapes.push(shape);
248
+ if (window.pushCreateAction) window.pushCreateAction(shape);
249
+ if (frame?.addShapeToFrame) frame.addShapeToFrame(shape);
250
+ return shape;
251
+ }
252
+
253
+ function canvasOrigin(width, height) {
254
+ const vb = window.currentViewBox || { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight };
255
+ return { x: vb.x + vb.width / 2 - width / 2, y: vb.y + vb.height / 2 - height / 2 };
256
+ }
257
+
258
+ function createFrame(x, y, width, height, name, type) {
259
+ const TK = theme();
260
+ const frame = new window.Frame(x - 45, y - 45, width + 90, height + 90, {
261
+ stroke: TK.frame, strokeWidth: 1, fill: 'transparent', opacity: .75, frameName: name,
262
+ });
263
+ frame._diagramType = type;
264
+ push(frame);
265
+ return frame;
266
+ }
267
+
268
+ export function renderEROnCanvas(diagram) {
269
+ if (!diagram?.entities?.length || !window.Rectangle || !window.Arrow || !window.Frame) return false;
270
+ const TK = theme();
271
+ const entities = layoutER(diagram);
272
+ const width = Math.max(...entities.map(entity => entity.x + entity.width)) + 40;
273
+ const height = Math.max(...entities.map(entity => entity.y + entity.height)) + 40;
274
+ const origin = canvasOrigin(width, height);
275
+ const frame = createFrame(origin.x, origin.y, width, height, diagram.title, 'mermaid-er');
276
+ const entityShapes = new Map();
277
+ let first = null;
278
+
279
+ for (const entity of entities) {
280
+ const x = origin.x + entity.x;
281
+ const y = origin.y + entity.y;
282
+ const header = push(new window.Rectangle(x, y, entity.width, entity.headerHeight, {
283
+ stroke: entity.color.stroke, strokeWidth: 1.5, fill: entity.color.fill, fillStyle: 'solid', roughness: 1,
284
+ label: entity.name, labelColor: '#343832', labelFontSize: 15,
285
+ }), frame);
286
+ if (!first) first = header;
287
+ entityShapes.set(entity.name, { shape: header, x, y, width: entity.width, height: entity.headerHeight });
288
+ const attributes = entity.attributes.length ? entity.attributes : [{ type: '', name: 'No attributes', key: '' }];
289
+ attributes.forEach((attribute, row) => push(new window.Rectangle(
290
+ x, y + entity.headerHeight + row * entity.rowHeight, entity.width, entity.rowHeight,
291
+ { stroke: TK.frame, strokeWidth: 1, fill: TK.panel, fillStyle: 'solid', roughness: .4,
292
+ label: [attribute.type, attribute.name, attribute.key].filter(Boolean).join(' '), labelColor: TK.text, labelFontSize: 11 }
293
+ ), frame));
294
+ }
295
+
296
+ for (const relation of diagram.relationships) {
297
+ const from = entityShapes.get(relation.from);
298
+ const to = entityShapes.get(relation.to);
299
+ if (!from || !to) continue;
300
+ const start = { x: from.x + from.width / 2, y: from.y + from.height / 2 };
301
+ const end = { x: to.x + to.width / 2, y: to.y + to.height / 2 };
302
+ const arrow = push(new window.Arrow(start, end, {
303
+ stroke: TK.line, strokeWidth: 1.5, roughness: 1,
304
+ label: `${relation.fromCardinality} ${relation.label} ${relation.toCardinality}`, labelColor: TK.text,
305
+ }), frame);
306
+ if (arrow && window.__autoAttach) {
307
+ window.__autoAttach(arrow, from.shape, true, start);
308
+ window.__autoAttach(arrow, to.shape, false, end);
309
+ }
310
+ }
311
+ if (first?.selectShape) { window.currentShape = first; first.selectShape(); }
312
+ return true;
313
+ }
314
+
315
+ export function renderChartOnCanvas(chart) {
316
+ if (!chart?.series?.length || !window.Rectangle || !window.Circle || !window.Line || !window.Frame) return false;
317
+ const TK = theme();
318
+ const width = 720;
319
+ const height = 450;
320
+ const origin = canvasOrigin(width, height);
321
+ const frame = createFrame(origin.x, origin.y, width, height, chart.title, 'mermaid-chart');
322
+ const left = origin.x + 70;
323
+ const top = origin.y + 65;
324
+ const plotWidth = 580;
325
+ const plotHeight = 300;
326
+ const values = chart.series.flatMap(series => series.values);
327
+ const maximum = Math.max(1, ...values.map(Math.abs));
328
+ const slot = plotWidth / Math.max(1, chart.categories.length);
329
+ let first = null;
330
+
331
+ push(new window.Line({ x: left, y: top }, { x: left, y: top + plotHeight }, { stroke: TK.line, strokeWidth: 1.5, roughness: 0 }), frame);
332
+ push(new window.Line({ x: left, y: top + plotHeight }, { x: left + plotWidth, y: top + plotHeight }, { stroke: TK.line, strokeWidth: 1.5, roughness: 0 }), frame);
333
+
334
+ chart.series.forEach((series, seriesIndex) => {
335
+ const color = PALETTE[seriesIndex % PALETTE.length];
336
+ if (series.kind === 'line') {
337
+ let previous = null;
338
+ series.values.forEach((value, index) => {
339
+ const point = { x: left + slot * (index + .5), y: top + plotHeight - Math.abs(value) / maximum * plotHeight };
340
+ const dot = push(new window.Circle(point.x, point.y, 8, 8, {
341
+ stroke: color.stroke, strokeWidth: 2, fill: color.fill, fillStyle: 'solid', roughness: .5,
342
+ label: String(value), labelColor: TK.text, labelFontSize: 9,
343
+ }), frame);
344
+ if (!first) first = dot;
345
+ if (previous) push(new window.Line(previous, point, { stroke: color.stroke, strokeWidth: 3, roughness: .5 }), frame);
346
+ previous = point;
347
+ });
348
+ } else {
349
+ const barWidth = Math.max(18, slot * .7 / chart.series.length);
350
+ series.values.forEach((value, index) => {
351
+ const barHeight = Math.max(8, Math.abs(value) / maximum * plotHeight);
352
+ const x = left + slot * index + slot * .15 + seriesIndex * barWidth;
353
+ const bar = push(new window.Rectangle(x, top + plotHeight - barHeight, barWidth, barHeight, {
354
+ stroke: color.stroke, strokeWidth: 1.5, fill: color.fill, fillStyle: 'solid', roughness: .7,
355
+ label: `${chart.categories[index] || index + 1}\n${value}`, labelColor: '#343832', labelFontSize: 11,
356
+ }), frame);
357
+ if (!first) first = bar;
358
+ });
359
+ }
360
+ });
361
+ if (first?.selectShape) { window.currentShape = first; first.selectShape(); }
362
+ return true;
363
+ }
@@ -236,7 +236,7 @@ export default function LixSketchCanvas({
236
236
  <div className="lixsketch-floating-header absolute top-2 right-2 z-[1000] flex items-center gap-1.5 font-[lixFont]">
237
237
  <button
238
238
  type="button"
239
- title="LixScript — write shapes in DSL"
239
+ title="LixScript MCP coming soon"
240
240
  onClick={() => useUIStore.getState().toggleAIModal?.()}
241
241
  className="w-9 h-9 flex items-center justify-center rounded-lg bg-surface border border-border-light text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
242
242
  >
@@ -49,7 +49,7 @@ export default function FindBar() {
49
49
  rect.setAttribute('width', w + 8)
50
50
  rect.setAttribute('height', h + 8)
51
51
  rect.setAttribute('fill', 'rgba(91, 87, 209, 0.15)')
52
- rect.setAttribute('stroke', '#5B57D1')
52
+ rect.setAttribute('stroke', '#7667a8')
53
53
  rect.setAttribute('stroke-width', '2')
54
54
  rect.setAttribute('stroke-dasharray', '4,2')
55
55
  rect.setAttribute('rx', '4')
@@ -320,7 +320,7 @@ export default function CanvasPropertiesModal() {
320
320
  >
321
321
  <span
322
322
  className="w-2.5 h-2.5 rounded-full shrink-0"
323
- style={{ background: user.color || '#5B57D1' }}
323
+ style={{ background: user.color || '#7667a8' }}
324
324
  />
325
325
  <span className="text-text-secondary text-[11px] flex-1 truncate">
326
326
  {user.displayName || user.name || `User ${i + 1}`}
@@ -1,85 +1,36 @@
1
1
  "use client"
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from 'react'
3
+ import { useEffect } from 'react'
4
4
  import useUIStore from '../../store/useUIStore'
5
5
 
6
- const SAMPLE = `# LixScript paste shapes / arrows in a simple DSL.
7
- # Hit Cmd/Ctrl+Enter to render onto the canvas.
8
-
9
- rect A "Idea" 100 100 220 90
10
- rect B "Plan" 400 100 220 90
11
- rect C "Ship" 700 100 220 90
12
-
13
- arrow A -> B
14
- arrow B -> C
15
- `
16
-
17
- /**
18
- * LixScript modal — takes a script the user types and parses it directly
19
- * into engine shapes via @elixpo/lixsketch's LixScriptParser. No AI
20
- * inference, no worker round-trip. Strictly client-side parsing.
21
- */
6
+ /** Keep the workspace entry point visible while the supported MCP-backed
7
+ * LixScript workflow is being prepared. No parser execution is exposed. */
22
8
  export default function LixScriptModal() {
23
- const open = useUIStore((s) => s.aiModalOpen)
24
- const toggle = useUIStore((s) => s.toggleAIModal)
25
- const [source, setSource] = useState(SAMPLE)
26
- const [busy, setBusy] = useState(false)
27
- const [status, setStatus] = useState(null) // { tone, message }
28
- const taRef = useRef(null)
9
+ const open = useUIStore((state) => state.aiModalOpen)
10
+ const toggle = useUIStore((state) => state.toggleAIModal)
29
11
 
30
12
  useEffect(() => {
31
13
  if (!open) return
32
- requestAnimationFrame(() => taRef.current?.focus())
33
- const onKey = (e) => {
34
- if (e.key === 'Escape') toggle()
14
+ const onKeyDown = (event) => {
15
+ if (event.key === 'Escape') toggle()
35
16
  }
36
- document.addEventListener('keydown', onKey)
37
- return () => document.removeEventListener('keydown', onKey)
17
+ document.addEventListener('keydown', onKeyDown)
18
+ return () => document.removeEventListener('keydown', onKeyDown)
38
19
  }, [open, toggle])
39
20
 
40
- const render = useCallback(async () => {
41
- if (busy) return
42
- setBusy(true)
43
- setStatus(null)
44
- try {
45
- // Lazy-import the parser. Pulling it eagerly would bring the whole
46
- // shape graph into the entry chunk (see SceneSerializer comment
47
- // in LixSketchCanvas) and crash with "svg is not defined" before
48
- // engine.init() has wired the engine globals.
49
- const mod = await import('../../../core/LixScriptParser.js')
50
- const parsed = mod.parseLixScript(source)
51
- const resolved = mod.resolveShapeRefs ? mod.resolveShapeRefs(parsed) : parsed
52
- mod.renderLixScript(resolved)
53
- setStatus({ tone: 'success', message: `Rendered ${Array.isArray(resolved) ? resolved.length : '?'} elements.` })
54
- // Auto-close shortly after a successful render.
55
- setTimeout(() => { toggle() }, 700)
56
- } catch (err) {
57
- console.warn('[LixScriptModal] parse failed:', err)
58
- setStatus({ tone: 'error', message: err?.message || 'Parse failed.' })
59
- } finally {
60
- setBusy(false)
61
- }
62
- }, [source, busy, toggle])
63
-
64
- const handleKeyDown = useCallback((e) => {
65
- if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
66
- e.preventDefault()
67
- render()
68
- }
69
- }, [render])
70
-
71
21
  if (!open) return null
72
22
 
73
23
  return (
74
24
  <div
75
- className="fixed inset-0 z-[1100] flex items-center justify-center bg-black/60 backdrop-blur-sm font-[lixFont]"
25
+ className="fixed inset-0 z-[1100] flex items-center justify-center bg-black/40 backdrop-blur-sm font-[lixFont]"
76
26
  onClick={toggle}
77
27
  role="dialog"
78
28
  aria-modal="true"
29
+ aria-labelledby="lixscript-coming-soon-title"
79
30
  >
80
31
  <div
81
- onClick={(e) => e.stopPropagation()}
82
- className="relative w-[560px] max-w-[94vw] bg-surface border border-border-light rounded-2xl p-5 shadow-2xl flex flex-col gap-3"
32
+ onClick={(event) => event.stopPropagation()}
33
+ className="relative w-[520px] max-w-[94vw] bg-surface border border-border-light rounded-2xl px-8 py-10 shadow-2xl text-center"
83
34
  >
84
35
  <button
85
36
  onClick={toggle}
@@ -89,41 +40,16 @@ export default function LixScriptModal() {
89
40
  <i className="bx bx-x text-lg" />
90
41
  </button>
91
42
 
92
- <div className="flex items-center gap-2">
93
- <div className="w-8 h-8 rounded-lg bg-accent-blue/10 border border-accent-blue/30 flex items-center justify-center">
94
- <i className="bx bx-code-alt text-accent-blue" />
95
- </div>
96
- <div className="flex-1">
97
- <h2 className="text-text-primary text-sm font-medium">LixScript</h2>
98
- <p className="text-text-dim text-[11px]">Type shapes + arrows, render directly. No AI involved.</p>
99
- </div>
100
- </div>
101
-
102
- <textarea
103
- ref={taRef}
104
- value={source}
105
- onChange={(e) => setSource(e.target.value)}
106
- onKeyDown={handleKeyDown}
107
- spellCheck={false}
108
- className="w-full h-[260px] resize-none bg-surface-card border border-border-light rounded-lg px-3 py-2 text-[12px] text-text-primary font-[lixCode] outline-none focus:border-accent-blue/50"
109
- />
110
-
111
- <div className="flex items-center justify-between">
112
- {status ? (
113
- <span className={`text-[11px] ${status.tone === 'error' ? 'text-red-400' : 'text-green-400'}`}>
114
- {status.message}
115
- </span>
116
- ) : (
117
- <span className="text-text-dim text-[11px]">Cmd/Ctrl+Enter to render · Esc to close</span>
118
- )}
119
- <button
120
- onClick={render}
121
- disabled={busy}
122
- className="px-3 py-1.5 rounded-lg bg-accent-blue text-text-primary text-[12px] font-medium hover:bg-accent-blue-hover transition-colors disabled:opacity-50 disabled:pointer-events-none"
123
- >
124
- {busy ? 'Rendering…' : 'Render'}
125
- </button>
43
+ <div className="mx-auto mb-5 w-12 h-12 rounded-xl bg-accent-blue/10 border border-accent-blue/25 flex items-center justify-center">
44
+ <i className="bx bx-plug text-2xl text-accent-blue" />
126
45
  </div>
46
+ <span className="inline-flex px-2.5 py-1 rounded-full bg-[#a97852]/15 text-[#8f6244] text-[10px] font-semibold uppercase tracking-wider">
47
+ Coming soon
48
+ </span>
49
+ <h2 id="lixscript-coming-soon-title" className="mt-4 text-text-primary text-xl font-medium">LixScript MCP</h2>
50
+ <p className="mt-2 text-text-muted text-sm leading-relaxed">
51
+ LixScript is being prepared as the programmable MCP interface for LixSketch. This workspace panel will become available with the supported platform integration.
52
+ </p>
127
53
  </div>
128
54
  </div>
129
55
  )
@@ -23,7 +23,7 @@ function ColorGrid({ colors, selected, onSelect }) {
23
23
  <div className="grid grid-cols-4 gap-1.5">
24
24
  {colors.map((c) => (
25
25
  <button key={c} onClick={() => onSelect(c)}
26
- className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${selected === c ? 'border-[#5B57D1] scale-110' : 'border-white/[0.08] hover:border-white/20'}`}
26
+ className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${selected === c ? 'border-[#7667a8] scale-110' : 'border-white/[0.08] hover:border-white/20'}`}
27
27
  style={{ backgroundColor: c }}
28
28
  />
29
29
  ))}
@@ -87,7 +87,7 @@ export default function ArrowSidebar() {
87
87
  <div className="flex items-center gap-1">
88
88
  {HEAD_STYLES.map((h) => (
89
89
  <button key={h.value} onClick={() => updateHead(h.value)}
90
- className={`w-10 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${headStyle === h.value ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
90
+ className={`w-10 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${headStyle === h.value ? 'bg-[#7667a8]/20 text-[#7667a8]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
91
91
  >
92
92
  <SvgIcon svg={h.svg} />
93
93
  </button>
@@ -111,7 +111,7 @@ export default function ArrowSidebar() {
111
111
  <div className="flex items-center gap-1">
112
112
  {[1, 2, 4, 7].map((w) => (
113
113
  <button key={w} onClick={() => updateThickness(w)}
114
- className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${thickness === w ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
114
+ className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${thickness === w ? 'bg-[#7667a8]/20 text-[#7667a8]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
115
115
  >
116
116
  <div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
117
117
  </button>
@@ -126,7 +126,7 @@ export default function ArrowSidebar() {
126
126
  <div className="flex items-center gap-1">
127
127
  {[{ v: 'solid', d: '' }, { v: 'dashed', d: '6 4' }, { v: 'dotted', d: '2 3' }].map((s) => (
128
128
  <button key={s.v} onClick={() => updateOutline(s.v)}
129
- className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${outlineStyle === s.v ? 'bg-[#5B57D1]/20' : 'hover:bg-white/[0.06]'}`}
129
+ className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${outlineStyle === s.v ? 'bg-[#7667a8]/20' : 'hover:bg-white/[0.06]'}`}
130
130
  >
131
131
  <svg width="28" height="4" viewBox="0 0 28 4"><line x1="0" y1="2" x2="28" y2="2" stroke="#fff" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" /></svg>
132
132
  </button>
@@ -146,7 +146,7 @@ export default function ArrowSidebar() {
146
146
  { v: 'elbow', i: 'bxs-network-chart', l: 'Elbow' },
147
147
  ].map((a) => (
148
148
  <button key={a.v} onClick={() => updateType(a.v)}
149
- className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${arrowType === a.v ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
149
+ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${arrowType === a.v ? 'bg-[#7667a8] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
150
150
  >
151
151
  <i className={`bx ${a.i} text-sm`} /> {a.l}
152
152
  </button>
@@ -158,7 +158,7 @@ export default function ArrowSidebar() {
158
158
  <div className="flex items-center gap-1">
159
159
  {[{ v: 8, l: 'Lo' }, { v: 20, l: 'Md' }, { v: 40, l: 'Hi' }].map((c) => (
160
160
  <button key={c.v} onClick={() => updateCurvature(c.v)}
161
- className={`flex-1 py-1 rounded-md text-xs text-center transition-all duration-100 ${curvature === c.v ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
161
+ className={`flex-1 py-1 rounded-md text-xs text-center transition-all duration-100 ${curvature === c.v ? 'bg-[#7667a8]/20 text-[#7667a8]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
162
162
  >{c.l}</button>
163
163
  ))}
164
164
  </div>
@@ -89,7 +89,7 @@ export default function CircleSidebar() {
89
89
  <div className="flex items-center gap-1">
90
90
  {[1, 2, 4, 7].map((w) => (
91
91
  <button key={w} onClick={() => updateThickness(w)}
92
- className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${thickness === w ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-muted hover:bg-white/[0.06]'}`}
92
+ className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${thickness === w ? 'bg-[#7667a8]/20 text-[#7667a8]' : 'text-text-muted hover:bg-white/[0.06]'}`}
93
93
  >
94
94
  <div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
95
95
  </button>
@@ -104,7 +104,7 @@ export default function CircleSidebar() {
104
104
  <div className="flex items-center gap-1">
105
105
  {[{ v: 'solid', d: '' }, { v: 'dashed', d: '6 4' }, { v: 'dotted', d: '2 3' }].map((s) => (
106
106
  <button key={s.v} onClick={() => updateStyle(s.v)}
107
- className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${lineStyle === s.v ? 'bg-[#5B57D1]/20' : 'hover:bg-white/[0.06]'}`}
107
+ className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${lineStyle === s.v ? 'bg-[#7667a8]/20' : 'hover:bg-white/[0.06]'}`}
108
108
  >
109
109
  <svg width="28" height="4" viewBox="0 0 28 4"><line x1="0" y1="2" x2="28" y2="2" stroke="#fff" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" /></svg>
110
110
  </button>
@@ -119,7 +119,7 @@ export default function CircleSidebar() {
119
119
  <div className="flex flex-col gap-0.5">
120
120
  {FILLS.map((f) => (
121
121
  <button key={f.value} onClick={() => updateFill(f.value)}
122
- className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${fillStyle === f.value ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
122
+ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${fillStyle === f.value ? 'bg-[#7667a8] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
123
123
  >
124
124
  <span className="w-1.5 h-1.5 rounded-full bg-current" />
125
125
  {t(f.label)}
@@ -101,7 +101,7 @@ export default function FrameSidebar() {
101
101
  type="text"
102
102
  value={frameName}
103
103
  onChange={updateName}
104
- className="w-32 px-2.5 py-1.5 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-xs outline-none focus:border-[#5B57D1]/50 transition-all duration-150 font-[lixFont]"
104
+ className="w-32 px-2.5 py-1.5 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-xs outline-none focus:border-[#7667a8]/50 transition-all duration-150 font-[lixFont]"
105
105
  spellCheck={false}
106
106
  />
107
107
  </ToolbarButton>