@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.
- package/dist/react/{AIRenderer-46WIFF2B.js → AIRenderer-PWWLWI6U.js} +84 -16
- package/dist/react/AIRenderer-PWWLWI6U.js.map +7 -0
- package/dist/react/{MermaidFlowchartRenderer-A26B2SM3.js → MermaidFlowchartRenderer-4YEMAYQC.js} +18 -3
- package/dist/react/MermaidFlowchartRenderer-4YEMAYQC.js.map +7 -0
- package/dist/react/{MermaidSequenceRenderer-OF4JK77D.js → MermaidSequenceRenderer-KCUHO7UI.js} +106 -4
- package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +7 -0
- package/dist/react/MermaidStructuredRenderer-GGOOGON5.js +365 -0
- package/dist/react/MermaidStructuredRenderer-GGOOGON5.js.map +7 -0
- package/dist/react/{SketchEngine-BCRJ62CV.js → SketchEngine-NPF2XN6Z.js} +2 -2
- package/dist/react/index.js +61 -127
- package/dist/react/index.js.map +3 -3
- package/package.json +1 -1
- package/src/core/AIRenderer.js +95 -11
- package/src/core/MermaidFlowchartRenderer.js +18 -3
- package/src/core/MermaidSequenceRenderer.js +116 -7
- package/src/core/MermaidStructuredRenderer.js +363 -0
- package/src/react/LixSketchCanvas.jsx +1 -1
- package/src/react/components/canvas/FindBar.jsx +1 -1
- package/src/react/components/modals/CanvasPropertiesModal.jsx +1 -1
- package/src/react/components/modals/LixScriptModal.jsx +22 -96
- package/src/react/components/sidebars/ArrowSidebar.jsx +6 -6
- package/src/react/components/sidebars/CircleSidebar.jsx +3 -3
- package/src/react/components/sidebars/FrameSidebar.jsx +1 -1
- package/src/react/components/sidebars/LineSidebar.jsx +4 -4
- package/src/react/components/sidebars/PaintbrushSidebar.jsx +5 -5
- package/src/react/components/sidebars/RectangleSidebar.jsx +4 -4
- package/src/react/components/sidebars/TextSidebar.jsx +5 -5
- package/src/react/store/useUIStore.js +1 -1
- package/dist/react/AIRenderer-46WIFF2B.js.map +0 -7
- package/dist/react/MermaidFlowchartRenderer-A26B2SM3.js.map +0 -7
- package/dist/react/MermaidSequenceRenderer-OF4JK77D.js.map +0 -7
- /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, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"');
|
|
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 —
|
|
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', '#
|
|
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 || '#
|
|
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 {
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
4
|
import useUIStore from '../../store/useUIStore'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
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((
|
|
24
|
-
const toggle = useUIStore((
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
if (e.key === 'Escape') toggle()
|
|
14
|
+
const onKeyDown = (event) => {
|
|
15
|
+
if (event.key === 'Escape') toggle()
|
|
35
16
|
}
|
|
36
|
-
document.addEventListener('keydown',
|
|
37
|
-
return () => document.removeEventListener('keydown',
|
|
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/
|
|
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={(
|
|
82
|
-
className="relative w-[
|
|
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
|
|
93
|
-
<
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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>
|