@elixpo/lixsketch 5.5.12 → 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.
Files changed (37) hide show
  1. package/dist/react/{AIRenderer-PWWLWI6U.js → AIRenderer-ATIKGFB4.js} +90 -8
  2. package/dist/react/{AIRenderer-PWWLWI6U.js.map → AIRenderer-ATIKGFB4.js.map} +2 -2
  3. package/dist/react/{CopyPaste-SA72NDFN.js → CopyPaste-VUM5WRDX.js} +2 -2
  4. package/dist/react/FreehandStroke-JKTCTOKU.js +8 -0
  5. package/dist/react/{GraphEngine-IHRVGUGG.js → GraphEngine-KQXHNTZQ.js} +39 -1
  6. package/dist/react/{GraphEngine-IHRVGUGG.js.map → GraphEngine-KQXHNTZQ.js.map} +2 -2
  7. package/dist/react/{MermaidStructuredRenderer-GGOOGON5.js → MermaidStructuredRenderer-26WGA6YD.js} +152 -25
  8. package/dist/react/MermaidStructuredRenderer-26WGA6YD.js.map +7 -0
  9. package/dist/react/{SceneSerializer-ZPHV6IUP.js → SceneSerializer-3RWFYSLY.js} +2 -2
  10. package/dist/react/{SketchEngine-NPF2XN6Z.js → SketchEngine-WW5LIHNT.js} +6 -6
  11. package/dist/react/{chunk-YKJUBFHE.js → chunk-ABZEV54U.js} +23 -2
  12. package/dist/react/chunk-ABZEV54U.js.map +7 -0
  13. package/dist/react/index.js +26 -18
  14. package/dist/react/index.js.map +2 -2
  15. package/dist/react/styles.css +10 -3
  16. package/package.json +1 -1
  17. package/src/core/AIRenderer.js +104 -6
  18. package/src/core/GraphEngine.js +45 -0
  19. package/src/core/MermaidStructuredRenderer.js +162 -22
  20. package/src/react/LixSketchCanvas.jsx +4 -0
  21. package/src/react/components/AppMenu.jsx +2 -2
  22. package/src/react/components/Toolbar.jsx +1 -1
  23. package/src/react/components/modals/CommandPalette.jsx +3 -3
  24. package/src/react/components/sidebars/FrameSidebar.jsx +1 -1
  25. package/src/react/components/sidebars/IconSidebar.jsx +2 -2
  26. package/src/react/components/sidebars/ShapeSidebar.jsx +1 -1
  27. package/src/react/store/useSketchStore.js +7 -2
  28. package/src/react/store/useUIStore.js +3 -4
  29. package/src/react/styles.css +12 -3
  30. package/src/shapes/FreehandStroke.js +28 -3
  31. package/dist/react/FreehandStroke-CK24NUT3.js +0 -8
  32. package/dist/react/MermaidStructuredRenderer-GGOOGON5.js.map +0 -7
  33. package/dist/react/chunk-YKJUBFHE.js.map +0 -7
  34. /package/dist/react/{CopyPaste-SA72NDFN.js.map → CopyPaste-VUM5WRDX.js.map} +0 -0
  35. /package/dist/react/{FreehandStroke-CK24NUT3.js.map → FreehandStroke-JKTCTOKU.js.map} +0 -0
  36. /package/dist/react/{SceneSerializer-ZPHV6IUP.js.map → SceneSerializer-3RWFYSLY.js.map} +0 -0
  37. /package/dist/react/{SketchEngine-NPF2XN6Z.js.map → SketchEngine-WW5LIHNT.js.map} +0 -0
@@ -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, #0e1014);
6494
- color: var(--lixsketch-fg, #e6e8ec);
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, #0e1014);
6521
+ background: var(--lixsketch-bg, #15111f);
6515
6522
  z-index: 50;
6516
6523
  }
6517
6524
  .lixsketch-canvas-spinner {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixsketch",
3
- "version": "5.5.12",
3
+ "version": "5.5.13",
4
4
  "description": "Open-source SVG whiteboard engine + React canvas component with hand-drawn aesthetics — the core of LixSketch",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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: '#e8f3ec', stroke: '#5f836c' },
49
- { fill: '#f6eadf', stroke: '#a97852' },
50
- { fill: '#eee9f4', stroke: '#7e6b91' },
51
- { fill: '#f5efcf', stroke: '#9a8745' },
52
- { fill: '#e5f0f1', stroke: '#5d7f82' },
53
- { fill: '#f4e5e3', stroke: '#9a6863' },
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;
@@ -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: '#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' },
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, '&quot;');
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="#343832" font-size="14" font-weight="600" font-family="lixFont">${escapeXml(entity.name)}</text>`;
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: '#343832', labelFontSize: 15,
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
- let previous = null;
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
- const dot = push(new window.Circle(point.x, point.y, 8, 8, {
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: TK.text, labelFontSize: 9,
414
+ label: String(value), labelColor: contrastText(color.fill), labelFontSize: 9,
343
415
  }), 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;
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
- const bar = push(new window.Rectangle(x, top + plotHeight - barHeight, barWidth, barHeight, {
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: '#343832', labelFontSize: 11,
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
- if (first?.selectShape) { window.currentShape = first; first.selectShape(); }
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: '#faf9f5', label: 'menu.canvasBg.cream' },
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: '#13171C', label: 'menu.canvasBg.blueBlack' },
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-surface-active text-text-primary'
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
  >
@@ -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-start justify-center pt-[12vh] font-[lixFont]"
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-full max-w-[540px] mx-4 overflow-hidden"
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="max-h-[55vh] overflow-y-auto no-scrollbar py-2">
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
@@ -16,7 +16,7 @@ const FILL_STYLES = [
16
16
 
17
17
  const FILL_COLORS = [
18
18
  '#1e1e28',
19
- '#13171C',
19
+ '#15111f',
20
20
  '#1a1a2e',
21
21
  '#0d1117',
22
22
  '#2d2d3a',
@@ -65,14 +65,14 @@ function IconCell({ icon, onClick }) {
65
65
  >
66
66
  {normalizedSvg ? (
67
67
  <div
68
- style={{ width: '24px', height: '24px', overflow: 'visible', flexShrink: 0, pointerEvents: 'none', filter: 'brightness(0) invert(1)' }}
68
+ style={{ width: '24px', height: '24px', overflow: 'visible', flexShrink: 0, pointerEvents: 'none', filter: 'var(--lix-icon-filter, brightness(0) invert(1))' }}
69
69
  dangerouslySetInnerHTML={{ __html: normalizedSvg }}
70
70
  />
71
71
  ) : (
72
72
  <img
73
73
  src={`/icons/${encodeURIComponent(icon.filename)}`}
74
74
  alt=""
75
- style={{ width: '24px', height: '24px', pointerEvents: 'none', filter: 'invert(1)' }}
75
+ style={{ width: '24px', height: '24px', pointerEvents: 'none', filter: 'var(--lix-icon-filter, brightness(0) invert(1))' }}
76
76
  loading="lazy"
77
77
  />
78
78
  )}
@@ -28,7 +28,7 @@ export function ToolbarButton({ icon, preview, children, tooltip }) {
28
28
  title={tooltip}
29
29
  className={`h-9 flex items-center gap-1.5 px-3 rounded-lg transition-all duration-100 ${
30
30
  open
31
- ? 'bg-surface-active text-text-primary'
31
+ ? 'bg-accent-blue/20 text-accent-blue'
32
32
  : 'text-text-muted hover:text-text-primary hover:bg-surface-hover'
33
33
  }`}
34
34
  >
@@ -177,8 +177,13 @@ const useSketchStore = create((set, get) => ({
177
177
  setPanStart: (p) => set({ panStart: p }),
178
178
 
179
179
  // --- Canvas background ---
180
- canvasBackground: 'var(--lixsketch-bg, #ffffff)',
181
- setCanvasBackground: (color) => set({ canvasBackground: color }),
180
+ canvasBackground: 'var(--lixsketch-bg, #15111f)',
181
+ setCanvasBackground: (color) => {
182
+ set({ canvasBackground: color })
183
+ if (typeof window !== 'undefined') {
184
+ requestAnimationFrame(() => window.__adaptCanvasContrast?.(color))
185
+ }
186
+ },
182
187
 
183
188
  // --- Grid ---
184
189
  gridEnabled: false,
@@ -59,10 +59,10 @@ function applyTheme(theme) {
59
59
  ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
60
60
  : theme
61
61
  body.classList.remove('theme-dark', 'theme-light')
62
- if (resolved === 'dark') body.classList.add('theme-dark')
62
+ body.classList.add(`theme-${resolved}`)
63
63
 
64
64
  const svgEl = window.svg
65
- if (svgEl) svgEl.style.background = resolved === 'light' ? '#f4f3ee' : ''
65
+ if (svgEl) svgEl.style.background = resolved === 'light' ? '#fbf9fd' : '#15111f'
66
66
  }
67
67
 
68
68
  const useUIStore = create((set, get) => ({
@@ -157,8 +157,7 @@ const useUIStore = create((set, get) => ({
157
157
  setCanvasLoading: (loading, message) => set({ canvasLoading: loading, canvasLoadingMessage: message || 'Loading canvas...' }),
158
158
 
159
159
  // --- Theme ---
160
- // Issue #38 bug #1: light by default. Dark stays available via toggle.
161
- theme: 'light',
160
+ theme: 'dark',
162
161
  setTheme: (newTheme) => {
163
162
  const prev = get().theme
164
163
  const resolve = (t) => t === 'system'