@nice2dev/ui-graphics 1.2.1 → 1.3.0

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 (46) hide show
  1. package/CHANGELOG.md +464 -0
  2. package/dist/cjs/animation/AnimatedPerson.js +3 -1
  3. package/dist/cjs/core/NiceBrushEngine.js +5 -4
  4. package/dist/cjs/core/collaboration.js +11 -6
  5. package/dist/cjs/core/minimap.js +21 -20
  6. package/dist/cjs/font/NiceFontEditor.js +6 -6
  7. package/dist/cjs/game/GameAsset2dEditor.js +58 -39
  8. package/dist/cjs/photo/NiceAITools.js +3 -3
  9. package/dist/cjs/photo/PhotoEditorLeftPanel.js +16 -16
  10. package/dist/cjs/pixel/PixelEditor.js +1 -1
  11. package/dist/cjs/pixel/PixelEditorMenuBar.js +92 -27
  12. package/dist/cjs/pixel/PixelEditorRightPanel.js +9 -5
  13. package/dist/cjs/pixel/PixelEditorStatusBar.js +6 -2
  14. package/dist/cjs/pixel/PixelEditorTimeline.js +5 -1
  15. package/dist/cjs/pixel/PixelEditorToolbar.js +3 -1
  16. package/dist/cjs/pixel/SpriteGeneratorPanel.js +18 -11
  17. package/dist/cjs/texture/Nice3DTexturePainter.js +10 -6
  18. package/dist/cjs/ui/NiceUIDesigner.js +2 -2
  19. package/dist/cjs/vector/NiceSvgToCode.js +3 -3
  20. package/dist/cjs/vector/VectorEditor.js +1 -1
  21. package/dist/cjs/vector/VectorEditorMenuBar.js +7 -1
  22. package/dist/cjs/vector/VectorEditorRightPanel.js +3 -3
  23. package/dist/cjs/vector/useVectorEditor.js +26 -16
  24. package/dist/esm/animation/AnimatedPerson.js +3 -1
  25. package/dist/esm/core/NiceBrushEngine.js +5 -4
  26. package/dist/esm/core/collaboration.js +11 -6
  27. package/dist/esm/core/minimap.js +21 -20
  28. package/dist/esm/font/NiceFontEditor.js +7 -7
  29. package/dist/esm/game/GameAsset2dEditor.js +59 -40
  30. package/dist/esm/photo/NiceAITools.js +3 -3
  31. package/dist/esm/photo/PhotoEditorLeftPanel.js +17 -17
  32. package/dist/esm/pixel/PixelEditor.js +1 -1
  33. package/dist/esm/pixel/PixelEditorMenuBar.js +92 -27
  34. package/dist/esm/pixel/PixelEditorRightPanel.js +10 -6
  35. package/dist/esm/pixel/PixelEditorStatusBar.js +6 -2
  36. package/dist/esm/pixel/PixelEditorTimeline.js +6 -2
  37. package/dist/esm/pixel/PixelEditorToolbar.js +3 -1
  38. package/dist/esm/pixel/SpriteGeneratorPanel.js +18 -11
  39. package/dist/esm/texture/Nice3DTexturePainter.js +11 -7
  40. package/dist/esm/ui/NiceUIDesigner.js +3 -3
  41. package/dist/esm/vector/NiceSvgToCode.js +3 -3
  42. package/dist/esm/vector/VectorEditor.js +1 -1
  43. package/dist/esm/vector/VectorEditorMenuBar.js +7 -1
  44. package/dist/esm/vector/VectorEditorRightPanel.js +4 -4
  45. package/dist/esm/vector/useVectorEditor.js +26 -16
  46. package/package.json +4 -4
@@ -346,7 +346,7 @@ const Nice3DTexturePainter = ({ textureWidth = 1024, textureHeight = 1024, onSav
346
346
  borderTop: '1px solid var(--nice-border, #373a40)',
347
347
  width: '100%',
348
348
  margin: '4px 0',
349
- } }), jsxRuntime.jsxs("label", { children: ["Size: ", painter.brush.size, "px"] }), jsxRuntime.jsx("input", { type: "range", min: 1, max: 100, value: painter.brush.size, onChange: (e) => painter.setBrush({ ...painter.brush, size: Number(e.target.value) }), style: { width: '100%' } }), jsxRuntime.jsxs("label", { children: ["Opacity: ", Math.round(painter.brush.opacity * 100), "%"] }), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: painter.brush.opacity, onChange: (e) => painter.setBrush({ ...painter.brush, opacity: Number(e.target.value) }), style: { width: '100%' } }), jsxRuntime.jsxs("label", { children: ["Hardness: ", Math.round(painter.brush.hardness * 100), "%"] }), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: painter.brush.hardness, onChange: (e) => painter.setBrush({ ...painter.brush, hardness: Number(e.target.value) }), style: { width: '100%' } }), jsxRuntime.jsx("label", { children: t('texturePainter3d.blendMode', 'Blend Mode') }), jsxRuntime.jsxs("select", { value: painter.brush.blendMode, onChange: (e) => painter.setBrush({
349
+ } }), jsxRuntime.jsx("label", { children: t('texturePainter3d.sizePx', 'Size: {size}px').replace('{size}', String(painter.brush.size)) }), jsxRuntime.jsx("input", { type: "range", min: 1, max: 100, value: painter.brush.size, onChange: (e) => painter.setBrush({ ...painter.brush, size: Number(e.target.value) }), style: { width: '100%' } }), jsxRuntime.jsx("label", { children: t('texturePainter3d.opacity', 'Opacity: {opacity}%').replace('{opacity}', String(Math.round(painter.brush.opacity * 100))) }), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: painter.brush.opacity, onChange: (e) => painter.setBrush({ ...painter.brush, opacity: Number(e.target.value) }), style: { width: '100%' } }), jsxRuntime.jsx("label", { children: t('texturePainter3d.hardness', 'Hardness: {hardness}%').replace('{hardness}', String(Math.round(painter.brush.hardness * 100))) }), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: painter.brush.hardness, onChange: (e) => painter.setBrush({ ...painter.brush, hardness: Number(e.target.value) }), style: { width: '100%' } }), jsxRuntime.jsx("label", { children: t('texturePainter3d.blendMode', 'Blend Mode') }), jsxRuntime.jsxs("select", { value: painter.brush.blendMode, onChange: (e) => painter.setBrush({
350
350
  ...painter.brush,
351
351
  blendMode: e.target.value,
352
352
  }), style: {
@@ -362,7 +362,7 @@ const Nice3DTexturePainter = ({ textureWidth = 1024, textureHeight = 1024, onSav
362
362
  borderTop: '1px solid var(--nice-border, #373a40)',
363
363
  width: '100%',
364
364
  margin: '4px 0',
365
- } }), jsxRuntime.jsxs("label", { children: ["Zoom: ", Math.round(zoom * 100), "%"] }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 2 }, children: [jsxRuntime.jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, z - 0.1)), style: btnStyle(false), children: "\u2212" }), jsxRuntime.jsx("button", { onClick: () => setZoom(0.5), style: btnStyle(false), children: t('texturePainter3d.reset', 'Reset') }), jsxRuntime.jsx("button", { onClick: () => setZoom((z) => Math.min(4, z + 0.1)), style: btnStyle(false), children: "+" })] })] }), jsxRuntime.jsxs("div", { style: {
365
+ } }), jsxRuntime.jsx("label", { children: t('texturePainter3d.zoom', 'Zoom: {zoom}%').replace('{zoom}', String(Math.round(zoom * 100))) }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 2 }, children: [jsxRuntime.jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, z - 0.1)), style: btnStyle(false), children: "\u2212" }), jsxRuntime.jsx("button", { onClick: () => setZoom(0.5), style: btnStyle(false), children: t('texturePainter3d.reset', 'Reset') }), jsxRuntime.jsx("button", { onClick: () => setZoom((z) => Math.min(4, z + 0.1)), style: btnStyle(false), children: "+" })] })] }), jsxRuntime.jsxs("div", { style: {
366
366
  flex: 1,
367
367
  overflow: 'hidden',
368
368
  position: 'relative',
@@ -384,7 +384,7 @@ const Nice3DTexturePainter = ({ textureWidth = 1024, textureHeight = 1024, onSav
384
384
  color: 'var(--nice-text-secondary, #868e96)',
385
385
  display: 'flex',
386
386
  gap: 12,
387
- }, children: [jsxRuntime.jsxs("span", { children: [textureWidth, "\u00D7", textureHeight] }), cursorPos && (jsxRuntime.jsxs("span", { children: ["X: ", cursorPos.x, " Y: ", cursorPos.y] })), jsxRuntime.jsxs("span", { children: ["Zoom: ", Math.round(zoom * 100), "%"] })] })] }), jsxRuntime.jsxs("div", { style: { width: 160, fontSize: 11, display: 'flex', flexDirection: 'column', gap: 4 }, children: [jsxRuntime.jsx("strong", { style: { fontSize: 12 }, children: t('texturePainter3d.layers', 'Layers') }), jsxRuntime.jsx("div", { style: { flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 2 }, children: [...painter.layers].reverse().map((l) => (jsxRuntime.jsxs("div", { style: {
387
+ }, children: [jsxRuntime.jsxs("span", { children: [textureWidth, "\u00D7", textureHeight] }), cursorPos && (jsxRuntime.jsxs("span", { children: ["X: ", cursorPos.x, " Y: ", cursorPos.y] })), jsxRuntime.jsx("span", { children: t('texturePainter3d.zoom', 'Zoom: {zoom}%').replace('{zoom}', String(Math.round(zoom * 100))) })] })] }), jsxRuntime.jsxs("div", { style: { width: 160, fontSize: 11, display: 'flex', flexDirection: 'column', gap: 4 }, children: [jsxRuntime.jsx("strong", { style: { fontSize: 12 }, children: t('texturePainter3d.layers', 'Layers') }), jsxRuntime.jsx("div", { style: { flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 2 }, children: [...painter.layers].reverse().map((l) => (jsxRuntime.jsxs("div", { style: {
388
388
  padding: '4px 6px',
389
389
  borderRadius: 3,
390
390
  background: painter.activeLayerId === l.id
@@ -393,7 +393,7 @@ const Nice3DTexturePainter = ({ textureWidth = 1024, textureHeight = 1024, onSav
393
393
  color: painter.activeLayerId === l.id
394
394
  ? 'var(--nice-color-white, #fff)'
395
395
  : 'var(--nice-text, #c1c2c5)',
396
- }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }, onClick: () => painter.setActiveLayerId(l.id), children: [jsxRuntime.jsx("button", { onClick: (e) => {
396
+ }, children: [jsxRuntime.jsxs("div", { role: "button", tabIndex: 0, onKeyDown: uiCore.clickOnActivationKey, style: { display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }, onClick: () => painter.setActiveLayerId(l.id), children: [jsxRuntime.jsx("button", { onClick: (e) => {
397
397
  e.stopPropagation();
398
398
  painter.toggleLayerVisibility(l.id);
399
399
  }, style: {
@@ -405,7 +405,11 @@ const Nice3DTexturePainter = ({ textureWidth = 1024, textureHeight = 1024, onSav
405
405
  ? 'var(--nice-text, #c1c2c5)'
406
406
  : 'var(--nice-text-secondary, #5c5f66)',
407
407
  fontSize: 12,
408
- }, title: l.visible ? 'Hide layer' : 'Show layer', "aria-label": l.visible ? 'Hide layer' : 'Show layer', children: jsxRuntime.jsx(uiCore.NiceIcon, { name: l.visible ? 'eye' : 'eye-off', size: 14 }) }), jsxRuntime.jsx("span", { style: {
408
+ }, title: l.visible
409
+ ? t('texturePainter3d.hideLayer', 'Hide layer')
410
+ : t('texturePainter3d.showLayer', 'Show layer'), "aria-label": l.visible
411
+ ? t('texturePainter3d.hideLayer', 'Hide layer')
412
+ : t('texturePainter3d.showLayer', 'Show layer'), children: jsxRuntime.jsx(uiCore.NiceIcon, { name: l.visible ? 'eye' : 'eye-off', size: 14 }) }), jsxRuntime.jsx("span", { style: {
409
413
  flex: 1,
410
414
  overflow: 'hidden',
411
415
  textOverflow: 'ellipsis',
@@ -427,7 +431,7 @@ const Nice3DTexturePainter = ({ textureWidth = 1024, textureHeight = 1024, onSav
427
431
  cursor: 'pointer',
428
432
  background: 'var(--nice-bg-secondary, #2c2e33)',
429
433
  color: 'var(--nice-text, #c1c2c5)',
430
- }, children: "+ Add Layer" }), jsxRuntime.jsx("hr", { style: {
434
+ }, children: t('texturePainter3d.addLayer', '+ Add Layer') }), jsxRuntime.jsx("hr", { style: {
431
435
  border: 'none',
432
436
  borderTop: '1px solid var(--nice-border, #373a40)',
433
437
  width: '100%',
@@ -375,7 +375,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
375
375
  var _a, _b, _c, _d;
376
376
  const isSelected = designer.selectedId === node.id;
377
377
  const def = COMPONENT_DEFS.find((d) => d.type === node.type);
378
- return (jsxRuntime.jsxs("div", { onClick: (e) => {
378
+ return (jsxRuntime.jsxs("div", { role: "presentation", onClick: (e) => {
379
379
  e.stopPropagation();
380
380
  designer.setSelectedId(node.id);
381
381
  }, onDrop: (e) => ((def === null || def === void 0 ? void 0 : def.allowChildren) ? handleDrop(e, node.id) : undefined), onDragOver: (def === null || def === void 0 ? void 0 : def.allowChildren) ? handleDragOver : undefined, style: {
@@ -415,7 +415,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
415
415
  };
416
416
  const renderTreeItem = (node, depth = 0) => {
417
417
  const def = COMPONENT_DEFS.find((d) => d.type === node.type);
418
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsxs("div", { onClick: () => designer.setSelectedId(node.id), style: {
418
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsxs("div", { role: "button", tabIndex: 0, onKeyDown: uiCore.clickOnActivationKey, onClick: () => designer.setSelectedId(node.id), style: {
419
419
  padding: '2px 4px',
420
420
  paddingLeft: 4 + depth * 12,
421
421
  fontSize: 10,
@@ -439,14 +439,14 @@ const NiceSvgToCode = ({ initialSvg = '', onConvert, className = '', }) => {
439
439
  navigator.clipboard.writeText(result.code);
440
440
  }
441
441
  };
442
- return (jsxRuntime.jsxs("div", { className: `nice-svg-to-code ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(uiCore.NiceIcon, { name: "refresh", size: 14 }), " SVG \u2192 Code"] }), jsxRuntime.jsx("textarea", { value: svgInput, onChange: (e) => setSvgInput(e.target.value), placeholder: t('svgToCode.pasteSvgMarkupHere', 'Paste SVG markup here…'), rows: 6, style: {
442
+ return (jsxRuntime.jsxs("div", { className: `nice-svg-to-code ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(uiCore.NiceIcon, { name: "refresh", size: 14 }), " ", t('svgToCode.svgCode', 'SVG → Code')] }), jsxRuntime.jsx("textarea", { value: svgInput, onChange: (e) => setSvgInput(e.target.value), placeholder: t('svgToCode.pasteSvgMarkupHere', 'Paste SVG markup here…'), rows: 6, style: {
443
443
  width: '100%',
444
444
  fontFamily: 'monospace',
445
445
  fontSize: 11,
446
446
  padding: 6,
447
447
  borderRadius: 4,
448
448
  border: '1px solid var(--nice-border, #e2e8f0)',
449
- } }), jsxRuntime.jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxRuntime.jsxs("label", { children: ["Framework:", jsxRuntime.jsxs("select", { value: config.framework, onChange: (e) => updateConfig({ framework: e.target.value }), style: { width: '100%' }, children: [jsxRuntime.jsx("option", { value: "react", children: "React (JSX/TSX)" }), jsxRuntime.jsx("option", { value: "vue", children: "Vue (SFC)" }), jsxRuntime.jsx("option", { value: "web-component", children: t('svgToCode.webComponent', 'Web Component') }), jsxRuntime.jsx("option", { value: "typescript", children: "TypeScript (utility)" })] })] }), jsxRuntime.jsxs("label", { children: ["Component name:", jsxRuntime.jsx("input", { type: "text", value: config.componentName, onChange: (e) => updateConfig({ componentName: e.target.value.replace(/[^a-zA-Z0-9]/g, '') }), style: { width: '100%', padding: '3px 6px' } })] }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 8, flexWrap: 'wrap', fontSize: 11 }, children: [jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.typescript, onChange: (e) => updateConfig({ typescript: e.target.checked }) }), ' ', t('svgToCode.typescript', 'TypeScript')] }), jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.optimize, onChange: (e) => updateConfig({ optimize: e.target.checked }) }), ' ', t('svgToCode.optimize', 'Optimize')] }), jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.sizeProps, onChange: (e) => updateConfig({ sizeProps: e.target.checked }) }), ' ', t('svgToCode.sizeProps', 'Size props')] }), jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.useCurrentColor, onChange: (e) => updateConfig({ useCurrentColor: e.target.checked }) }), ' ', "currentColor"] }), jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.memo, onChange: (e) => updateConfig({ memo: e.target.checked }) }), ' ', "memo"] })] })] }), jsxRuntime.jsxs("button", { type: "button", onClick: handleConvert, disabled: !svgInput.trim(), style: {
449
+ } }), jsxRuntime.jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxRuntime.jsxs("label", { children: [t('svgToCode.framework', 'Framework:'), jsxRuntime.jsxs("select", { value: config.framework, onChange: (e) => updateConfig({ framework: e.target.value }), style: { width: '100%' }, children: [jsxRuntime.jsx("option", { value: "react", children: "React (JSX/TSX)" }), jsxRuntime.jsx("option", { value: "vue", children: "Vue (SFC)" }), jsxRuntime.jsx("option", { value: "web-component", children: t('svgToCode.webComponent', 'Web Component') }), jsxRuntime.jsx("option", { value: "typescript", children: t('svgToCode.typescriptUtility', 'TypeScript (utility)') })] })] }), jsxRuntime.jsxs("label", { children: [t('svgToCode.componentName', 'Component name:'), jsxRuntime.jsx("input", { type: "text", value: config.componentName, onChange: (e) => updateConfig({ componentName: e.target.value.replace(/[^a-zA-Z0-9]/g, '') }), style: { width: '100%', padding: '3px 6px' } })] }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 8, flexWrap: 'wrap', fontSize: 11 }, children: [jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.typescript, onChange: (e) => updateConfig({ typescript: e.target.checked }) }), ' ', t('svgToCode.typescript', 'TypeScript')] }), jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.optimize, onChange: (e) => updateConfig({ optimize: e.target.checked }) }), ' ', t('svgToCode.optimize', 'Optimize')] }), jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.sizeProps, onChange: (e) => updateConfig({ sizeProps: e.target.checked }) }), ' ', t('svgToCode.sizeProps', 'Size props')] }), jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.useCurrentColor, onChange: (e) => updateConfig({ useCurrentColor: e.target.checked }) }), ' ', "currentColor"] }), jsxRuntime.jsxs("label", { children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.memo, onChange: (e) => updateConfig({ memo: e.target.checked }) }), ' ', "memo"] })] })] }), jsxRuntime.jsxs("button", { type: "button", onClick: handleConvert, disabled: !svgInput.trim(), style: {
450
450
  background: 'var(--nice-color-accent, #6366f1)',
451
451
  color: 'var(--nice-text-on-primary, #fff)',
452
452
  border: 'none',
@@ -474,7 +474,7 @@ const NiceSvgToCode = ({ initialSvg = '', onConvert, className = '', }) => {
474
474
  overflow: 'auto',
475
475
  maxHeight: 300,
476
476
  whiteSpace: 'pre-wrap',
477
- }, children: result.code }), result.extractedColors.length > 0 && (jsxRuntime.jsxs("div", { style: { fontSize: 11, marginTop: 4 }, children: ["Kolory:", ' ', result.extractedColors.map((c) => (jsxRuntime.jsx("span", { style: {
477
+ }, children: result.code }), result.extractedColors.length > 0 && (jsxRuntime.jsxs("div", { style: { fontSize: 11, marginTop: 4 }, children: [t('svgToCode.colors', 'Colors:'), ' ', result.extractedColors.map((c) => (jsxRuntime.jsx("span", { style: {
478
478
  display: 'inline-block',
479
479
  width: 12,
480
480
  height: 12,
@@ -112,7 +112,7 @@ const NiceVectorEditor = ({ width: artW = 512, height: artH = 512, className, on
112
112
  }
113
113
  const node = (jsxRuntime.jsx(VectorEditorShapeRenderer.ShapeRenderer, { shape: s, selected: s.id === selId, zoom: zoom }, s.id));
114
114
  return previewTr ? (jsxRuntime.jsx("g", { transform: previewTr, children: node }, s.id)) : (node);
115
- })] }) }) }), jsxRuntime.jsxs(ui.NiceModal, { open: dlgSvg, onClose: () => setDlgSvg(false), title: t('vectorEditor.pasteSvg', 'Paste SVG'), children: [jsxRuntime.jsx("label", { children: "Paste SVG source code below:" }), jsxRuntime.jsx(ui.NiceTextArea, { value: svgText, onChange: (val) => setSvgText(val), placeholder: t('vectorEditor.svgXmlnsHttpWwwW3', '<svg xmlns="http://www.w3.org/2000/svg" ...>') }), jsxRuntime.jsxs("div", { className: VectorEditor_module.dialogActions, children: [jsxRuntime.jsx(ui.NiceButton, { variant: "ghost", onClick: () => setDlgSvg(false), children: t('vectorEditor.cancel', 'Cancel') }), jsxRuntime.jsx(ui.NiceButton, { variant: "primary", onClick: () => importSVGString(svgText), children: t('vectorEditor.import', 'Import') })] })] }), jsxRuntime.jsxs(ui.NiceModal, { open: snippetOpen, onClose: () => setSnippetOpen(false), title: t('vectorEditor.snippet.title', 'Icon snippet copied to clipboard'), children: [jsxRuntime.jsxs("p", { style: { marginTop: 0 }, children: [t('vectorEditor.snippet.pasteHint', 'Paste into'), ' ', jsxRuntime.jsxs("code", { children: ["packages/icons/src/icons/", iconCategory ? `${iconCategory.toLowerCase()}` : '<category>', ".tsx"] }), ":"] }), jsxRuntime.jsx(ui.NiceTextArea, { value: snippetText, onChange: (val) => setSnippetText(val), rows: 16 }), jsxRuntime.jsxs("div", { className: VectorEditor_module.dialogActions, children: [jsxRuntime.jsx(ui.NiceButton, { variant: "primary", onClick: () => {
115
+ })] }) }) }), jsxRuntime.jsxs(ui.NiceModal, { open: dlgSvg, onClose: () => setDlgSvg(false), title: t('vectorEditor.pasteSvg', 'Paste SVG'), children: [jsxRuntime.jsx("label", { children: t('vectorEditor.pasteSvgSourceCodeBelow', 'Paste SVG source code below:') }), jsxRuntime.jsx(ui.NiceTextArea, { value: svgText, onChange: (val) => setSvgText(val), placeholder: t('vectorEditor.svgXmlnsHttpWwwW3', '<svg xmlns="http://www.w3.org/2000/svg" ...>') }), jsxRuntime.jsxs("div", { className: VectorEditor_module.dialogActions, children: [jsxRuntime.jsx(ui.NiceButton, { variant: "ghost", onClick: () => setDlgSvg(false), children: t('vectorEditor.cancel', 'Cancel') }), jsxRuntime.jsx(ui.NiceButton, { variant: "primary", onClick: () => importSVGString(svgText), children: t('vectorEditor.import', 'Import') })] })] }), jsxRuntime.jsxs(ui.NiceModal, { open: snippetOpen, onClose: () => setSnippetOpen(false), title: t('vectorEditor.snippet.title', 'Icon snippet copied to clipboard'), children: [jsxRuntime.jsxs("p", { style: { marginTop: 0 }, children: [t('vectorEditor.snippet.pasteHint', 'Paste into'), ' ', jsxRuntime.jsxs("code", { children: ["packages/icons/src/icons/", iconCategory ? `${iconCategory.toLowerCase()}` : '<category>', ".tsx"] }), ":"] }), jsxRuntime.jsx(ui.NiceTextArea, { value: snippetText, onChange: (val) => setSnippetText(val), rows: 16 }), jsxRuntime.jsxs("div", { className: VectorEditor_module.dialogActions, children: [jsxRuntime.jsx(ui.NiceButton, { variant: "primary", onClick: () => {
116
116
  if (typeof navigator !== 'undefined' && navigator.clipboard) {
117
117
  navigator.clipboard.writeText(snippetText).catch(() => { });
118
118
  }
@@ -264,7 +264,13 @@ const VectorEditorMenuBar = ({ api, hasOnSaveToLibrary, onExportIconSnippet }) =
264
264
  { value: '', label: t('vectorEditor.menu.secondShape', '2nd Shape') },
265
265
  ...shapes
266
266
  .filter((s) => s.id !== selId)
267
- .map((s, i) => { var _a; return ({ value: s.id, label: (_a = s.name) !== null && _a !== void 0 ? _a : `Shape ${i + 1}` }); }),
267
+ .map((s, i) => {
268
+ var _a;
269
+ return ({
270
+ value: s.id,
271
+ label: (_a = s.name) !== null && _a !== void 0 ? _a : t('vectorEditor.shape', 'Shape {i}').replace('{i}', String(i + 1)),
272
+ });
273
+ }),
268
274
  ], style: { minWidth: 100 } })), jsxRuntime.jsxs("label", { className: VectorEditor_module.menuLabel, children: [jsxRuntime.jsx(ui.NiceCheckbox, { checked: snapGrid, onChange: () => setSnapGrid((v) => !v) }), t('vectorEditor.menu.snap', 'Snap')] }), onExportIconSnippet && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: VectorEditor_module.menuSep }), jsxRuntime.jsxs(ui.NiceButton, { className: VectorEditor_module.menuBtn, variant: "primary", size: "sm", onClick: onExportIconSnippet, children: [jsxRuntime.jsx(uiCore.NiceIcon, { name: "clipboard", size: 14 }), ' ', t('vectorEditor.menu.exportIcon', 'Export Icon')] })] }))] }));
269
275
  };
270
276
  var VectorEditorMenuBar$1 = React.memo(VectorEditorMenuBar);
@@ -37,7 +37,7 @@ const VectorEditorRightPanel = ({ api, library, artW = 512, artH = 512 }) => {
37
37
  } }) }), jsxRuntime.jsx(ui.NiceTextInput, { className: VectorEditor_module.propInput, value: (selShape === null || selShape === void 0 ? void 0 : selShape.fill) || fillCol, onChange: (val) => {
38
38
  setFillCol(val);
39
39
  updateSel({ fill: val });
40
- } })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: VectorEditor_module.propRow, style: { flexWrap: 'wrap', gap: 4 }, children: vectorGradients.PRESET_GRADIENTS.map((grad, i) => (jsxRuntime.jsx("div", { style: {
40
+ } })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: VectorEditor_module.propRow, style: { flexWrap: 'wrap', gap: 4 }, children: vectorGradients.PRESET_GRADIENTS.map((grad, i) => (jsxRuntime.jsx("div", { role: "button", tabIndex: 0, onKeyDown: uiCore.clickOnActivationKey, style: {
41
41
  width: 24,
42
42
  height: 24,
43
43
  borderRadius: 4,
@@ -46,7 +46,7 @@ const VectorEditorRightPanel = ({ api, library, artW = 512, artH = 512 }) => {
46
46
  border: (selectedGradient === null || selectedGradient === void 0 ? void 0 : selectedGradient.id) === grad.id
47
47
  ? '2px solid var(--nice-bg, #FFF)'
48
48
  : '1px solid var(--nice-border-color, #444)',
49
- }, onClick: () => applyGradient(grad), title: `${grad.type} gradient` }, i))) }), jsxRuntime.jsx("div", { className: VectorEditor_module.propRow, children: jsxRuntime.jsxs(ui.NiceButton, { className: VectorEditor_module.menuBtn, variant: "ghost", size: "sm", style: { width: '100%', fontSize: 10 }, onClick: () => setShowGradientEditor(!showGradientEditor), children: [jsxRuntime.jsx(uiCore.NiceIcon, { name: showGradientEditor ? 'chevron-down' : 'chevron-right', size: 14 }), ' ', showGradientEditor
49
+ }, onClick: () => applyGradient(grad), title: t('vectorEditor.gradient', '{type} gradient').replace('{type}', String(grad.type)) }, i))) }), jsxRuntime.jsx("div", { className: VectorEditor_module.propRow, children: jsxRuntime.jsxs(ui.NiceButton, { className: VectorEditor_module.menuBtn, variant: "ghost", size: "sm", style: { width: '100%', fontSize: 10 }, onClick: () => setShowGradientEditor(!showGradientEditor), children: [jsxRuntime.jsx(uiCore.NiceIcon, { name: showGradientEditor ? 'chevron-down' : 'chevron-right', size: 14 }), ' ', showGradientEditor
50
50
  ? t('vectorEditor.properties.hideEditor', 'Hide Editor')
51
51
  : t('vectorEditor.properties.editGradient', 'Edit Gradient')] }) }), showGradientEditor && selectedGradient && (jsxRuntime.jsxs("div", { style: {
52
52
  padding: 4,
@@ -233,7 +233,7 @@ const VectorEditorRightPanel = ({ api, library, artW = 512, artH = 512 }) => {
233
233
  var _a;
234
234
  const realIdx = shapes.length - 1 - revIdx;
235
235
  const active = s.id === selId;
236
- return (jsxRuntime.jsxs("div", { className: `${VectorEditor_module.layerRow} ${active ? VectorEditor_module.layerRowActive : ''} ${s.locked ? VectorEditor_module.layerRowLocked : ''}`, onClick: () => {
236
+ return (jsxRuntime.jsxs("div", { role: "button", tabIndex: 0, onKeyDown: uiCore.clickOnActivationKey, className: `${VectorEditor_module.layerRow} ${active ? VectorEditor_module.layerRowActive : ''} ${s.locked ? VectorEditor_module.layerRowLocked : ''}`, onClick: () => {
237
237
  setSelId(s.id);
238
238
  setTool('pointer');
239
239
  }, children: [jsxRuntime.jsx("button", { type: "button", className: VectorEditor_module.layerIconBtn, title: s.hidden
@@ -94,8 +94,12 @@ function useVectorEditor({ artW, artH, onSaveToLibrary }) {
94
94
  if (!r) {
95
95
  return { x: 0, y: 0 };
96
96
  }
97
- let x = (cx - r.left - r.width / 2 - px) / zoom + artW / 2;
98
- let y = (cy - r.top - r.height / 2 - py) / zoom + artH / 2;
97
+ // The <svg> fills the box and its viewBox fits the artboard into it (xMidYMid meet), so one
98
+ // art unit is `fit` screen px before the zoom. Dividing by the zoom alone put every shape
99
+ // closer to the centre than the pointer — off by half in the 900×520 showcase demo.
100
+ const fit = Math.min(r.width / artW, r.height / artH) || 1;
101
+ let x = (cx - r.left - r.width / 2 - px) / (zoom * fit) + artW / 2;
102
+ let y = (cy - r.top - r.height / 2 - py) / (zoom * fit) + artH / 2;
99
103
  if (snapGrid) {
100
104
  x = vectorEditorTypes.snapToGrid(x, gridSize);
101
105
  y = vectorEditorTypes.snapToGrid(y, gridSize);
@@ -333,32 +337,37 @@ function useVectorEditor({ artW, artH, onSaveToLibrary }) {
333
337
  }));
334
338
  return;
335
339
  }
336
- if (!pendingShape.current) {
340
+ const pending = pendingShape.current;
341
+ if (!pending) {
337
342
  return;
338
343
  }
339
- const t = pendingShape.current.type;
344
+ // The pending shape's ref follows the drag as well. onUp sizes the result from the ref,
345
+ // which kept the 0×0 shape of the press — so every dragged rectangle, ellipse, line and
346
+ // arrow counted as "tiny" and was deleted on release: nothing appeared on the canvas.
347
+ const resize = (patch) => {
348
+ pendingShape.current = { ...pending, ...patch };
349
+ setShapes((prev) => prev.map((s) => (s.id === pending.id ? { ...s, ...patch } : s)));
350
+ };
351
+ const t = pending.type;
340
352
  if (t === 'polygon' || t === 'star') {
341
353
  const cx = startPt.current.x;
342
354
  const cy = startPt.current.y;
343
355
  const dx = p.x - cx;
344
356
  const dy = p.y - cy;
345
357
  const r2 = Math.sqrt(dx * dx + dy * dy);
346
- setShapes((prev) => prev.map((s) => s.id === pendingShape.current.id
347
- ? { ...s, x: cx - r2, y: cy - r2, w: r2 * 2, h: r2 * 2 }
348
- : s));
358
+ resize({ x: cx - r2, y: cy - r2, w: r2 * 2, h: r2 * 2 });
349
359
  return;
350
360
  }
351
361
  if (t === 'rect' || t === 'roundrect' || t === 'ellipse') {
352
- const x1 = Math.min(startPt.current.x, p.x);
353
- const y1 = Math.min(startPt.current.y, p.y);
354
- const w2 = Math.abs(p.x - startPt.current.x);
355
- const h2 = Math.abs(p.y - startPt.current.y);
356
- setShapes((prev) => prev.map((s) => s.id === pendingShape.current.id ? { ...s, x: x1, y: y1, w: w2, h: h2 } : s));
362
+ resize({
363
+ x: Math.min(startPt.current.x, p.x),
364
+ y: Math.min(startPt.current.y, p.y),
365
+ w: Math.abs(p.x - startPt.current.x),
366
+ h: Math.abs(p.y - startPt.current.y),
367
+ });
357
368
  }
358
369
  else if (t === 'line' || t === 'arrow') {
359
- setShapes((prev) => prev.map((s) => s.id === pendingShape.current.id
360
- ? { ...s, w: Math.abs(p.x - s.x), h: Math.abs(p.y - s.y), x2: p.x, y2: p.y }
361
- : s));
370
+ resize({ w: Math.abs(p.x - pending.x), h: Math.abs(p.y - pending.y), x2: p.x, y2: p.y });
362
371
  }
363
372
  }, [tool, scr2art, selId]);
364
373
  const onUp = React.useCallback(() => {
@@ -408,7 +417,8 @@ function useVectorEditor({ artW, artH, onSaveToLibrary }) {
408
417
  dragStarted.current = false;
409
418
  return;
410
419
  }
411
- snap(shapesRef.current);
420
+ // the ref holds the final geometry even when the last move has not rendered yet
421
+ snap(shapesRef.current.map((s) => (s.id === ps.id ? ps : s)));
412
422
  setSelId(ps.id);
413
423
  pendingShape.current = null;
414
424
  dragStarted.current = false;
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { motion } from 'framer-motion';
3
3
  import { useMemo, useEffect } from 'react';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
  import { usePersonControls, playPose } from './animationHelper.js';
5
6
  import { DEFAULT_CHARACTER, pick } from './characterTypes.js';
6
7
 
@@ -9,6 +10,7 @@ const NiceAnimatedPerson = ({ id, name, size, score = 10, startPose = 'idle', on
9
10
  // colors prop is destructured for API completeness but not yet fully wired into character defaults.
10
11
  }) => {
11
12
  var _a, _b, _c, _d, _e;
13
+ const { t } = useNiceTranslation();
12
14
  const fx = usePersonControls();
13
15
  const effectiveStartPose = pose !== null && pose !== void 0 ? pose : startPose;
14
16
  const effectiveCharacter = character !== null && character !== void 0 ? character : config;
@@ -20,7 +22,7 @@ const NiceAnimatedPerson = ({ id, name, size, score = 10, startPose = 'idle', on
20
22
  }, [fx, onReady, effectiveStartPose]);
21
23
  const W = (_c = (_b = (_a = cfg.size) !== null && _a !== void 0 ? _a : effectiveSize) !== null && _b !== void 0 ? _b : DEFAULT_CHARACTER.size) !== null && _c !== void 0 ? _c : 180;
22
24
  const skin = pick(cfg.face.colors, 0, 'var(--anim-skin, #FFD2B3)');
23
- return (jsx(motion.div, { id: id, style: { width: W, height: W * 1.35, overflow: 'visible' }, animate: fx.root, children: jsxs("svg", { viewBox: "0 0 180 240", width: "100%", height: "100%", role: "img", "aria-label": (_e = (_d = cfg.name) !== null && _d !== void 0 ? _d : name) !== null && _e !== void 0 ? _e : 'Juror', children: [jsxs(motion.g, { animate: fx.body, children: [jsx("rect", { x: "68", y: "158", width: "14", height: "46", rx: "6", fill: pick(cfg.outfit.colors, 1, 'var(--anim-outfit-leg, #334155)') }), jsx("ellipse", { cx: "75", cy: "204", rx: "10", ry: "5", fill: pick(cfg.outfit.colors, 1, 'var(--anim-outfit-leg-shadow, #1E293B)') }), jsx("rect", { x: "98", y: "158", width: "14", height: "46", rx: "6", fill: pick(cfg.outfit.colors, 1, 'var(--anim-outfit-leg, #334155)') }), jsx("ellipse", { cx: "105", cy: "204", rx: "10", ry: "5", fill: pick(cfg.outfit.colors, 1, 'var(--anim-outfit-leg-shadow, #1E293B)') })] }), jsx(motion.g, { animate: fx.body, children: renderOutfit(cfg.outfit) }), jsx("rect", { x: "84", y: "82", width: "12", height: "12", rx: "4", fill: skin }), jsxs(motion.g, { animate: fx.leftArm, style: { transformOrigin: '64px 96px' }, children: [jsx("rect", { x: "36", y: "96", width: "16", height: "36", rx: "7", fill: skin }), jsx("circle", { cx: "44", cy: "136", r: "7", fill: skin })] }), jsxs(motion.g, { animate: fx.rightArm, style: { transformOrigin: '116px 96px' }, children: [jsx("rect", { x: "128", y: "96", width: "16", height: "36", rx: "7", fill: skin }), jsx("circle", { cx: "136", cy: "136", r: "7", fill: skin }), jsxs(motion.g, { animate: fx.card, children: [jsx("rect", { x: "126", y: "50", width: "42", height: "30", rx: "4", fill: "var(--nice-color-surface, #fff)", stroke: "var(--nice-text-primary, #111)", strokeWidth: 1.2 }), jsx("text", { x: "147", y: "70", textAnchor: "middle", fontSize: "16", fontWeight: 700, fill: "var(--nice-text-primary, #111)", children: String(score) })] }), renderProp(cfg.prop)] }), jsxs(motion.g, { animate: fx.head, children: [renderHairBack(cfg.hair), renderFace(cfg.face), jsx(motion.g, { animate: fx.eyes, children: renderEyes(cfg.eyes) }), renderNose(cfg.nose), jsx(motion.g, { animate: fx.mouth, children: renderMouth(cfg.mouth) }), renderHeadwear(cfg.headwear)] }), jsxs(motion.g, { animate: fx.curse, initial: { opacity: 0 }, children: [jsx("rect", { x: "62", y: "10", width: "56", height: "22", rx: "8", fill: "var(--nice-text-primary, #111)" }), jsx("text", { x: "90", y: "26", textAnchor: "middle", fontSize: "12", fill: "var(--nice-text-inverse, #fff)", children: "#!%&*" }), jsx("path", { d: "M82 32 l4 8 l4 -8", fill: "var(--nice-text-primary, #111)" })] })] }) }));
25
+ return (jsx(motion.div, { id: id, style: { width: W, height: W * 1.35, overflow: 'visible' }, animate: fx.root, children: jsxs("svg", { viewBox: "0 0 180 240", width: "100%", height: "100%", role: "img", "aria-label": (_e = (_d = cfg.name) !== null && _d !== void 0 ? _d : name) !== null && _e !== void 0 ? _e : t('animatedPerson.juror', 'Juror'), children: [jsxs(motion.g, { animate: fx.body, children: [jsx("rect", { x: "68", y: "158", width: "14", height: "46", rx: "6", fill: pick(cfg.outfit.colors, 1, 'var(--anim-outfit-leg, #334155)') }), jsx("ellipse", { cx: "75", cy: "204", rx: "10", ry: "5", fill: pick(cfg.outfit.colors, 1, 'var(--anim-outfit-leg-shadow, #1E293B)') }), jsx("rect", { x: "98", y: "158", width: "14", height: "46", rx: "6", fill: pick(cfg.outfit.colors, 1, 'var(--anim-outfit-leg, #334155)') }), jsx("ellipse", { cx: "105", cy: "204", rx: "10", ry: "5", fill: pick(cfg.outfit.colors, 1, 'var(--anim-outfit-leg-shadow, #1E293B)') })] }), jsx(motion.g, { animate: fx.body, children: renderOutfit(cfg.outfit) }), jsx("rect", { x: "84", y: "82", width: "12", height: "12", rx: "4", fill: skin }), jsxs(motion.g, { animate: fx.leftArm, style: { transformOrigin: '64px 96px' }, children: [jsx("rect", { x: "36", y: "96", width: "16", height: "36", rx: "7", fill: skin }), jsx("circle", { cx: "44", cy: "136", r: "7", fill: skin })] }), jsxs(motion.g, { animate: fx.rightArm, style: { transformOrigin: '116px 96px' }, children: [jsx("rect", { x: "128", y: "96", width: "16", height: "36", rx: "7", fill: skin }), jsx("circle", { cx: "136", cy: "136", r: "7", fill: skin }), jsxs(motion.g, { animate: fx.card, children: [jsx("rect", { x: "126", y: "50", width: "42", height: "30", rx: "4", fill: "var(--nice-color-surface, #fff)", stroke: "var(--nice-text-primary, #111)", strokeWidth: 1.2 }), jsx("text", { x: "147", y: "70", textAnchor: "middle", fontSize: "16", fontWeight: 700, fill: "var(--nice-text-primary, #111)", children: String(score) })] }), renderProp(cfg.prop)] }), jsxs(motion.g, { animate: fx.head, children: [renderHairBack(cfg.hair), renderFace(cfg.face), jsx(motion.g, { animate: fx.eyes, children: renderEyes(cfg.eyes) }), renderNose(cfg.nose), jsx(motion.g, { animate: fx.mouth, children: renderMouth(cfg.mouth) }), renderHeadwear(cfg.headwear)] }), jsxs(motion.g, { animate: fx.curse, initial: { opacity: 0 }, children: [jsx("rect", { x: "62", y: "10", width: "56", height: "22", rx: "8", fill: "var(--nice-text-primary, #111)" }), jsx("text", { x: "90", y: "26", textAnchor: "middle", fontSize: "12", fill: "var(--nice-text-inverse, #fff)", children: "#!%&*" }), jsx("path", { d: "M82 32 l4 8 l4 -8", fill: "var(--nice-text-primary, #111)" })] })] }) }));
24
26
  };
25
27
  /* ---------------- RENDERERS (redesigned proportions) ---------------- */
26
28
  function renderOutfit(cfg) {
@@ -544,20 +544,21 @@ color = '#000000') {
544
544
  const NiceBrushEngine = ({ canvas, color = '#000000', onStrokeEnd, className = '', }) => {
545
545
  const { t } = useNiceTranslation();
546
546
  const engine = useBrushEngine(canvas, color);
547
+ const { endStroke } = engine;
547
548
  // Override endStroke to fire callback
548
549
  useEffect(() => {
549
550
  if (!canvas) {
550
551
  return;
551
552
  }
552
553
  const onUp = () => {
553
- const stroke = engine.endStroke();
554
+ const stroke = endStroke();
554
555
  if (stroke && onStrokeEnd) {
555
556
  onStrokeEnd(stroke);
556
557
  }
557
558
  };
558
559
  canvas.addEventListener('pointerup', onUp);
559
560
  return () => canvas.removeEventListener('pointerup', onUp);
560
- }, [canvas, engine.endStroke, onStrokeEnd]);
561
+ }, [canvas, endStroke, onStrokeEnd]);
561
562
  const s = engine.settings;
562
563
  return (jsxs("div", { className: `nice-brush-engine ${className}`, style: { display: 'grid', gap: 6, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " ", t('brushEngine.brushEngineV2', 'Brush Engine v2')] }), jsx("div", { style: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: engine.presetNames.map((name) => {
563
564
  var _a, _b;
@@ -572,13 +573,13 @@ const NiceBrushEngine = ({ canvas, color = '#000000', onStrokeEnd, className = '
572
573
  : 'transparent',
573
574
  color: s.name === ((_b = BRUSH_PRESETS[name]) === null || _b === void 0 ? void 0 : _b.name) ? 'var(--nice-color-white, #fff)' : 'inherit',
574
575
  }, children: BRUSH_PRESETS[name].name }, name));
575
- }) }), jsxs("label", { children: ["Size: ", s.size, "px", jsx("input", { type: "range", min: 1, max: 200, value: s.size, onChange: (e) => engine.setSettings({ ...s, size: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: ["Opacity: ", Math.round(s.opacity * 100), "%", jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.opacity, onChange: (e) => engine.setSettings({ ...s, opacity: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: ["Flow: ", Math.round(s.flow * 100), "%", jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.flow, onChange: (e) => engine.setSettings({ ...s, flow: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: ["Hardness: ", Math.round(s.hardness * 100), "%", jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.hardness, onChange: (e) => engine.setSettings({ ...s, hardness: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: ["Smoothing: ", Math.round(s.smoothing * 100), "%", jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.smoothing, onChange: (e) => engine.setSettings({ ...s, smoothing: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: ["Tip:", jsxs("select", { value: s.tipShape, onChange: (e) => engine.setSettings({ ...s, tipShape: e.target.value }), style: { width: '100%' }, children: [jsx("option", { value: "circle", children: t('brushEngine.circle', 'Circle') }), jsx("option", { value: "square", children: t('brushEngine.square', 'Square') }), jsx("option", { value: "diamond", children: t('brushEngine.diamond', 'Diamond') }), jsx("option", { value: "texture", children: t('brushEngine.texture', 'Texture') })] })] }), jsxs("label", { children: ["Roundness: ", Math.round(s.roundness * 100), "%", jsx("input", { type: "range", min: 0.05, max: 1, step: 0.05, value: s.roundness, onChange: (e) => engine.setSettings({ ...s, roundness: +e.target.value }), style: { width: '100%' } })] }), jsxs("details", { children: [jsxs("summary", { style: { cursor: 'pointer', fontSize: 12 }, children: [jsx(NiceIcon, { name: "bar-chart", size: 12 }), " ", t('brushEngine.sizeDynamics', 'Size Dynamics')] }), jsxs("label", { children: ["Source:", jsxs("select", { value: s.sizeDynamics.source, onChange: (e) => engine.setSettings({
576
+ }) }), jsxs("label", { children: [t('brushEngine.sizePx', 'Size: {size}px').replace('{size}', String(s.size)), jsx("input", { type: "range", min: 1, max: 200, value: s.size, onChange: (e) => engine.setSettings({ ...s, size: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: [t('brushEngine.opacity', 'Opacity: {opacity}%').replace('{opacity}', String(Math.round(s.opacity * 100))), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.opacity, onChange: (e) => engine.setSettings({ ...s, opacity: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: [t('brushEngine.flow', 'Flow: {flow}%').replace('{flow}', String(Math.round(s.flow * 100))), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.flow, onChange: (e) => engine.setSettings({ ...s, flow: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: [t('brushEngine.hardness', 'Hardness: {hardness}%').replace('{hardness}', String(Math.round(s.hardness * 100))), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.hardness, onChange: (e) => engine.setSettings({ ...s, hardness: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: [t('brushEngine.smoothing', 'Smoothing: {smoothing}%').replace('{smoothing}', String(Math.round(s.smoothing * 100))), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.smoothing, onChange: (e) => engine.setSettings({ ...s, smoothing: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: [t('brushEngine.tip', 'Tip:'), jsxs("select", { value: s.tipShape, onChange: (e) => engine.setSettings({ ...s, tipShape: e.target.value }), style: { width: '100%' }, children: [jsx("option", { value: "circle", children: t('brushEngine.circle', 'Circle') }), jsx("option", { value: "square", children: t('brushEngine.square', 'Square') }), jsx("option", { value: "diamond", children: t('brushEngine.diamond', 'Diamond') }), jsx("option", { value: "texture", children: t('brushEngine.texture', 'Texture') })] })] }), jsxs("label", { children: [t('brushEngine.roundness', 'Roundness: {roundness}%').replace('{roundness}', String(Math.round(s.roundness * 100))), jsx("input", { type: "range", min: 0.05, max: 1, step: 0.05, value: s.roundness, onChange: (e) => engine.setSettings({ ...s, roundness: +e.target.value }), style: { width: '100%' } })] }), jsxs("details", { children: [jsxs("summary", { style: { cursor: 'pointer', fontSize: 12 }, children: [jsx(NiceIcon, { name: "bar-chart", size: 12 }), " ", t('brushEngine.sizeDynamics', 'Size Dynamics')] }), jsxs("label", { children: [t('brushEngine.source', 'Source:'), jsxs("select", { value: s.sizeDynamics.source, onChange: (e) => engine.setSettings({
576
577
  ...s,
577
578
  sizeDynamics: {
578
579
  ...s.sizeDynamics,
579
580
  source: e.target.value,
580
581
  },
581
- }), style: { width: '100%' }, children: [jsx("option", { value: "none", children: "Off" }), jsx("option", { value: "pressure", children: t('brushEngine.pressure', 'Pressure') }), jsx("option", { value: "tilt", children: t('brushEngine.tilt', 'Tilt') }), jsx("option", { value: "speed", children: t('brushEngine.speed', 'Speed') }), jsx("option", { value: "random", children: t('brushEngine.random', 'Random') })] })] })] }), jsxs("label", { children: ["Scatter: ", Math.round(s.scatter * 100), "%", jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.scatter, onChange: (e) => engine.setSettings({ ...s, scatter: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: ["Blend:", jsx("select", { value: s.blendMode, onChange: (e) => engine.setSettings({ ...s, blendMode: e.target.value }), style: { width: '100%' }, children: ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten'].map((m) => (jsx("option", { value: m, children: m }, m))) })] }), jsxs("label", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [jsx("input", { type: "checkbox", checked: s.wetEdges, onChange: (e) => engine.setSettings({ ...s, wetEdges: e.target.checked }) }), t('brushEngine.wetEdges', 'Wet Edges')] })] }));
582
+ }), style: { width: '100%' }, children: [jsx("option", { value: "none", children: t('brushEngine.off', 'Off') }), jsx("option", { value: "pressure", children: t('brushEngine.pressure', 'Pressure') }), jsx("option", { value: "tilt", children: t('brushEngine.tilt', 'Tilt') }), jsx("option", { value: "speed", children: t('brushEngine.speed', 'Speed') }), jsx("option", { value: "random", children: t('brushEngine.random', 'Random') })] })] })] }), jsxs("label", { children: [t('brushEngine.scatter', 'Scatter: {scatter}%').replace('{scatter}', String(Math.round(s.scatter * 100))), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: s.scatter, onChange: (e) => engine.setSettings({ ...s, scatter: +e.target.value }), style: { width: '100%' } })] }), jsxs("label", { children: [t('brushEngine.blend', 'Blend:'), jsx("select", { value: s.blendMode, onChange: (e) => engine.setSettings({ ...s, blendMode: e.target.value }), style: { width: '100%' }, children: ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten'].map((m) => (jsx("option", { value: m, children: m }, m))) })] }), jsxs("label", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [jsx("input", { type: "checkbox", checked: s.wetEdges, onChange: (e) => engine.setSettings({ ...s, wetEdges: e.target.checked }) }), t('brushEngine.wetEdges', 'Wet Edges')] })] }));
582
583
  };
583
584
 
584
585
  export { BRUSH_PRESETS, NiceBrushEngine, useBrushEngine };
@@ -169,10 +169,14 @@ function useCollaboration(transport, roomId, currentUser) {
169
169
  const [users, setUsers] = useState([]);
170
170
  const [connected, setConnected] = useState(false);
171
171
  const operationHandlers = useRef([]);
172
+ // the user as it is when the room is joined: a new object with the same id must not rejoin
173
+ const currentUserRef = useRef(currentUser);
174
+ currentUserRef.current = currentUser;
172
175
  useEffect(() => {
173
- transport.connect(roomId, currentUser.id);
176
+ const user = currentUserRef.current;
177
+ transport.connect(roomId, user.id);
174
178
  setConnected(true);
175
- transport.send({ type: 'join', user: currentUser });
179
+ transport.send({ type: 'join', user });
176
180
  const unsubscribe = transport.onMessage((msg) => {
177
181
  switch (msg.type) {
178
182
  case 'join':
@@ -202,17 +206,18 @@ function useCollaboration(transport, roomId, currentUser) {
202
206
  }
203
207
  });
204
208
  return () => {
205
- transport.send({ type: 'leave', userId: currentUser.id });
209
+ transport.send({ type: 'leave', userId: user.id });
206
210
  unsubscribe();
207
211
  transport.disconnect();
208
212
  setConnected(false);
209
213
  };
210
- }, [roomId, currentUser.id]);
211
- const sendCursor = useCallback((x, y) => transport.send({ type: 'cursor', userId: currentUser.id, x, y }), [currentUser.id]);
214
+ // a new transport reconnects (until 1.2.2 it was ignored and messages went to the old one)
215
+ }, [transport, roomId, currentUser.id]);
216
+ const sendCursor = useCallback((x, y) => transport.send({ type: 'cursor', userId: currentUser.id, x, y }), [transport, currentUser.id]);
212
217
  const sendOperation = useCallback((op) => transport.send({
213
218
  type: 'operation',
214
219
  operation: { ...op, timestamp: Date.now(), userId: currentUser.id },
215
- }), [currentUser.id]);
220
+ }), [transport, currentUser.id]);
216
221
  const onOperation = useCallback((handler) => {
217
222
  operationHandlers.current.push(handler);
218
223
  return () => {
@@ -18,6 +18,27 @@ const Minimap = ({ canvasRef, imageDataUrl, documentWidth, documentHeight, viewp
18
18
  const scale = Math.min(scaleX, scaleY);
19
19
  // Draw minimap
20
20
  React.useEffect(() => {
21
+ // inside the effect: it draws with this run's viewport, scale and size
22
+ const drawViewport = (ctx) => {
23
+ // Draw viewport rect
24
+ const vx = viewport.x * scale;
25
+ const vy = viewport.y * scale;
26
+ const vw = viewport.width * scale;
27
+ const vh = viewport.height * scale;
28
+ ctx.strokeStyle = resolveCssColor('var(--nice-primary-tint-80, rgba(59, 130, 246, 0.8))', ctx.canvas);
29
+ ctx.lineWidth = 1.5;
30
+ ctx.strokeRect(vx, vy, vw, vh);
31
+ // Dim areas outside viewport
32
+ ctx.fillStyle = resolveCssColor('var(--nice-overlay-30, rgba(0, 0, 0, 0.3))', ctx.canvas);
33
+ // Top
34
+ ctx.fillRect(0, 0, mapW, vy);
35
+ // Bottom
36
+ ctx.fillRect(0, vy + vh, mapW, mapH - vy - vh);
37
+ // Left
38
+ ctx.fillRect(0, vy, vx, vh);
39
+ // Right
40
+ ctx.fillRect(vx + vw, vy, mapW - vx - vw, vh);
41
+ };
21
42
  if (!visible) {
22
43
  return;
23
44
  }
@@ -62,26 +83,6 @@ const Minimap = ({ canvasRef, imageDataUrl, documentWidth, documentHeight, viewp
62
83
  mapW,
63
84
  mapH,
64
85
  ]);
65
- function drawViewport(ctx) {
66
- // Draw viewport rect
67
- const vx = viewport.x * scale;
68
- const vy = viewport.y * scale;
69
- const vw = viewport.width * scale;
70
- const vh = viewport.height * scale;
71
- ctx.strokeStyle = resolveCssColor('var(--nice-primary-tint-80, rgba(59, 130, 246, 0.8))', ctx.canvas);
72
- ctx.lineWidth = 1.5;
73
- ctx.strokeRect(vx, vy, vw, vh);
74
- // Dim areas outside viewport
75
- ctx.fillStyle = resolveCssColor('var(--nice-overlay-30, rgba(0, 0, 0, 0.3))', ctx.canvas);
76
- // Top
77
- ctx.fillRect(0, 0, mapW, vy);
78
- // Bottom
79
- ctx.fillRect(0, vy + vh, mapW, mapH - vy - vh);
80
- // Left
81
- ctx.fillRect(0, vy, vx, vh);
82
- // Right
83
- ctx.fillRect(vx + vw, vy, mapW - vx - vw, vh);
84
- }
85
86
  const handlePointerDown = (e) => {
86
87
  isDragging.current = true;
87
88
  updateViewportFromPointer(e);
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceTutorialButton, resolveTutorial } from '@nice2dev/ui';
3
- import { NiceIcon } from '@nice2dev/ui-core';
3
+ import { clickOnActivationKey, NiceIcon } from '@nice2dev/ui-core';
4
4
  import React from 'react';
5
5
  import { useNiceTranslation } from '../core/i18n.js';
6
6
 
@@ -237,17 +237,17 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
237
237
  borderRadius: 3,
238
238
  cursor: 'pointer',
239
239
  fontSize: 11,
240
- }, children: t('fontEditor.exportSvgFont', 'Export SVG Font') })] }), jsxs("div", { style: { display: 'flex', gap: 4, borderBottom: '1px solid var(--nice-border, #373a40)' }, children: [jsx("button", { onClick: () => setTab('glyph'), style: tabBtnStyle(tab === 'glyph'), children: t('fontEditor.glyphs', 'Glyphs') }), jsx("button", { onClick: () => setTab('metrics'), style: tabBtnStyle(tab === 'metrics'), children: t('fontEditor.metrics', 'Metrics') }), jsx("button", { onClick: () => setTab('kerning'), style: tabBtnStyle(tab === 'kerning'), children: t('fontEditor.kerning', 'Kerning') })] }), tab === 'glyph' && (jsxs("div", { style: { display: 'flex', gap: 8 }, children: [jsxs("div", { style: { flex: 1 }, children: [jsxs("div", { style: {
240
+ }, children: t('fontEditor.exportSvgFont', 'Export SVG Font') })] }), jsxs("div", { style: { display: 'flex', gap: 4, borderBottom: '1px solid var(--nice-border, #373a40)' }, children: [jsx("button", { onClick: () => setTab('glyph'), style: tabBtnStyle(tab === 'glyph'), children: t('fontEditor.glyphs', 'Glyphs') }), jsx("button", { onClick: () => setTab('metrics'), style: tabBtnStyle(tab === 'metrics'), children: t('fontEditor.metrics', 'Metrics') }), jsx("button", { onClick: () => setTab('kerning'), style: tabBtnStyle(tab === 'kerning'), children: t('fontEditor.kerning', 'Kerning') })] }), tab === 'glyph' && (jsxs("div", { style: { display: 'flex', gap: 8 }, children: [jsxs("div", { style: { flex: 1 }, children: [jsx("div", { style: {
241
241
  fontSize: 11,
242
242
  marginBottom: 4,
243
243
  color: 'var(--nice-text-secondary, #868e96)',
244
- }, children: [editor.glyphs.length, " glyphs"] }), jsx("div", { style: {
244
+ }, children: t('fontEditor.glyphs2', '{glyphs} glyphs').replace('{glyphs}', String(editor.glyphs.length)) }), jsx("div", { style: {
245
245
  display: 'grid',
246
246
  gridTemplateColumns: 'repeat(auto-fill, 36px)',
247
247
  gap: 2,
248
248
  maxHeight: 300,
249
249
  overflowY: 'auto',
250
- }, children: editor.glyphs.map((g) => (jsx("div", { onClick: () => {
250
+ }, children: editor.glyphs.map((g) => (jsx("div", { role: "button", tabIndex: 0, "aria-pressed": editor.selectedGlyph === g.unicode, onKeyDown: clickOnActivationKey, onClick: () => {
251
251
  editor.setSelectedGlyph(g.unicode);
252
252
  setPathPoints([]);
253
253
  }, title: `U+${g.unicode.toString(16).toUpperCase().padStart(4, '0')} - ${g.name}`, style: {
@@ -272,7 +272,7 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
272
272
  border: g.path
273
273
  ? '1px solid var(--nice-border, #373a40)'
274
274
  : '1px solid transparent',
275
- }, children: g.char }, g.unicode))) })] }), jsx("div", { style: { width: 320, fontSize: 11 }, children: selectedG ? (jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: ["\"", selectedG.char, "\" (U+", selectedG.unicode.toString(16).toUpperCase().padStart(4, '0'), ")"] }), jsxs("div", { style: { display: 'flex', gap: 2 }, children: [jsx("button", { onClick: () => setDrawMode('select'), style: btnStyle(drawMode === 'select'), children: t('fontEditor.select', 'Select') }), jsx("button", { onClick: () => setDrawMode('pen'), style: btnStyle(drawMode === 'pen'), children: "Pen" }), jsx("button", { onClick: clearPath, style: btnStyle(false), children: t('fontEditor.clear', 'Clear') }), jsx("button", { onClick: () => setShowGrid((g) => !g), style: btnStyle(showGrid), children: t('fontEditor.grid', 'Grid') })] })] }), jsxs("svg", { width: 280, height: 280, viewBox: `0 0 ${em} ${em}`, style: {
275
+ }, children: g.char }, g.unicode))) })] }), jsx("div", { style: { width: 320, fontSize: 11 }, children: selectedG ? (jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: ["\"", selectedG.char, "\" (U+", selectedG.unicode.toString(16).toUpperCase().padStart(4, '0'), ")"] }), jsxs("div", { style: { display: 'flex', gap: 2 }, children: [jsx("button", { onClick: () => setDrawMode('select'), style: btnStyle(drawMode === 'select'), children: t('fontEditor.select', 'Select') }), jsx("button", { onClick: () => setDrawMode('pen'), style: btnStyle(drawMode === 'pen'), children: t('fontEditor.pen', 'Pen') }), jsx("button", { onClick: clearPath, style: btnStyle(false), children: t('fontEditor.clear', 'Clear') }), jsx("button", { onClick: () => setShowGrid((g) => !g), style: btnStyle(showGrid), children: t('fontEditor.grid', 'Grid') })] })] }), jsxs("svg", { width: 280, height: 280, viewBox: `0 0 ${em} ${em}`, style: {
276
276
  background: 'var(--nice-bg, #141517)',
277
277
  borderRadius: 4,
278
278
  cursor: drawMode === 'pen' ? 'crosshair' : 'default',
@@ -280,7 +280,7 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
280
280
  Array.from({ length: 11 }, (_, i) => {
281
281
  const pos = (i / 10) * em;
282
282
  return (jsxs(React.Fragment, { children: [jsx("line", { x1: pos, y1: 0, x2: pos, y2: em, stroke: "var(--nice-overlay-light-5, rgba(255, 255, 255, 0.05))", strokeWidth: em * 0.002 }), jsx("line", { x1: 0, y1: pos, x2: em, y2: pos, stroke: "var(--nice-overlay-light-5, rgba(255, 255, 255, 0.05))", strokeWidth: em * 0.002 })] }, i));
283
- }), jsx("line", { x1: 0, y1: em - editor.metrics.ascent, x2: em, y2: em - editor.metrics.ascent, stroke: "rgba(51,154,240,0.3)", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.01}` }), jsx("line", { x1: 0, y1: em, x2: em, y2: em, stroke: "rgba(47,158,68,0.3)", strokeWidth: em * 0.003 }), jsx("line", { x1: 0, y1: em - editor.metrics.xHeight, x2: em, y2: em - editor.metrics.xHeight, stroke: "rgba(249,226,175,0.3)", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.01}` }), jsx("line", { x1: 0, y1: em - editor.metrics.capHeight, x2: em, y2: em - editor.metrics.capHeight, stroke: "rgba(243,139,168,0.3)", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.01}` }), jsx("line", { x1: selectedG.leftBearing, y1: 0, x2: selectedG.leftBearing, y2: em, stroke: "rgba(166,227,161,0.2)", strokeWidth: em * 0.002 }), jsx("line", { x1: selectedG.advanceWidth, y1: 0, x2: selectedG.advanceWidth, y2: em, stroke: "rgba(243,139,168,0.2)", strokeWidth: em * 0.002 }), selectedG.path && (jsx("path", { d: selectedG.path, fill: "rgba(205,214,244,0.8)", stroke: "#89b4fa", strokeWidth: em * 0.005 })), pathPoints.map((p, i) => (jsx("circle", { cx: p.x, cy: p.y, r: em * 0.015, fill: i === 0 ? 'var(--nice-success, #2f9e44)' : 'var(--nice-info, #339af0)', stroke: "var(--nice-bg, #fff)", strokeWidth: em * 0.003 }, i))), jsx("text", { x: em * 0.02, y: em - editor.metrics.ascent - em * 0.01, fill: "rgba(51,154,240,0.5)", fontSize: em * 0.03, children: "ascent" }), jsx("text", { x: em * 0.02, y: em - editor.metrics.xHeight - em * 0.01, fill: "rgba(249,226,175,0.5)", fontSize: em * 0.03, children: "x-height" }), jsx("text", { x: em * 0.02, y: em - editor.metrics.capHeight - em * 0.01, fill: "rgba(243,139,168,0.5)", fontSize: em * 0.03, children: "cap" })] }), jsxs("div", { children: [jsx("label", { children: t('fontEditor.svgPath', 'SVG Path') }), jsx("textarea", { value: selectedG.path, onChange: (e) => editor.updateGlyph(selectedG.unicode, { path: e.target.value }), rows: 3, style: { ...fieldStyle, fontFamily: 'monospace', resize: 'vertical' } })] }), jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [jsx("label", { children: t('fontEditor.advanceWidth', 'Advance Width') }), jsx("input", { type: "number", value: selectedG.advanceWidth, onChange: (e) => editor.updateGlyph(selectedG.unicode, {
283
+ }), jsx("line", { x1: 0, y1: em - editor.metrics.ascent, x2: em, y2: em - editor.metrics.ascent, stroke: "rgba(51,154,240,0.3)", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.01}` }), jsx("line", { x1: 0, y1: em, x2: em, y2: em, stroke: "rgba(47,158,68,0.3)", strokeWidth: em * 0.003 }), jsx("line", { x1: 0, y1: em - editor.metrics.xHeight, x2: em, y2: em - editor.metrics.xHeight, stroke: "rgba(249,226,175,0.3)", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.01}` }), jsx("line", { x1: 0, y1: em - editor.metrics.capHeight, x2: em, y2: em - editor.metrics.capHeight, stroke: "rgba(243,139,168,0.3)", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.01}` }), jsx("line", { x1: selectedG.leftBearing, y1: 0, x2: selectedG.leftBearing, y2: em, stroke: "rgba(166,227,161,0.2)", strokeWidth: em * 0.002 }), jsx("line", { x1: selectedG.advanceWidth, y1: 0, x2: selectedG.advanceWidth, y2: em, stroke: "rgba(243,139,168,0.2)", strokeWidth: em * 0.002 }), selectedG.path && (jsx("path", { d: selectedG.path, fill: "rgba(205,214,244,0.8)", stroke: "#89b4fa", strokeWidth: em * 0.005 })), pathPoints.map((p, i) => (jsx("circle", { cx: p.x, cy: p.y, r: em * 0.015, fill: i === 0 ? 'var(--nice-success, #2f9e44)' : 'var(--nice-info, #339af0)', stroke: "var(--nice-bg, #fff)", strokeWidth: em * 0.003 }, i))), jsx("text", { x: em * 0.02, y: em - editor.metrics.ascent - em * 0.01, fill: "rgba(51,154,240,0.5)", fontSize: em * 0.03, children: t('fontEditor.ascent2', 'ascent') }), jsx("text", { x: em * 0.02, y: em - editor.metrics.xHeight - em * 0.01, fill: "rgba(249,226,175,0.5)", fontSize: em * 0.03, children: t('fontEditor.xHeight2', 'x-height') }), jsx("text", { x: em * 0.02, y: em - editor.metrics.capHeight - em * 0.01, fill: "rgba(243,139,168,0.5)", fontSize: em * 0.03, children: t('fontEditor.cap2', 'cap') })] }), jsxs("div", { children: [jsx("label", { children: t('fontEditor.svgPath', 'SVG Path') }), jsx("textarea", { value: selectedG.path, onChange: (e) => editor.updateGlyph(selectedG.unicode, { path: e.target.value }), rows: 3, style: { ...fieldStyle, fontFamily: 'monospace', resize: 'vertical' } })] }), jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [jsx("label", { children: t('fontEditor.advanceWidth', 'Advance Width') }), jsx("input", { type: "number", value: selectedG.advanceWidth, onChange: (e) => editor.updateGlyph(selectedG.unicode, {
284
284
  advanceWidth: Number(e.target.value),
285
285
  }), style: fieldStyle }), jsx("label", { children: t('fontEditor.leftBearing', 'Left Bearing') }), jsx("input", { type: "number", value: selectedG.leftBearing, onChange: (e) => editor.updateGlyph(selectedG.unicode, { leftBearing: Number(e.target.value) }), style: fieldStyle })] })] })) : (jsx("div", { style: {
286
286
  color: 'var(--nice-text-secondary, #868e96)',
@@ -293,7 +293,7 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
293
293
  ['lineGap', 'Line Gap'],
294
294
  ['xHeight', 'x-Height'],
295
295
  ['capHeight', 'Cap Height'],
296
- ].map(([key, label]) => (jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }, children: [jsx("span", { style: { minWidth: 100, fontSize: 11 }, children: label }), jsx("input", { type: "number", value: editor.metrics[key], onChange: (e) => editor.setMetrics({ ...editor.metrics, [key]: Number(e.target.value) }), style: fieldStyle })] }, key))), jsxs("svg", { width: 300, height: 200, viewBox: `0 0 ${em} ${em}`, style: { background: 'var(--nice-bg, #141517)', borderRadius: 4, marginTop: 12 }, children: [jsx("rect", { x: 0, y: em - editor.metrics.ascent, width: em, height: editor.metrics.ascent - editor.metrics.descent, fill: "rgba(51,154,240,0.05)", stroke: "rgba(51,154,240,0.2)", strokeWidth: em * 0.003 }), jsx("line", { x1: 0, y1: em - editor.metrics.ascent, x2: em, y2: em - editor.metrics.ascent, stroke: "var(--nice-info, #339af0)", strokeWidth: em * 0.004 }), jsx("line", { x1: 0, y1: em, x2: em, y2: em, stroke: "var(--nice-success, #2f9e44)", strokeWidth: em * 0.004 }), jsx("line", { x1: 0, y1: em + Math.abs(editor.metrics.descent), x2: em, y2: em + Math.abs(editor.metrics.descent), stroke: "#e03131", strokeWidth: em * 0.004 }), jsx("line", { x1: 0, y1: em - editor.metrics.xHeight, x2: em, y2: em - editor.metrics.xHeight, stroke: "#f9e2af", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.015}` }), jsx("line", { x1: 0, y1: em - editor.metrics.capHeight, x2: em, y2: em - editor.metrics.capHeight, stroke: "#f38ba8", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.015}` }), jsxs("text", { x: em * 0.02, y: em - editor.metrics.ascent + em * 0.05, fill: "var(--nice-info, #339af0)", fontSize: em * 0.04, children: ["Ascent (", editor.metrics.ascent, ")"] }), jsx("text", { x: em * 0.02, y: em + em * 0.05, fill: "var(--nice-success, #2f9e44)", fontSize: em * 0.04, children: t('fontEditor.baseline', 'Baseline') }), jsxs("text", { x: em * 0.02, y: em - editor.metrics.xHeight + em * 0.05, fill: "#f9e2af", fontSize: em * 0.04, children: ["x-Height (", editor.metrics.xHeight, ")"] }), jsxs("text", { x: em * 0.02, y: em - editor.metrics.capHeight + em * 0.05, fill: "#f38ba8", fontSize: em * 0.04, children: ["Cap (", editor.metrics.capHeight, ")"] })] })] })), tab === 'kerning' && (jsxs("div", { style: { maxWidth: 500 }, children: [jsx("strong", { style: { fontSize: 12, display: 'block', marginBottom: 8 }, children: t('fontEditor.kerningPairs', 'Kerning Pairs') }), jsxs("div", { style: { display: 'flex', gap: 4, marginBottom: 8, alignItems: 'center', fontSize: 11 }, children: [jsx("input", { value: newKernLeft, onChange: (e) => setNewKernLeft(e.target.value), maxLength: 1, style: { ...fieldStyle, width: 30, textAlign: 'center' } }), jsx("input", { value: newKernRight, onChange: (e) => setNewKernRight(e.target.value), maxLength: 1, style: { ...fieldStyle, width: 30, textAlign: 'center' } }), jsx("button", { onClick: () => {
296
+ ].map(([key, label]) => (jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }, children: [jsx("span", { style: { minWidth: 100, fontSize: 11 }, children: label }), jsx("input", { type: "number", value: editor.metrics[key], onChange: (e) => editor.setMetrics({ ...editor.metrics, [key]: Number(e.target.value) }), style: fieldStyle })] }, key))), jsxs("svg", { width: 300, height: 200, viewBox: `0 0 ${em} ${em}`, style: { background: 'var(--nice-bg, #141517)', borderRadius: 4, marginTop: 12 }, children: [jsx("rect", { x: 0, y: em - editor.metrics.ascent, width: em, height: editor.metrics.ascent - editor.metrics.descent, fill: "rgba(51,154,240,0.05)", stroke: "rgba(51,154,240,0.2)", strokeWidth: em * 0.003 }), jsx("line", { x1: 0, y1: em - editor.metrics.ascent, x2: em, y2: em - editor.metrics.ascent, stroke: "var(--nice-info, #339af0)", strokeWidth: em * 0.004 }), jsx("line", { x1: 0, y1: em, x2: em, y2: em, stroke: "var(--nice-success, #2f9e44)", strokeWidth: em * 0.004 }), jsx("line", { x1: 0, y1: em + Math.abs(editor.metrics.descent), x2: em, y2: em + Math.abs(editor.metrics.descent), stroke: "#e03131", strokeWidth: em * 0.004 }), jsx("line", { x1: 0, y1: em - editor.metrics.xHeight, x2: em, y2: em - editor.metrics.xHeight, stroke: "#f9e2af", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.015}` }), jsx("line", { x1: 0, y1: em - editor.metrics.capHeight, x2: em, y2: em - editor.metrics.capHeight, stroke: "#f38ba8", strokeWidth: em * 0.003, strokeDasharray: `${em * 0.015}` }), jsx("text", { x: em * 0.02, y: em - editor.metrics.ascent + em * 0.05, fill: "var(--nice-info, #339af0)", fontSize: em * 0.04, children: t('fontEditor.ascent', 'Ascent ({ascent})').replace('{ascent}', String(editor.metrics.ascent)) }), jsx("text", { x: em * 0.02, y: em + em * 0.05, fill: "var(--nice-success, #2f9e44)", fontSize: em * 0.04, children: t('fontEditor.baseline', 'Baseline') }), jsx("text", { x: em * 0.02, y: em - editor.metrics.xHeight + em * 0.05, fill: "#f9e2af", fontSize: em * 0.04, children: t('fontEditor.xHeight', 'x-Height ({xHeight})').replace('{xHeight}', String(editor.metrics.xHeight)) }), jsx("text", { x: em * 0.02, y: em - editor.metrics.capHeight + em * 0.05, fill: "#f38ba8", fontSize: em * 0.04, children: t('fontEditor.cap', 'Cap ({capHeight})').replace('{capHeight}', String(editor.metrics.capHeight)) })] })] })), tab === 'kerning' && (jsxs("div", { style: { maxWidth: 500 }, children: [jsx("strong", { style: { fontSize: 12, display: 'block', marginBottom: 8 }, children: t('fontEditor.kerningPairs', 'Kerning Pairs') }), jsxs("div", { style: { display: 'flex', gap: 4, marginBottom: 8, alignItems: 'center', fontSize: 11 }, children: [jsx("input", { value: newKernLeft, onChange: (e) => setNewKernLeft(e.target.value), maxLength: 1, style: { ...fieldStyle, width: 30, textAlign: 'center' } }), jsx("input", { value: newKernRight, onChange: (e) => setNewKernRight(e.target.value), maxLength: 1, style: { ...fieldStyle, width: 30, textAlign: 'center' } }), jsx("button", { onClick: () => {
297
297
  if (newKernLeft && newKernRight) {
298
298
  setKerningPairs((prev) => [
299
299
  ...prev,