@elixpo/lixsketch 5.5.11 → 5.5.13
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-PWWLWI6U.js → AIRenderer-ATIKGFB4.js} +90 -8
- package/dist/react/{AIRenderer-PWWLWI6U.js.map → AIRenderer-ATIKGFB4.js.map} +2 -2
- package/dist/react/{CopyPaste-SA72NDFN.js → CopyPaste-VUM5WRDX.js} +2 -2
- package/dist/react/FreehandStroke-JKTCTOKU.js +8 -0
- package/dist/react/{GraphEngine-IHRVGUGG.js → GraphEngine-KQXHNTZQ.js} +39 -1
- package/dist/react/{GraphEngine-IHRVGUGG.js.map → GraphEngine-KQXHNTZQ.js.map} +2 -2
- package/dist/react/{MermaidStructuredRenderer-GGOOGON5.js → MermaidStructuredRenderer-26WGA6YD.js} +152 -25
- package/dist/react/MermaidStructuredRenderer-26WGA6YD.js.map +7 -0
- package/dist/react/{SceneSerializer-ZPHV6IUP.js → SceneSerializer-3RWFYSLY.js} +2 -2
- package/dist/react/{SketchEngine-NPF2XN6Z.js → SketchEngine-WW5LIHNT.js} +6 -6
- package/dist/react/{chunk-YKJUBFHE.js → chunk-ABZEV54U.js} +23 -2
- package/dist/react/chunk-ABZEV54U.js.map +7 -0
- package/dist/react/index.js +55 -47
- package/dist/react/index.js.map +2 -2
- package/dist/react/styles.css +10 -3
- package/package.json +1 -1
- package/src/core/AIRenderer.js +104 -6
- package/src/core/GraphEngine.js +45 -0
- package/src/core/MermaidStructuredRenderer.js +162 -22
- package/src/react/LixSketchCanvas.jsx +4 -0
- package/src/react/components/AppMenu.jsx +2 -2
- package/src/react/components/Toolbar.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/CommandPalette.jsx +3 -3
- 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 +2 -2
- package/src/react/components/sidebars/IconSidebar.jsx +2 -2
- 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/ShapeSidebar.jsx +1 -1
- package/src/react/components/sidebars/TextSidebar.jsx +5 -5
- package/src/react/store/useSketchStore.js +7 -2
- package/src/react/store/useUIStore.js +3 -4
- package/src/react/styles.css +12 -3
- package/src/shapes/FreehandStroke.js +28 -3
- package/dist/react/FreehandStroke-CK24NUT3.js +0 -8
- package/dist/react/MermaidStructuredRenderer-GGOOGON5.js.map +0 -7
- package/dist/react/chunk-YKJUBFHE.js.map +0 -7
- /package/dist/react/{CopyPaste-SA72NDFN.js.map → CopyPaste-VUM5WRDX.js.map} +0 -0
- /package/dist/react/{FreehandStroke-CK24NUT3.js.map → FreehandStroke-JKTCTOKU.js.map} +0 -0
- /package/dist/react/{SceneSerializer-ZPHV6IUP.js.map → SceneSerializer-3RWFYSLY.js.map} +0 -0
- /package/dist/react/{SketchEngine-NPF2XN6Z.js.map → SketchEngine-WW5LIHNT.js.map} +0 -0
package/dist/react/styles.css
CHANGED
|
@@ -6487,15 +6487,22 @@
|
|
|
6487
6487
|
|
|
6488
6488
|
/* src/react/styles.css */
|
|
6489
6489
|
.lixsketch-canvas-root {
|
|
6490
|
+
--lix-icon-filter: brightness(0) invert(1);
|
|
6490
6491
|
position: relative;
|
|
6491
6492
|
width: 100%;
|
|
6492
6493
|
height: 100%;
|
|
6493
|
-
background: var(--lixsketch-bg, #
|
|
6494
|
-
color: var(--lixsketch-fg, #
|
|
6494
|
+
background: var(--lixsketch-bg, #15111f);
|
|
6495
|
+
color: var(--lixsketch-fg, #f5effb);
|
|
6495
6496
|
font-family: var(--lixsketch-font, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
6496
6497
|
overflow: hidden;
|
|
6497
6498
|
isolation: isolate;
|
|
6498
6499
|
}
|
|
6500
|
+
.theme-light .lixsketch-canvas-root {
|
|
6501
|
+
--lix-icon-filter: none;
|
|
6502
|
+
}
|
|
6503
|
+
.theme-dark .lixsketch-canvas-root {
|
|
6504
|
+
--lix-icon-filter: brightness(0) invert(1);
|
|
6505
|
+
}
|
|
6499
6506
|
.lixsketch-canvas-svg {
|
|
6500
6507
|
position: absolute;
|
|
6501
6508
|
inset: 0;
|
|
@@ -6511,7 +6518,7 @@
|
|
|
6511
6518
|
display: flex;
|
|
6512
6519
|
align-items: center;
|
|
6513
6520
|
justify-content: center;
|
|
6514
|
-
background: var(--lixsketch-bg, #
|
|
6521
|
+
background: var(--lixsketch-bg, #15111f);
|
|
6515
6522
|
z-index: 50;
|
|
6516
6523
|
}
|
|
6517
6524
|
.lixsketch-canvas-spinner {
|
package/package.json
CHANGED
package/src/core/AIRenderer.js
CHANGED
|
@@ -45,12 +45,12 @@ export function parseMermaid(src) {
|
|
|
45
45
|
const classAssigns = []; // { nodeIds: [...], className }
|
|
46
46
|
const nodeStyles = new Map(); // Mermaid `style A fill:...,stroke:...`
|
|
47
47
|
const palette = [
|
|
48
|
-
{ fill: '#
|
|
49
|
-
{ fill: '#
|
|
50
|
-
{ fill: '#
|
|
51
|
-
{ fill: '#
|
|
52
|
-
{ fill: '#
|
|
53
|
-
{ fill: '#
|
|
48
|
+
{ fill: '#eee9f7', stroke: '#7667a8' },
|
|
49
|
+
{ fill: '#e6def2', stroke: '#8875b5' },
|
|
50
|
+
{ fill: '#ddd2eb', stroke: '#6f5f9c' },
|
|
51
|
+
{ fill: '#f2edf8', stroke: '#9a88bf' },
|
|
52
|
+
{ fill: '#e2daf0', stroke: '#806cab' },
|
|
53
|
+
{ fill: '#ebe4f5', stroke: '#705d9c' },
|
|
54
54
|
];
|
|
55
55
|
|
|
56
56
|
function parseStyleProps(value) {
|
|
@@ -1349,6 +1349,103 @@ export function generateFramePreviewSVG(frame, width = 500, height = 350) {
|
|
|
1349
1349
|
</svg>`;
|
|
1350
1350
|
}
|
|
1351
1351
|
|
|
1352
|
+
// ============================================================
|
|
1353
|
+
// CANVAS BACKGROUND CONTRAST
|
|
1354
|
+
// ============================================================
|
|
1355
|
+
|
|
1356
|
+
function colorRgb(value) {
|
|
1357
|
+
if (!value || typeof value !== 'string') return null;
|
|
1358
|
+
const rgbMatch = value.trim().match(/^rgba?\(\s*(\d+(?:\.\d+)?)\s*[, ]\s*(\d+(?:\.\d+)?)\s*[, ]\s*(\d+(?:\.\d+)?)/i);
|
|
1359
|
+
if (rgbMatch) {
|
|
1360
|
+
return {
|
|
1361
|
+
r: Math.min(255, Number(rgbMatch[1])),
|
|
1362
|
+
g: Math.min(255, Number(rgbMatch[2])),
|
|
1363
|
+
b: Math.min(255, Number(rgbMatch[3])),
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
let hex = value.trim().replace('#', '');
|
|
1367
|
+
if (hex.length === 3) hex = hex.split('').map(char => char + char).join('');
|
|
1368
|
+
if (!/^[0-9a-f]{6}$/i.test(hex)) return null;
|
|
1369
|
+
return {
|
|
1370
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
1371
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
1372
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function relativeLuminance(value) {
|
|
1377
|
+
const rgb = colorRgb(value);
|
|
1378
|
+
if (!rgb) return null;
|
|
1379
|
+
const channel = component => {
|
|
1380
|
+
const normalized = component / 255;
|
|
1381
|
+
return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
|
|
1382
|
+
};
|
|
1383
|
+
return .2126 * channel(rgb.r) + .7152 * channel(rgb.g) + .0722 * channel(rgb.b);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function contrastRatio(a, b) {
|
|
1387
|
+
const first = relativeLuminance(a);
|
|
1388
|
+
const second = relativeLuminance(b);
|
|
1389
|
+
if (first == null || second == null) return 1;
|
|
1390
|
+
return (Math.max(first, second) + .05) / (Math.min(first, second) + .05);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function readableForeground(background) {
|
|
1394
|
+
return contrastRatio('#34304a', background) >= contrastRatio('#f6f2fb', background)
|
|
1395
|
+
? '#34304a'
|
|
1396
|
+
: '#f6f2fb';
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function readableColor(color, background, minimum = 3) {
|
|
1400
|
+
if (!colorRgb(color) || contrastRatio(color, background) >= minimum) return color;
|
|
1401
|
+
return readableForeground(background);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
export function adaptCanvasContrast(background) {
|
|
1405
|
+
if (typeof window === 'undefined' || !Array.isArray(window.shapes)) return;
|
|
1406
|
+
const foreground = readableForeground(background);
|
|
1407
|
+
const muted = foreground === '#34304a' ? '#716a7d' : '#c8bddb';
|
|
1408
|
+
|
|
1409
|
+
window.__canvasContrastColor = foreground;
|
|
1410
|
+
window.__ensureCanvasContrast = readableColor;
|
|
1411
|
+
|
|
1412
|
+
for (const shape of window.shapes) {
|
|
1413
|
+
if (!shape) continue;
|
|
1414
|
+
if (typeof shape.adaptToBackground === 'function') {
|
|
1415
|
+
shape.adaptToBackground(background, foreground, muted);
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const owner = shape.parentFrame;
|
|
1420
|
+
const generated = !!(shape._diagramType || shape._frameType === 'graph' || owner?._diagramType || owner?._frameType === 'graph');
|
|
1421
|
+
const fill = shape.options?.fill;
|
|
1422
|
+
const solidFill = fill && fill !== 'transparent' && fill !== 'none';
|
|
1423
|
+
|
|
1424
|
+
if (shape.options?.closedFill) {
|
|
1425
|
+
shape.options.outlineStroke = background;
|
|
1426
|
+
if (typeof shape.draw === 'function') shape.draw();
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (shape.options?.stroke && (generated || ['#fff', '#ffffff', '#000', '#000000', '#1a1a2e'].includes(shape.options.stroke.toLowerCase()))) {
|
|
1431
|
+
shape.options.stroke = readableColor(shape.options.stroke, background, 3);
|
|
1432
|
+
}
|
|
1433
|
+
if (shape.shapeName === 'frame' && generated && shape.options) {
|
|
1434
|
+
shape.options.stroke = muted;
|
|
1435
|
+
}
|
|
1436
|
+
if ('labelColor' in shape && generated) {
|
|
1437
|
+
shape.labelColor = solidFill ? readableColor(shape.labelColor || foreground, fill, 4.5) : foreground;
|
|
1438
|
+
}
|
|
1439
|
+
if (shape.group && generated) {
|
|
1440
|
+
shape.group.querySelectorAll('text').forEach(text => {
|
|
1441
|
+
const current = text.getAttribute('fill') || foreground;
|
|
1442
|
+
text.setAttribute('fill', solidFill ? readableColor(current, fill, 4.5) : readableColor(current, background, 4.5));
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
if (typeof shape.draw === 'function') shape.draw();
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1352
1449
|
// ============================================================
|
|
1353
1450
|
// INIT
|
|
1354
1451
|
// ============================================================
|
|
@@ -1358,6 +1455,7 @@ export function initAIRenderer() {
|
|
|
1358
1455
|
// Graph, LixScript) can wire arrow endpoints into shapes without
|
|
1359
1456
|
// import-cycling through this large module.
|
|
1360
1457
|
window.__autoAttach = autoAttach;
|
|
1458
|
+
window.__adaptCanvasContrast = adaptCanvasContrast;
|
|
1361
1459
|
|
|
1362
1460
|
// Lazy-load sequence renderer
|
|
1363
1461
|
let _seqParser = null;
|
package/src/core/GraphEngine.js
CHANGED
|
@@ -140,6 +140,48 @@ function renderGraphOnCanvas(equations, settings) {
|
|
|
140
140
|
window.updateAttachedArrows(this);
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
+
adaptToBackground(background, foreground, muted) {
|
|
144
|
+
const ensureContrast = window.__ensureCanvasContrast || ((color) => color);
|
|
145
|
+
|
|
146
|
+
this.group.querySelectorAll('rect').forEach((rect) => {
|
|
147
|
+
const fill = rect.getAttribute('fill');
|
|
148
|
+
const stroke = rect.getAttribute('stroke');
|
|
149
|
+
if (fill && fill !== 'none') rect.setAttribute('fill', background);
|
|
150
|
+
if (stroke && stroke !== 'none') {
|
|
151
|
+
rect.setAttribute('stroke', muted);
|
|
152
|
+
rect.setAttribute('stroke-opacity', '0.45');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this.group.querySelectorAll('line').forEach((line) => {
|
|
157
|
+
const width = Number.parseFloat(line.getAttribute('stroke-width') || '1');
|
|
158
|
+
line.setAttribute('stroke', foreground);
|
|
159
|
+
line.setAttribute('stroke-opacity', width <= 0.5 ? '0.18' : '0.5');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.group.querySelectorAll('path[stroke]').forEach((path) => {
|
|
163
|
+
const original = path.dataset.graphColor || path.getAttribute('stroke');
|
|
164
|
+
if (!original || original === 'none') return;
|
|
165
|
+
path.dataset.graphColor = original;
|
|
166
|
+
path.setAttribute('stroke', ensureContrast(original, background, 3));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.group.querySelectorAll('circle[fill]').forEach((circle) => {
|
|
170
|
+
const original = circle.dataset.graphColor || circle.getAttribute('fill');
|
|
171
|
+
if (!original || original === 'none') return;
|
|
172
|
+
circle.dataset.graphColor = original;
|
|
173
|
+
circle.setAttribute('fill', ensureContrast(original, background, 3));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.group.querySelectorAll('text').forEach((text) => {
|
|
177
|
+
const original = text.dataset.graphColor || text.getAttribute('fill');
|
|
178
|
+
if (original) text.dataset.graphColor = original;
|
|
179
|
+
const isAxisLabel = !original || ['#8b949e', '#62627a'].includes(original.toLowerCase());
|
|
180
|
+
text.setAttribute('fill', isAxisLabel
|
|
181
|
+
? muted
|
|
182
|
+
: ensureContrast(original, background, 4.5));
|
|
183
|
+
});
|
|
184
|
+
},
|
|
143
185
|
};
|
|
144
186
|
|
|
145
187
|
window.shapes.push(graphShape);
|
|
@@ -148,6 +190,9 @@ function renderGraphOnCanvas(equations, settings) {
|
|
|
148
190
|
// Auto-select so the user sees something landed.
|
|
149
191
|
window.currentShape = graphShape;
|
|
150
192
|
graphShape.selectShape();
|
|
193
|
+
window.__adaptCanvasContrast?.(
|
|
194
|
+
window.getComputedStyle(window.svg).backgroundColor || '#13171C'
|
|
195
|
+
);
|
|
151
196
|
} catch (err) {
|
|
152
197
|
console.error('[GraphEngine] SVG insertion failed:', err);
|
|
153
198
|
return false;
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
const NS = 'http://www.w3.org/2000/svg';
|
|
11
11
|
const PALETTE = [
|
|
12
|
-
{ fill: '#
|
|
13
|
-
{ fill: '#
|
|
14
|
-
{ fill: '#
|
|
15
|
-
{ fill: '#
|
|
16
|
-
{ fill: '#
|
|
17
|
-
{ fill: '#
|
|
12
|
+
{ fill: '#8b7abb', stroke: '#665793' },
|
|
13
|
+
{ fill: '#a992cf', stroke: '#7762a6' },
|
|
14
|
+
{ fill: '#c2addc', stroke: '#8974aa' },
|
|
15
|
+
{ fill: '#7765a6', stroke: '#584883' },
|
|
16
|
+
{ fill: '#9d83c1', stroke: '#725b9a' },
|
|
17
|
+
{ fill: '#d1c3e7', stroke: '#927daf' },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
function escapeXml(value) {
|
|
@@ -25,6 +25,17 @@ function escapeXml(value) {
|
|
|
25
25
|
.replace(/"/g, '"');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function contrastText(fill) {
|
|
29
|
+
const raw = String(fill || '').replace('#', '');
|
|
30
|
+
const hex = raw.length === 3 ? raw.split('').map(char => char + char).join('') : raw;
|
|
31
|
+
if (!/^[0-9a-f]{6}$/i.test(hex)) return theme().text;
|
|
32
|
+
const red = parseInt(hex.slice(0, 2), 16);
|
|
33
|
+
const green = parseInt(hex.slice(2, 4), 16);
|
|
34
|
+
const blue = parseInt(hex.slice(4, 6), 16);
|
|
35
|
+
const luminance = .299 * red + .587 * green + .114 * blue;
|
|
36
|
+
return luminance > 145 ? '#34304a' : '#ffffff';
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
function isDark() {
|
|
29
40
|
return typeof document !== 'undefined' && document.body?.classList.contains('theme-dark');
|
|
30
41
|
}
|
|
@@ -149,7 +160,7 @@ export function renderERPreviewSVG(diagram) {
|
|
|
149
160
|
for (const entity of entities) {
|
|
150
161
|
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
162
|
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="
|
|
163
|
+
content += `<text x="${entity.x + entity.width / 2}" y="${entity.y + 27}" text-anchor="middle" fill="${contrastText(entity.color.fill)}" font-size="14" font-weight="600" font-family="lixFont">${escapeXml(entity.name)}</text>`;
|
|
153
164
|
const attributes = entity.attributes.length ? entity.attributes : [{ type: '', name: 'No attributes', key: '' }];
|
|
154
165
|
attributes.forEach((attribute, row) => {
|
|
155
166
|
const y = entity.y + entity.headerHeight + row * entity.rowHeight;
|
|
@@ -205,6 +216,34 @@ export function renderChartPreviewSVG(chart) {
|
|
|
205
216
|
const TK = theme();
|
|
206
217
|
const width = 720;
|
|
207
218
|
const height = 450;
|
|
219
|
+
|
|
220
|
+
if (chart.kind === 'pie') {
|
|
221
|
+
const values = chart.series[0].values.map(value => Math.max(0, value));
|
|
222
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
223
|
+
if (total <= 0) return '';
|
|
224
|
+
const cx = 255;
|
|
225
|
+
const cy = 235;
|
|
226
|
+
const radius = 145;
|
|
227
|
+
let startAngle = -Math.PI / 2;
|
|
228
|
+
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>`;
|
|
229
|
+
|
|
230
|
+
values.forEach((value, index) => {
|
|
231
|
+
const endAngle = startAngle + value / total * Math.PI * 2;
|
|
232
|
+
const x1 = cx + Math.cos(startAngle) * radius;
|
|
233
|
+
const y1 = cy + Math.sin(startAngle) * radius;
|
|
234
|
+
const x2 = cx + Math.cos(endAngle) * radius;
|
|
235
|
+
const y2 = cy + Math.sin(endAngle) * radius;
|
|
236
|
+
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
|
|
237
|
+
const color = PALETTE[index % PALETTE.length];
|
|
238
|
+
content += `<path d="M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z" fill="${color.fill}" stroke="${TK.bg}" stroke-width="3"/>`;
|
|
239
|
+
const percent = Math.round(value / total * 100);
|
|
240
|
+
const legendY = 125 + index * 42;
|
|
241
|
+
content += `<rect x="460" y="${legendY - 14}" width="18" height="18" rx="4" fill="${color.fill}" stroke="${color.stroke}"/><text x="488" y="${legendY}" fill="${TK.text}" font-size="12" font-family="lixFont">${escapeXml(chart.categories[index] || index + 1)} — ${value} (${percent}%)</text>`;
|
|
242
|
+
startAngle = endAngle;
|
|
243
|
+
});
|
|
244
|
+
return `<svg xmlns="${NS}" width="600" height="450" viewBox="0 0 ${width} ${height}">${content}</svg>`;
|
|
245
|
+
}
|
|
246
|
+
|
|
208
247
|
const left = 70;
|
|
209
248
|
const top = 60;
|
|
210
249
|
const plotWidth = 580;
|
|
@@ -250,6 +289,30 @@ function push(shape, frame) {
|
|
|
250
289
|
return shape;
|
|
251
290
|
}
|
|
252
291
|
|
|
292
|
+
function pushText(text, x, y, fontSize, fill, frame) {
|
|
293
|
+
if (!window.TextShape || !window.svg) return null;
|
|
294
|
+
const group = document.createElementNS(NS, 'g');
|
|
295
|
+
group.setAttribute('data-type', 'text-group');
|
|
296
|
+
group.setAttribute('transform', `translate(${x}, ${y})`);
|
|
297
|
+
group.setAttribute('data-x', x);
|
|
298
|
+
group.setAttribute('data-y', y);
|
|
299
|
+
|
|
300
|
+
const element = document.createElementNS(NS, 'text');
|
|
301
|
+
element.setAttribute('x', 0);
|
|
302
|
+
element.setAttribute('y', 0);
|
|
303
|
+
element.setAttribute('dominant-baseline', 'central');
|
|
304
|
+
element.setAttribute('fill', fill);
|
|
305
|
+
element.setAttribute('font-size', fontSize);
|
|
306
|
+
element.setAttribute('font-family', 'lixFont, sans-serif');
|
|
307
|
+
element.setAttribute('data-initial-font', 'lixFont');
|
|
308
|
+
element.setAttribute('data-initial-color', fill);
|
|
309
|
+
element.setAttribute('data-initial-size', `${fontSize}px`);
|
|
310
|
+
element.textContent = text;
|
|
311
|
+
group.appendChild(element);
|
|
312
|
+
window.svg.appendChild(group);
|
|
313
|
+
return push(new window.TextShape(group), frame);
|
|
314
|
+
}
|
|
315
|
+
|
|
253
316
|
function canvasOrigin(width, height) {
|
|
254
317
|
const vb = window.currentViewBox || { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight };
|
|
255
318
|
return { x: vb.x + vb.width / 2 - width / 2, y: vb.y + vb.height / 2 - height / 2 };
|
|
@@ -265,6 +328,17 @@ function createFrame(x, y, width, height, name, type) {
|
|
|
265
328
|
return frame;
|
|
266
329
|
}
|
|
267
330
|
|
|
331
|
+
function selectOnlyFrame(frame) {
|
|
332
|
+
if (!frame) return;
|
|
333
|
+
if (window.multiSelection?.clearSelection) window.multiSelection.clearSelection();
|
|
334
|
+
if (window.currentShape && window.currentShape !== frame && typeof window.currentShape.removeSelection === 'function') {
|
|
335
|
+
window.currentShape.removeSelection();
|
|
336
|
+
}
|
|
337
|
+
if (window.__deselectTextElement) window.__deselectTextElement();
|
|
338
|
+
window.currentShape = frame;
|
|
339
|
+
if (typeof frame.selectFrame === 'function') frame.selectFrame();
|
|
340
|
+
}
|
|
341
|
+
|
|
268
342
|
export function renderEROnCanvas(diagram) {
|
|
269
343
|
if (!diagram?.entities?.length || !window.Rectangle || !window.Arrow || !window.Frame) return false;
|
|
270
344
|
const TK = theme();
|
|
@@ -281,7 +355,7 @@ export function renderEROnCanvas(diagram) {
|
|
|
281
355
|
const y = origin.y + entity.y;
|
|
282
356
|
const header = push(new window.Rectangle(x, y, entity.width, entity.headerHeight, {
|
|
283
357
|
stroke: entity.color.stroke, strokeWidth: 1.5, fill: entity.color.fill, fillStyle: 'solid', roughness: 1,
|
|
284
|
-
label: entity.name, labelColor:
|
|
358
|
+
label: entity.name, labelColor: contrastText(entity.color.fill), labelFontSize: 15,
|
|
285
359
|
}), frame);
|
|
286
360
|
if (!first) first = header;
|
|
287
361
|
entityShapes.set(entity.name, { shape: header, x, y, width: entity.width, height: entity.headerHeight });
|
|
@@ -313,7 +387,8 @@ export function renderEROnCanvas(diagram) {
|
|
|
313
387
|
}
|
|
314
388
|
|
|
315
389
|
export function renderChartOnCanvas(chart) {
|
|
316
|
-
if (!chart?.series?.length || !window.Rectangle || !window.Circle || !window.Line || !window.Frame) return false;
|
|
390
|
+
if (!chart?.series?.length || !window.Rectangle || !window.Circle || !window.Line || !window.FreehandStroke || !window.Frame) return false;
|
|
391
|
+
if (chart.kind === 'pie') return renderPieOnCanvas(chart);
|
|
317
392
|
const TK = theme();
|
|
318
393
|
const width = 720;
|
|
319
394
|
const height = 450;
|
|
@@ -326,38 +401,103 @@ export function renderChartOnCanvas(chart) {
|
|
|
326
401
|
const values = chart.series.flatMap(series => series.values);
|
|
327
402
|
const maximum = Math.max(1, ...values.map(Math.abs));
|
|
328
403
|
const slot = plotWidth / Math.max(1, chart.categories.length);
|
|
329
|
-
let first = null;
|
|
330
|
-
|
|
331
404
|
push(new window.Line({ x: left, y: top }, { x: left, y: top + plotHeight }, { stroke: TK.line, strokeWidth: 1.5, roughness: 0 }), frame);
|
|
332
405
|
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
406
|
|
|
334
407
|
chart.series.forEach((series, seriesIndex) => {
|
|
335
408
|
const color = PALETTE[seriesIndex % PALETTE.length];
|
|
336
409
|
if (series.kind === 'line') {
|
|
337
|
-
|
|
338
|
-
series.values.forEach((value, index) => {
|
|
410
|
+
const linePoints = series.values.map((value, index) => {
|
|
339
411
|
const point = { x: left + slot * (index + .5), y: top + plotHeight - Math.abs(value) / maximum * plotHeight };
|
|
340
|
-
|
|
412
|
+
push(new window.Circle(point.x, point.y, 8, 8, {
|
|
341
413
|
stroke: color.stroke, strokeWidth: 2, fill: color.fill, fillStyle: 'solid', roughness: .5,
|
|
342
|
-
label: String(value), labelColor:
|
|
414
|
+
label: String(value), labelColor: contrastText(color.fill), labelFontSize: 9,
|
|
343
415
|
}), frame);
|
|
344
|
-
|
|
345
|
-
if (previous) push(new window.Line(previous, point, { stroke: color.stroke, strokeWidth: 3, roughness: .5 }), frame);
|
|
346
|
-
previous = point;
|
|
416
|
+
return [point.x, point.y, .5];
|
|
347
417
|
});
|
|
418
|
+
if (linePoints.length > 1) {
|
|
419
|
+
push(new window.FreehandStroke(linePoints, {
|
|
420
|
+
stroke: color.stroke,
|
|
421
|
+
strokeWidth: 3,
|
|
422
|
+
thinning: 0,
|
|
423
|
+
roughness: 0,
|
|
424
|
+
strokeStyle: 'solid',
|
|
425
|
+
}), frame);
|
|
426
|
+
}
|
|
348
427
|
} else {
|
|
349
428
|
const barWidth = Math.max(18, slot * .7 / chart.series.length);
|
|
350
429
|
series.values.forEach((value, index) => {
|
|
351
430
|
const barHeight = Math.max(8, Math.abs(value) / maximum * plotHeight);
|
|
352
431
|
const x = left + slot * index + slot * .15 + seriesIndex * barWidth;
|
|
353
|
-
|
|
432
|
+
push(new window.Rectangle(x, top + plotHeight - barHeight, barWidth, barHeight, {
|
|
354
433
|
stroke: color.stroke, strokeWidth: 1.5, fill: color.fill, fillStyle: 'solid', roughness: .7,
|
|
355
|
-
label: `${chart.categories[index] || index + 1}\n${value}`, labelColor:
|
|
434
|
+
label: `${chart.categories[index] || index + 1}\n${value}`, labelColor: contrastText(color.fill), labelFontSize: 11,
|
|
356
435
|
}), frame);
|
|
357
|
-
if (!first) first = bar;
|
|
358
436
|
});
|
|
359
437
|
}
|
|
360
438
|
});
|
|
361
|
-
|
|
439
|
+
selectOnlyFrame(frame);
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function renderPieOnCanvas(chart) {
|
|
444
|
+
if (!window.FreehandStroke || !window.TextShape) return false;
|
|
445
|
+
const width = 720;
|
|
446
|
+
const height = 450;
|
|
447
|
+
const origin = canvasOrigin(width, height);
|
|
448
|
+
const frame = createFrame(origin.x, origin.y, width, height, chart.title, 'mermaid-pie');
|
|
449
|
+
const values = chart.series[0].values.map(value => Math.max(0, value));
|
|
450
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
451
|
+
if (total <= 0) return false;
|
|
452
|
+
|
|
453
|
+
const cx = origin.x + 255;
|
|
454
|
+
const cy = origin.y + 235;
|
|
455
|
+
const radius = 145;
|
|
456
|
+
let startAngle = -Math.PI / 2;
|
|
457
|
+
|
|
458
|
+
values.forEach((value, index) => {
|
|
459
|
+
const sweep = value / total * Math.PI * 2;
|
|
460
|
+
const endAngle = startAngle + sweep;
|
|
461
|
+
const steps = Math.max(6, Math.ceil(sweep / (Math.PI / 60)));
|
|
462
|
+
const points = [[cx, cy, .5]];
|
|
463
|
+
for (let step = 0; step <= steps; step++) {
|
|
464
|
+
const angle = startAngle + sweep * (step / steps);
|
|
465
|
+
points.push([
|
|
466
|
+
cx + Math.cos(angle) * radius,
|
|
467
|
+
cy + Math.sin(angle) * radius,
|
|
468
|
+
.5,
|
|
469
|
+
]);
|
|
470
|
+
}
|
|
471
|
+
const color = PALETTE[index % PALETTE.length];
|
|
472
|
+
push(new window.FreehandStroke(points, {
|
|
473
|
+
stroke: color.fill,
|
|
474
|
+
strokeWidth: 3,
|
|
475
|
+
outlineStroke: theme().bg,
|
|
476
|
+
outlineWidth: 3,
|
|
477
|
+
closedFill: true,
|
|
478
|
+
roughness: 0,
|
|
479
|
+
strokeStyle: 'solid',
|
|
480
|
+
}), frame);
|
|
481
|
+
const percent = Math.round(value / total * 100);
|
|
482
|
+
const legendY = origin.y + 125 + index * 42;
|
|
483
|
+
push(new window.Rectangle(origin.x + 460, legendY - 14, 18, 18, {
|
|
484
|
+
stroke: color.stroke,
|
|
485
|
+
strokeWidth: 1,
|
|
486
|
+
fill: color.fill,
|
|
487
|
+
fillStyle: 'solid',
|
|
488
|
+
roughness: .5,
|
|
489
|
+
}), frame);
|
|
490
|
+
pushText(
|
|
491
|
+
`${chart.categories[index] || index + 1} — ${value} (${percent}%)`,
|
|
492
|
+
origin.x + 488,
|
|
493
|
+
legendY,
|
|
494
|
+
12,
|
|
495
|
+
theme().text,
|
|
496
|
+
frame,
|
|
497
|
+
);
|
|
498
|
+
startAngle = endAngle;
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
selectOnlyFrame(frame);
|
|
362
502
|
return true;
|
|
363
503
|
}
|
|
@@ -111,6 +111,10 @@ export default function LixSketchCanvas({
|
|
|
111
111
|
// real persistence layer.
|
|
112
112
|
useEffect(() => {
|
|
113
113
|
function handleKey(e) {
|
|
114
|
+
if (e.key === 'Escape') {
|
|
115
|
+
useUIStore.getState().closeAllModals?.();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
114
118
|
if (!(e.ctrlKey || e.metaKey)) return;
|
|
115
119
|
const key = (e.key || '').toLowerCase();
|
|
116
120
|
if (key !== 's' || e.shiftKey) return;
|
|
@@ -12,7 +12,7 @@ import { useTranslation } from '../hooks/useTranslation'
|
|
|
12
12
|
// matching list at render time based on the active theme.
|
|
13
13
|
const CANVAS_BACKGROUNDS_LIGHT = [
|
|
14
14
|
{ color: '#ffffff', label: 'menu.canvasBg.white' },
|
|
15
|
-
{ color: '#
|
|
15
|
+
{ color: '#fbf9fd', label: 'menu.canvasBg.cream' },
|
|
16
16
|
{ color: '#f5f3ed', label: 'menu.canvasBg.paper' },
|
|
17
17
|
{ color: '#f0f5fb', label: 'menu.canvasBg.skyTint' },
|
|
18
18
|
{ color: '#f0f5ef', label: 'menu.canvasBg.sageTint' },
|
|
@@ -20,7 +20,7 @@ const CANVAS_BACKGROUNDS_LIGHT = [
|
|
|
20
20
|
const CANVAS_BACKGROUNDS_DARK = [
|
|
21
21
|
{ color: '#000000', label: 'menu.canvasBg.black' },
|
|
22
22
|
{ color: '#161718', label: 'menu.canvasBg.darkGray' },
|
|
23
|
-
{ color: '#
|
|
23
|
+
{ color: '#15111f', label: 'menu.canvasBg.blueBlack' },
|
|
24
24
|
{ color: '#181605', label: 'menu.canvasBg.darkYellow' },
|
|
25
25
|
{ color: '#1B1615', label: 'menu.canvasBg.darkBrown' },
|
|
26
26
|
]
|
|
@@ -110,7 +110,7 @@ export default function Toolbar() {
|
|
|
110
110
|
onClick={() => setActiveTool(item.tool)}
|
|
111
111
|
className={`relative w-[33px] h-[31px] flex items-center justify-center rounded-lg transition-all duration-200 ${
|
|
112
112
|
isActive
|
|
113
|
-
? 'bg-
|
|
113
|
+
? 'bg-accent-blue/20 text-accent-blue'
|
|
114
114
|
: 'text-text-muted hover:text-text-primary hover:bg-surface-hover'
|
|
115
115
|
}`}
|
|
116
116
|
>
|
|
@@ -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}`}
|
|
@@ -227,13 +227,13 @@ export default function CommandPalette() {
|
|
|
227
227
|
|
|
228
228
|
return (
|
|
229
229
|
<div
|
|
230
|
-
className="fixed inset-0 z-[9999] flex items-
|
|
230
|
+
className="fixed inset-0 z-[9999] flex items-center justify-center font-[lixFont]"
|
|
231
231
|
onClick={toggleCommandPalette}
|
|
232
232
|
>
|
|
233
233
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
234
234
|
|
|
235
235
|
<div
|
|
236
|
-
className="relative bg-surface-card border border-border-light rounded-2xl w-
|
|
236
|
+
className="relative bg-surface-card border border-border-light rounded-2xl w-[92vw] max-w-[1200px] h-[88vh] max-h-[88vh] mx-4 overflow-hidden flex flex-col"
|
|
237
237
|
onClick={(e) => e.stopPropagation()}
|
|
238
238
|
>
|
|
239
239
|
{/* Search */}
|
|
@@ -251,7 +251,7 @@ export default function CommandPalette() {
|
|
|
251
251
|
</div>
|
|
252
252
|
|
|
253
253
|
{/* Results */}
|
|
254
|
-
<div ref={listRef} className="
|
|
254
|
+
<div ref={listRef} className="flex-1 min-h-0 overflow-y-auto no-scrollbar py-2">
|
|
255
255
|
{filtered.length === 0 && (
|
|
256
256
|
<div className="px-4 py-6 text-center text-text-dim text-xs">
|
|
257
257
|
No commands found
|
|
@@ -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>
|