@elixpo/lixsketch 5.5.9 → 5.5.11

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 (43) 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/{CopyPaste-4CKJRHJ3.js → CopyPaste-SA72NDFN.js} +2 -2
  4. package/dist/react/{EventDispatcher-AD2BPPPS.js → EventDispatcher-BZQRHROG.js} +2 -2
  5. package/dist/react/{MermaidFlowchartRenderer-A26B2SM3.js → MermaidFlowchartRenderer-4YEMAYQC.js} +18 -3
  6. package/dist/react/MermaidFlowchartRenderer-4YEMAYQC.js.map +7 -0
  7. package/dist/react/{MermaidSequenceRenderer-OF4JK77D.js → MermaidSequenceRenderer-KCUHO7UI.js} +106 -4
  8. package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +7 -0
  9. package/dist/react/MermaidStructuredRenderer-GGOOGON5.js +365 -0
  10. package/dist/react/MermaidStructuredRenderer-GGOOGON5.js.map +7 -0
  11. package/dist/react/{SceneSerializer-4U4B24JM.js → SceneSerializer-ZPHV6IUP.js} +2 -2
  12. package/dist/react/{SketchEngine-XWWTA2MT.js → SketchEngine-NPF2XN6Z.js} +7 -7
  13. package/dist/react/TextShape-U6ZZKB35.js +8 -0
  14. package/dist/react/{chunk-QXFBG7OM.js → chunk-42O6W7DF.js} +18 -1
  15. package/dist/react/chunk-42O6W7DF.js.map +7 -0
  16. package/dist/react/{chunk-YVCENC7J.js → chunk-5QIT2WBE.js} +19 -116
  17. package/dist/react/chunk-5QIT2WBE.js.map +7 -0
  18. package/dist/react/index.js +34 -100
  19. package/dist/react/index.js.map +3 -3
  20. package/dist/react/{textTool-KY4AYUTC.js → textTool-HY5NMM7H.js} +2 -2
  21. package/package.json +1 -1
  22. package/src/core/AIRenderer.js +95 -11
  23. package/src/core/MermaidFlowchartRenderer.js +18 -3
  24. package/src/core/MermaidSequenceRenderer.js +116 -7
  25. package/src/core/MermaidStructuredRenderer.js +363 -0
  26. package/src/react/LixSketchCanvas.jsx +1 -1
  27. package/src/react/components/modals/LixScriptModal.jsx +22 -96
  28. package/src/react/components/sidebars/TextSidebar.jsx +1 -1
  29. package/src/react/store/useUIStore.js +1 -1
  30. package/src/shapes/TextShape.js +19 -0
  31. package/src/tools/textTool.js +23 -157
  32. package/dist/react/AIRenderer-46WIFF2B.js.map +0 -7
  33. package/dist/react/MermaidFlowchartRenderer-A26B2SM3.js.map +0 -7
  34. package/dist/react/MermaidSequenceRenderer-OF4JK77D.js.map +0 -7
  35. package/dist/react/TextShape-54ZJ66E3.js +0 -8
  36. package/dist/react/chunk-QXFBG7OM.js.map +0 -7
  37. package/dist/react/chunk-YVCENC7J.js.map +0 -7
  38. /package/dist/react/{CopyPaste-4CKJRHJ3.js.map → CopyPaste-SA72NDFN.js.map} +0 -0
  39. /package/dist/react/{EventDispatcher-AD2BPPPS.js.map → EventDispatcher-BZQRHROG.js.map} +0 -0
  40. /package/dist/react/{SceneSerializer-4U4B24JM.js.map → SceneSerializer-ZPHV6IUP.js.map} +0 -0
  41. /package/dist/react/{SketchEngine-XWWTA2MT.js.map → SketchEngine-NPF2XN6Z.js.map} +0 -0
  42. /package/dist/react/{TextShape-54ZJ66E3.js.map → TextShape-U6ZZKB35.js.map} +0 -0
  43. /package/dist/react/{textTool-KY4AYUTC.js.map → textTool-HY5NMM7H.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
  >
@@ -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
  )
@@ -27,7 +27,7 @@ export default function TextSidebar() {
27
27
  const activeTool = useSketchStore((s) => s.activeTool)
28
28
  const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
29
29
  const [textColor, setTextColor] = useState('#fff')
30
- const [fontSize, setFontSize] = useState('M')
30
+ const [fontSize, setFontSize] = useState('S')
31
31
  const [font, setFont] = useState('lixFont')
32
32
  const [codeMode, setCodeMode] = useState(false)
33
33
  const [language, setLanguage] = useState('javascript')
@@ -158,7 +158,7 @@ const useUIStore = create((set, get) => ({
158
158
 
159
159
  // --- Theme ---
160
160
  // Issue #38 bug #1: light by default. Dark stays available via toggle.
161
- theme: 'dark',
161
+ theme: 'light',
162
162
  setTheme: (newTheme) => {
163
163
  const prev = get().theme
164
164
  const resolve = (t) => t === 'system'
@@ -25,9 +25,28 @@ function updateSelectionFeedback() {}
25
25
  function deselectElement() { selectedElement = null; }
26
26
  function selectElement(el) { selectedElement = el; }
27
27
 
28
+ function removeLegacySoftWrap(groupElement) {
29
+ const textElement = groupElement.querySelector('text');
30
+ if (!textElement || !textElement.hasAttribute('data-wrap-width')) return;
31
+ const source = textElement.getAttribute('data-wrap-source');
32
+ if (source === null) return;
33
+
34
+ while (textElement.firstChild) textElement.removeChild(textElement.firstChild);
35
+ const x = textElement.getAttribute('x') || 0;
36
+ source.split('\n').forEach((line, index) => {
37
+ const tspan = textElement.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'tspan');
38
+ tspan.setAttribute('x', x);
39
+ tspan.setAttribute('dy', index === 0 ? '0' : '1.2em');
40
+ tspan.textContent = line || ' ';
41
+ textElement.appendChild(tspan);
42
+ });
43
+ textElement.removeAttribute('data-wrap-width');
44
+ }
45
+
28
46
  class TextShape {
29
47
  constructor(groupElement) {
30
48
  this.group = groupElement;
49
+ removeLegacySoftWrap(groupElement);
31
50
  this.shapeName = 'text';
32
51
  this.shapeID = groupElement.getAttribute('id') || `text-${String(Date.now()).slice(0, 8)}-${Math.floor(Math.random() * 10000)}`;
33
52