@nice2dev/ui-graphics 1.2.0 → 1.2.2

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 (98) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/dist/cjs/animation/AnimatedPerson.js +3 -1
  3. package/dist/cjs/animation/Audience.js +3 -1
  4. package/dist/cjs/animation/rig/RigPlayer.js +1 -1
  5. package/dist/cjs/animation/rig/presets.js +67 -55
  6. package/dist/cjs/animation/shapes/headwear.js +4 -1
  7. package/dist/cjs/core/NiceBrushEngine.js +8 -5
  8. package/dist/cjs/core/collaboration.js +11 -6
  9. package/dist/cjs/core/editorAsk.js +48 -0
  10. package/dist/cjs/core/i18n.js +7 -3
  11. package/dist/cjs/core/minimap.js +21 -20
  12. package/dist/cjs/core/plugins.js +1 -1
  13. package/dist/cjs/core/rtl.js +1 -1
  14. package/dist/cjs/core/theme.js +1 -1
  15. package/dist/cjs/font/NiceFontEditor.js +14 -12
  16. package/dist/cjs/game/GameAsset2dEditor.js +75 -44
  17. package/dist/cjs/nice2dev-ui-graphics.css +1 -1
  18. package/dist/cjs/photo/FilterThumb.js +3 -3
  19. package/dist/cjs/photo/NiceAITools.js +6 -4
  20. package/dist/cjs/photo/PhotoEditor.js +3 -1
  21. package/dist/cjs/photo/PhotoEditorCanvas.js +8 -6
  22. package/dist/cjs/photo/PhotoEditorLeftPanel.js +29 -29
  23. package/dist/cjs/photo/PhotoEditorToolbar.js +4 -4
  24. package/dist/cjs/photo/photoFilters.js +1 -0
  25. package/dist/cjs/pixel/PixelEditor.js +6 -3
  26. package/dist/cjs/pixel/PixelEditorCanvas.js +10 -6
  27. package/dist/cjs/pixel/PixelEditorMenuBar.js +95 -30
  28. package/dist/cjs/pixel/PixelEditorRightPanel.js +32 -24
  29. package/dist/cjs/pixel/PixelEditorStatusBar.js +12 -4
  30. package/dist/cjs/pixel/PixelEditorTimeline.js +15 -2
  31. package/dist/cjs/pixel/PixelEditorToolbar.js +12 -6
  32. package/dist/cjs/pixel/SpriteGeneratorPanel.js +28 -17
  33. package/dist/cjs/pixel/animalGeneratorPresets.js +31 -31
  34. package/dist/cjs/pixel/buildingGeneratorPresets.js +24 -24
  35. package/dist/cjs/pixel/characterGeneratorPresets.js +25 -25
  36. package/dist/cjs/pixel/usePixelEditor.js +13 -10
  37. package/dist/cjs/texture/Nice3DTexturePainter.js +16 -10
  38. package/dist/cjs/ui/NiceUIDesigner.js +17 -13
  39. package/dist/cjs/vector/NiceSvgToCode.js +7 -5
  40. package/dist/cjs/vector/VectorEditor.js +3 -2
  41. package/dist/cjs/vector/VectorEditorMenuBar.js +7 -1
  42. package/dist/cjs/vector/VectorEditorRightPanel.js +1 -1
  43. package/dist/cjs/vector/VectorEditorShapeRenderer.js +3 -1
  44. package/dist/cjs/vector/useVectorEditor.js +38 -29
  45. package/dist/cjs/vector/vectorEditorImport.js +1 -0
  46. package/dist/cjs/vector/vectorIconImport.js +1 -0
  47. package/dist/esm/animation/AnimatedPerson.js +3 -1
  48. package/dist/esm/animation/Audience.js +3 -1
  49. package/dist/esm/animation/rig/RigPlayer.js +1 -1
  50. package/dist/esm/animation/rig/presets.js +68 -56
  51. package/dist/esm/animation/shapes/headwear.js +4 -1
  52. package/dist/esm/animation/spineExport.js +1 -1
  53. package/dist/esm/core/NiceBrushEngine.js +8 -5
  54. package/dist/esm/core/collaboration.js +11 -6
  55. package/dist/esm/core/editorAsk.js +45 -0
  56. package/dist/esm/core/i18n.js +8 -4
  57. package/dist/esm/core/minimap.js +21 -20
  58. package/dist/esm/core/plugins.js +1 -1
  59. package/dist/esm/core/rtl.js +1 -1
  60. package/dist/esm/core/theme.js +1 -1
  61. package/dist/esm/font/NiceFontEditor.js +14 -12
  62. package/dist/esm/game/GameAsset2dEditor.js +75 -44
  63. package/dist/esm/nice2dev-ui-graphics.css +1 -1
  64. package/dist/esm/photo/FilterThumb.js +3 -3
  65. package/dist/esm/photo/NiceAITools.js +6 -4
  66. package/dist/esm/photo/PhotoEditor.js +3 -1
  67. package/dist/esm/photo/PhotoEditorCanvas.js +8 -6
  68. package/dist/esm/photo/PhotoEditorLeftPanel.js +29 -29
  69. package/dist/esm/photo/PhotoEditorToolbar.js +4 -4
  70. package/dist/esm/photo/photoFilters.js +1 -0
  71. package/dist/esm/pixel/PixelEditor.js +6 -3
  72. package/dist/esm/pixel/PixelEditorCanvas.js +10 -6
  73. package/dist/esm/pixel/PixelEditorMenuBar.js +95 -30
  74. package/dist/esm/pixel/PixelEditorRightPanel.js +32 -24
  75. package/dist/esm/pixel/PixelEditorStatusBar.js +12 -4
  76. package/dist/esm/pixel/PixelEditorTimeline.js +15 -2
  77. package/dist/esm/pixel/PixelEditorToolbar.js +12 -6
  78. package/dist/esm/pixel/SpriteGeneratorPanel.js +28 -17
  79. package/dist/esm/pixel/animalGeneratorPresets.js +32 -32
  80. package/dist/esm/pixel/buildingGeneratorPresets.js +25 -25
  81. package/dist/esm/pixel/characterGeneratorPresets.js +26 -26
  82. package/dist/esm/pixel/usePixelEditor.js +13 -10
  83. package/dist/esm/texture/Nice3DTexturePainter.js +16 -10
  84. package/dist/esm/ui/NiceUIDesigner.js +17 -13
  85. package/dist/esm/vector/NiceSvgToCode.js +7 -5
  86. package/dist/esm/vector/VectorEditor.js +3 -2
  87. package/dist/esm/vector/VectorEditorMenuBar.js +7 -1
  88. package/dist/esm/vector/VectorEditorRightPanel.js +1 -1
  89. package/dist/esm/vector/VectorEditorShapeRenderer.js +3 -1
  90. package/dist/esm/vector/useVectorEditor.js +38 -29
  91. package/dist/esm/vector/vectorEditorImport.js +1 -0
  92. package/dist/esm/vector/vectorIconImport.js +1 -0
  93. package/dist/types/core/LocalUI.d.ts +1 -2
  94. package/dist/types/core/editorAsk.d.ts +26 -0
  95. package/dist/types/core/i18n.d.ts +1 -17
  96. package/dist/types/pixel/usePixelEditor.d.ts +3 -0
  97. package/dist/types/vector/useVectorEditor.d.ts +2 -0
  98. package/package.json +5 -5
@@ -1,65 +1,77 @@
1
- import { DEFAULT_RIG, DEFAULT_POSE } from './rigTypes.js';
1
+ import { DEFAULT_POSE, DEFAULT_RIG } from './rigTypes.js';
2
2
 
3
3
  /** Slim proportions preset. */
4
- const RIG_SLIM = {
5
- ...DEFAULT_RIG,
6
- lUpperArm: { ...DEFAULT_RIG.lUpperArm, length: 0.11 },
7
- lForearm: { ...DEFAULT_RIG.lForearm, length: 0.11 },
8
- rUpperArm: { ...DEFAULT_RIG.rUpperArm, length: 0.11 },
9
- rForearm: { ...DEFAULT_RIG.rForearm, length: 0.11 },
10
- lThigh: { ...DEFAULT_RIG.lThigh, length: 0.14 },
11
- lShin: { ...DEFAULT_RIG.lShin, length: 0.14 },
12
- rThigh: { ...DEFAULT_RIG.rThigh, length: 0.14 },
13
- rShin: { ...DEFAULT_RIG.rShin, length: 0.14 },
14
- };
4
+ const RIG_SLIM = /* @__PURE__ */ (function () {
5
+ return {
6
+ ...DEFAULT_RIG,
7
+ lUpperArm: { ...DEFAULT_RIG.lUpperArm, length: 0.11 },
8
+ lForearm: { ...DEFAULT_RIG.lForearm, length: 0.11 },
9
+ rUpperArm: { ...DEFAULT_RIG.rUpperArm, length: 0.11 },
10
+ rForearm: { ...DEFAULT_RIG.rForearm, length: 0.11 },
11
+ lThigh: { ...DEFAULT_RIG.lThigh, length: 0.14 },
12
+ lShin: { ...DEFAULT_RIG.lShin, length: 0.14 },
13
+ rThigh: { ...DEFAULT_RIG.rThigh, length: 0.14 },
14
+ rShin: { ...DEFAULT_RIG.rShin, length: 0.14 },
15
+ };
16
+ })();
15
17
  /** Broad shoulders preset. */
16
- const RIG_HERO = {
17
- ...DEFAULT_RIG,
18
- lShoulder: { ...DEFAULT_RIG.lShoulder, offset: [-0.13, 0] },
19
- rShoulder: { ...DEFAULT_RIG.rShoulder, offset: [0.13, 0] },
20
- lUpperArm: { ...DEFAULT_RIG.lUpperArm, length: 0.13 },
21
- rUpperArm: { ...DEFAULT_RIG.rUpperArm, length: 0.13 },
22
- };
18
+ const RIG_HERO = /* @__PURE__ */ (function () {
19
+ return {
20
+ ...DEFAULT_RIG,
21
+ lShoulder: { ...DEFAULT_RIG.lShoulder, offset: [-0.13, 0] },
22
+ rShoulder: { ...DEFAULT_RIG.rShoulder, offset: [0.13, 0] },
23
+ lUpperArm: { ...DEFAULT_RIG.lUpperArm, length: 0.13 },
24
+ rUpperArm: { ...DEFAULT_RIG.rUpperArm, length: 0.13 },
25
+ };
26
+ })();
23
27
  /** Robot-like short limbs preset. */
24
- const RIG_ROBOT = {
25
- ...DEFAULT_RIG,
26
- lUpperArm: { ...DEFAULT_RIG.lUpperArm, length: 0.1 },
27
- lForearm: { ...DEFAULT_RIG.lForearm, length: 0.1 },
28
- rUpperArm: { ...DEFAULT_RIG.rUpperArm, length: 0.1 },
29
- rForearm: { ...DEFAULT_RIG.rForearm, length: 0.1 },
30
- lThigh: { ...DEFAULT_RIG.lThigh, length: 0.13 },
31
- lShin: { ...DEFAULT_RIG.lShin, length: 0.13 },
32
- rThigh: { ...DEFAULT_RIG.rThigh, length: 0.13 },
33
- rShin: { ...DEFAULT_RIG.rShin, length: 0.13 },
34
- };
28
+ const RIG_ROBOT = /* @__PURE__ */ (function () {
29
+ return {
30
+ ...DEFAULT_RIG,
31
+ lUpperArm: { ...DEFAULT_RIG.lUpperArm, length: 0.1 },
32
+ lForearm: { ...DEFAULT_RIG.lForearm, length: 0.1 },
33
+ rUpperArm: { ...DEFAULT_RIG.rUpperArm, length: 0.1 },
34
+ rForearm: { ...DEFAULT_RIG.rForearm, length: 0.1 },
35
+ lThigh: { ...DEFAULT_RIG.lThigh, length: 0.13 },
36
+ lShin: { ...DEFAULT_RIG.lShin, length: 0.13 },
37
+ rThigh: { ...DEFAULT_RIG.rThigh, length: 0.13 },
38
+ rShin: { ...DEFAULT_RIG.rShin, length: 0.13 },
39
+ };
40
+ })();
35
41
  /** Happy pose. */
36
- const POSE_HAPPY = {
37
- ...DEFAULT_POSE,
38
- spineUpper: 80,
39
- neck: 70,
40
- head: 60,
41
- lUpperArm: 60,
42
- lForearm: 20,
43
- rUpperArm: 120,
44
- rForearm: 160,
45
- };
42
+ const POSE_HAPPY = /* @__PURE__ */ (function () {
43
+ return {
44
+ ...DEFAULT_POSE,
45
+ spineUpper: 80,
46
+ neck: 70,
47
+ head: 60,
48
+ lUpperArm: 60,
49
+ lForearm: 20,
50
+ rUpperArm: 120,
51
+ rForearm: 160,
52
+ };
53
+ })();
46
54
  /** Angry pose. */
47
- const POSE_ANGRY = {
48
- ...DEFAULT_POSE,
49
- head: 90,
50
- lUpperArm: 150,
51
- rUpperArm: 30,
52
- lForearm: 160,
53
- rForearm: 20,
54
- };
55
+ const POSE_ANGRY = /* @__PURE__ */ (function () {
56
+ return {
57
+ ...DEFAULT_POSE,
58
+ head: 90,
59
+ lUpperArm: 150,
60
+ rUpperArm: 30,
61
+ lForearm: 160,
62
+ rForearm: 20,
63
+ };
64
+ })();
55
65
  /** Dance pose A. */
56
- const POSE_DANCE_A = {
57
- ...DEFAULT_POSE,
58
- pelvis: 100,
59
- lThigh: 120,
60
- rThigh: 70,
61
- lUpperArm: 30,
62
- rUpperArm: 150,
63
- };
66
+ const POSE_DANCE_A = /* @__PURE__ */ (function () {
67
+ return {
68
+ ...DEFAULT_POSE,
69
+ pelvis: 100,
70
+ lThigh: 120,
71
+ rThigh: 70,
72
+ lUpperArm: 30,
73
+ rUpperArm: 150,
74
+ };
75
+ })();
64
76
 
65
77
  export { POSE_ANGRY, POSE_DANCE_A, POSE_HAPPY, RIG_HERO, RIG_ROBOT, RIG_SLIM };
@@ -8,13 +8,16 @@ const HeadwearCap = ({ colors, scale = 1, transform }) => {
8
8
  };
9
9
  const HeadwearCrown = ({ colors, scale = 1, transform }) => {
10
10
  const base = pick(colors, 0, 'var(--nice-warning, #FFD700)');
11
- return (jsx("g", { transform: `${transform !== null && transform !== void 0 ? transform : ''} scale(${scale})`, children: jsx("path", { d: "M-22 -30 l6 -16 l8 10 l8 -14 l8 14 l8 -10 l6 16 z", fill: base, stroke: "#B8860B", strokeWidth: 1.2 }) }));
11
+ return (jsx("g", { transform: `${transform !== null && transform !== void 0 ? transform : ''} scale(${scale})`, children: jsx("path", { d: "M-22 -30 l6 -16 l8 10 l8 -14 l8 14 l8 -10 l6 16 z", fill: base,
12
+ // audit-allow: literal-color — a character-generator shape: its own drawing colour
13
+ stroke: "#B8860B", strokeWidth: 1.2 }) }));
12
14
  };
13
15
  const HeadwearHeadphones = ({ colors, scale = 1, transform }) => {
14
16
  const base = pick(colors, 0, 'var(--nice-text, #333)');
15
17
  return (jsxs("g", { transform: `${transform !== null && transform !== void 0 ? transform : ''} scale(${scale})`, stroke: base, strokeWidth: 2.5, fill: "none", children: [jsx("path", { d: "M-30 0 q0 -40 30 -42 q30 2 30 42" }), jsx("rect", { x: -36, y: -6, width: 10, height: 16, rx: 3, fill: base }), jsx("rect", { x: 26, y: -6, width: 10, height: 16, rx: 3, fill: base })] }));
16
18
  };
17
19
  const HeadwearBeanie = ({ colors, scale = 1, transform }) => {
20
+ // audit-allow: literal-color — the hat's default colour when the character sets none
18
21
  const base = pick(colors, 0, '#E53E3E');
19
22
  return (jsxs("g", { transform: `${transform !== null && transform !== void 0 ? transform : ''} scale(${scale})`, children: [jsx("path", { d: "M-28 -14 q0 -22 28 -26 q28 4 28 26 z", fill: base }), jsx("circle", { cx: 0, cy: -42, r: 6, fill: base })] }));
20
23
  };
@@ -1,5 +1,5 @@
1
1
  import { solveFK } from './rig/rigMath.js';
2
- import { DEFAULT_RIG, DEFAULT_POSE, BONE_ORDER } from './rig/rigTypes.js';
2
+ import { DEFAULT_RIG, BONE_ORDER, DEFAULT_POSE } from './rig/rigTypes.js';
3
3
 
4
4
  /**
5
5
  * spineExport.ts — Export animations to Spine JSON format.
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui-core';
3
3
  import { useEffect, useState, useRef, useCallback } from 'react';
4
+ import { useNiceTranslation } from './i18n.js';
4
5
 
5
6
  /* ── Brush Presets ── */
6
7
  const DEFAULT_DYNAMICS = {
@@ -541,23 +542,25 @@ color = '#000000') {
541
542
  }
542
543
  /* ── Component ── */
543
544
  const NiceBrushEngine = ({ canvas, color = '#000000', onStrokeEnd, className = '', }) => {
545
+ const { t } = useNiceTranslation();
544
546
  const engine = useBrushEngine(canvas, color);
547
+ const { endStroke } = engine;
545
548
  // Override endStroke to fire callback
546
549
  useEffect(() => {
547
550
  if (!canvas) {
548
551
  return;
549
552
  }
550
553
  const onUp = () => {
551
- const stroke = engine.endStroke();
554
+ const stroke = endStroke();
552
555
  if (stroke && onStrokeEnd) {
553
556
  onStrokeEnd(stroke);
554
557
  }
555
558
  };
556
559
  canvas.addEventListener('pointerup', onUp);
557
560
  return () => canvas.removeEventListener('pointerup', onUp);
558
- }, [canvas, engine.endStroke, onStrokeEnd]);
561
+ }, [canvas, endStroke, onStrokeEnd]);
559
562
  const s = engine.settings;
560
- 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 }), " Brush Engine v2"] }), jsx("div", { style: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: engine.presetNames.map((name) => {
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) => {
561
564
  var _a, _b;
562
565
  return (jsx("button", { type: "button", onClick: () => engine.applyPreset(name), style: {
563
566
  padding: '2px 6px',
@@ -570,13 +573,13 @@ const NiceBrushEngine = ({ canvas, color = '#000000', onStrokeEnd, className = '
570
573
  : 'transparent',
571
574
  color: s.name === ((_b = BRUSH_PRESETS[name]) === null || _b === void 0 ? void 0 : _b.name) ? 'var(--nice-color-white, #fff)' : 'inherit',
572
575
  }, children: BRUSH_PRESETS[name].name }, name));
573
- }) }), 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: "Circle" }), jsx("option", { value: "square", children: "Square" }), jsx("option", { value: "diamond", children: "Diamond" }), jsx("option", { value: "texture", children: "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 }), " 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({
574
577
  ...s,
575
578
  sizeDynamics: {
576
579
  ...s.sizeDynamics,
577
580
  source: e.target.value,
578
581
  },
579
- }), style: { width: '100%' }, children: [jsx("option", { value: "none", children: "Off" }), jsx("option", { value: "pressure", children: "Pressure" }), jsx("option", { value: "tilt", children: "Tilt" }), jsx("option", { value: "speed", children: "Speed" }), jsx("option", { value: "random", children: "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 }) }), "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')] })] }));
580
583
  };
581
584
 
582
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 () => {
@@ -0,0 +1,45 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { NiceConfirmDialog, NicePromptDialog } from '@nice2dev/ui';
3
+ import { useState, useRef, useCallback } from 'react';
4
+
5
+ /**
6
+ * The library's dialogs in place of `window.prompt` / `window.confirm` for the editor hooks,
7
+ * which hold state but render nothing: `ask` resolves with the answer (`null` when cancelled)
8
+ * and `pendingAsk` is the open question, for the editor component to pass to
9
+ * {@link EditorAskDialog}. One question at a time: a new one settles the open one as cancelled.
10
+ */
11
+ function useEditorAsk() {
12
+ const [pendingAsk, setPendingAsk] = useState(null);
13
+ const pendingRef = useRef(null);
14
+ const ask = useCallback((kind, text, defaultValue) => new Promise((resolve) => {
15
+ var _a;
16
+ (_a = pendingRef.current) === null || _a === void 0 ? void 0 : _a.answer(null);
17
+ const next = {
18
+ kind,
19
+ text,
20
+ defaultValue,
21
+ answer: (value) => {
22
+ if (pendingRef.current === next) {
23
+ pendingRef.current = null;
24
+ setPendingAsk(null);
25
+ }
26
+ resolve(value);
27
+ },
28
+ };
29
+ pendingRef.current = next;
30
+ setPendingAsk(next);
31
+ }), []);
32
+ return { pendingAsk, ask };
33
+ }
34
+ /** Shows the question {@link useEditorAsk} is waiting on, if any. */
35
+ const EditorAskDialog = ({ ask }) => {
36
+ if (!ask) {
37
+ return null;
38
+ }
39
+ if (ask.kind === 'confirm') {
40
+ return (jsx(NiceConfirmDialog, { visible: true, message: ask.text, onAccept: () => ask.answer(''), onReject: () => ask.answer(null) }));
41
+ }
42
+ return (jsx(NicePromptDialog, { open: true, title: ask.text, defaultValue: ask.defaultValue, onSubmit: (value) => ask.answer(value), onCancel: () => ask.answer(null) }));
43
+ };
44
+
45
+ export { EditorAskDialog, useEditorAsk };
@@ -1,11 +1,15 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { createContext, useContext } from 'react';
2
+ import { useNiceTranslation as useNiceTranslation$1 } from '@nice2dev/ui-core';
3
+ import { useContext, useMemo, createContext } from 'react';
3
4
 
4
- const fallback = (_key, defaultValue) => defaultValue;
5
- const NiceI18nContext = createContext(fallback);
5
+ /** `null` outside this package's provider: the global translator applies. */
6
+ const NiceI18nContext = /* @__PURE__ */ createContext(null);
7
+ /** Overrides the app's global translator for the graphics controls below it. */
6
8
  const NiceI18nProvider = ({ t, children }) => (jsx(NiceI18nContext.Provider, { value: t, children: children }));
7
9
  function useNiceTranslation() {
8
- const t = useContext(NiceI18nContext);
10
+ const own = useContext(NiceI18nContext);
11
+ const { t: global } = useNiceTranslation$1();
12
+ const t = useMemo(() => own !== null && own !== void 0 ? own : ((key, defaultValue) => global(key, defaultValue)), [own, global]);
9
13
  return { t };
10
14
  }
11
15
 
@@ -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);
@@ -110,7 +110,7 @@ function usePlugins(editor) {
110
110
  };
111
111
  }
112
112
  // ── Plugin Context (React) ──────────────────────────────────────
113
- const PluginContext = React.createContext(PluginRegistry);
113
+ const PluginContext = /* @__PURE__ */ React.createContext(PluginRegistry);
114
114
  const NicePluginProvider = ({ plugins, children }) => {
115
115
  React.useEffect(() => {
116
116
  if (plugins) {
@@ -12,7 +12,7 @@ const defaultRTLConfig = {
12
12
  mirrorToolbar: false,
13
13
  mirrorPanels: false,
14
14
  };
15
- const RTLContext = React.createContext(defaultRTLConfig);
15
+ const RTLContext = /* @__PURE__ */ React.createContext(defaultRTLConfig);
16
16
  /** Provider for RTL configuration. Wraps editors to enable RTL support. */
17
17
  const NiceRTLProvider = ({ direction = 'ltr', mirrorToolbar = true, mirrorPanels = true, children }) => {
18
18
  const config = React.useMemo(() => ({
@@ -107,7 +107,7 @@ function resolveThemeMode(mode) {
107
107
  }
108
108
  return mode;
109
109
  }
110
- const ThemeContext = React.createContext({
110
+ const ThemeContext = /* @__PURE__ */ React.createContext({
111
111
  theme: DARK_THEME,
112
112
  mode: 'dark',
113
113
  setMode: () => { },
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceTutorialButton, resolveTutorial } from '@nice2dev/ui';
3
3
  import { NiceIcon } from '@nice2dev/ui-core';
4
4
  import React from 'react';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
7
8
  const FONT_EDITOR_TOUR = [
@@ -132,6 +133,7 @@ function useFontEditor(initialName) {
132
133
  /** Font glyph editor with metrics, visual path editing, and preview. */
133
134
  const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial, }) => {
134
135
  var _a;
136
+ const { t } = useNiceTranslation();
135
137
  const editor = useFontEditor(fontName);
136
138
  const [drawMode, setDrawMode] = React.useState('select');
137
139
  const [pathPoints, setPathPoints] = React.useState([]);
@@ -219,7 +221,7 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
219
221
  color: 'var(--nice-text, #c1c2c5)',
220
222
  padding: 12,
221
223
  borderRadius: 8,
222
- }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, FONT_EDITOR_TOUR) }) }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 12 }, children: [jsx("strong", { style: { fontSize: 14 }, children: "Font Editor" }), jsx("input", { value: editor.fontName, onChange: (e) => editor.setFontName(e.target.value), placeholder: "Font name", style: { flex: 1, maxWidth: 200, ...fieldStyle, fontSize: 12 } }), jsx("button", { onClick: handleSave, style: {
224
+ }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, FONT_EDITOR_TOUR) }) }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 12 }, children: [jsx("strong", { style: { fontSize: 14 }, children: t('fontEditor.fontEditor', 'Font Editor') }), jsx("input", { value: editor.fontName, onChange: (e) => editor.setFontName(e.target.value), placeholder: t('fontEditor.fontName', 'Font name'), style: { flex: 1, maxWidth: 200, ...fieldStyle, fontSize: 12 } }), jsx("button", { onClick: handleSave, style: {
223
225
  padding: '4px 12px',
224
226
  background: 'var(--nice-success, #2f9e44)',
225
227
  color: 'var(--nice-text-on-primary, #fff)',
@@ -227,7 +229,7 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
227
229
  borderRadius: 3,
228
230
  cursor: 'pointer',
229
231
  fontSize: 11,
230
- }, children: "Save" }), jsx("button", { onClick: handleExportSvg, style: {
232
+ }, children: t('fontEditor.save', 'Save') }), jsx("button", { onClick: handleExportSvg, style: {
231
233
  padding: '4px 12px',
232
234
  background: '#228be6',
233
235
  color: 'var(--nice-color-white, #fff)',
@@ -235,11 +237,11 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
235
237
  borderRadius: 3,
236
238
  cursor: 'pointer',
237
239
  fontSize: 11,
238
- }, children: "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: "Glyphs" }), jsx("button", { onClick: () => setTab('metrics'), style: tabBtnStyle(tab === 'metrics'), children: "Metrics" }), jsx("button", { onClick: () => setTab('kerning'), style: tabBtnStyle(tab === 'kerning'), children: "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: {
239
241
  fontSize: 11,
240
242
  marginBottom: 4,
241
243
  color: 'var(--nice-text-secondary, #868e96)',
242
- }, children: [editor.glyphs.length, " glyphs"] }), jsx("div", { style: {
244
+ }, children: t('fontEditor.glyphs2', '{glyphs} glyphs').replace('{glyphs}', String(editor.glyphs.length)) }), jsx("div", { style: {
243
245
  display: 'grid',
244
246
  gridTemplateColumns: 'repeat(auto-fill, 36px)',
245
247
  gap: 2,
@@ -270,7 +272,7 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
270
272
  border: g.path
271
273
  ? '1px solid var(--nice-border, #373a40)'
272
274
  : '1px solid transparent',
273
- }, 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: "Select" }), jsx("button", { onClick: () => setDrawMode('pen'), style: btnStyle(drawMode === 'pen'), children: "Pen" }), jsx("button", { onClick: clearPath, style: btnStyle(false), children: "Clear" }), jsx("button", { onClick: () => setShowGrid((g) => !g), style: btnStyle(showGrid), children: "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: {
274
276
  background: 'var(--nice-bg, #141517)',
275
277
  borderRadius: 4,
276
278
  cursor: drawMode === 'pen' ? 'crosshair' : 'default',
@@ -278,32 +280,32 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
278
280
  Array.from({ length: 11 }, (_, i) => {
279
281
  const pos = (i / 10) * em;
280
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));
281
- }), 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: "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: "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, {
282
284
  advanceWidth: Number(e.target.value),
283
- }), style: fieldStyle }), jsx("label", { children: "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: {
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: {
284
286
  color: 'var(--nice-text-secondary, #868e96)',
285
287
  padding: 20,
286
288
  textAlign: 'center',
287
- }, children: "Select a glyph to edit" })) })] })), tab === 'metrics' && (jsxs("div", { style: { maxWidth: 400 }, children: [jsx("strong", { style: { fontSize: 12, display: 'block', marginBottom: 8 }, children: "Font Metrics" }), [
289
+ }, children: t('fontEditor.selectAGlyphToEdit', 'Select a glyph to edit') })) })] })), tab === 'metrics' && (jsxs("div", { style: { maxWidth: 400 }, children: [jsx("strong", { style: { fontSize: 12, display: 'block', marginBottom: 8 }, children: t('fontEditor.fontMetrics', 'Font Metrics') }), [
288
290
  ['unitsPerEm', 'Units per Em'],
289
291
  ['ascent', 'Ascent'],
290
292
  ['descent', 'Descent'],
291
293
  ['lineGap', 'Line Gap'],
292
294
  ['xHeight', 'x-Height'],
293
295
  ['capHeight', 'Cap Height'],
294
- ].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: "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: "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: () => {
295
297
  if (newKernLeft && newKernRight) {
296
298
  setKerningPairs((prev) => [
297
299
  ...prev,
298
300
  { left: newKernLeft, right: newKernRight, value: -50 },
299
301
  ]);
300
302
  }
301
- }, style: btnStyle(false), children: "Add Pair" })] }), jsx("div", { style: { maxHeight: 200, overflowY: 'auto' }, children: kerningPairs.length === 0 ? (jsx("div", { style: {
303
+ }, style: btnStyle(false), children: t('fontEditor.addPair', 'Add Pair') })] }), jsx("div", { style: { maxHeight: 200, overflowY: 'auto' }, children: kerningPairs.length === 0 ? (jsx("div", { style: {
302
304
  color: 'var(--nice-text-secondary, #868e96)',
303
305
  fontSize: 11,
304
306
  padding: 8,
305
307
  textAlign: 'center',
306
- }, children: "No kerning pairs defined" })) : (kerningPairs.map((kp, i) => (jsxs("div", { style: {
308
+ }, children: t('fontEditor.noKerningPairsDefined', 'No kerning pairs defined') })) : (kerningPairs.map((kp, i) => (jsxs("div", { style: {
307
309
  display: 'flex',
308
310
  gap: 8,
309
311
  alignItems: 'center',
@@ -314,7 +316,7 @@ const NiceFontEditor = ({ fontName = 'MyFont', onSave, className = '', tutorial,
314
316
  border: 'none',
315
317
  color: 'var(--nice-danger, #ff6b6b)',
316
318
  cursor: 'pointer',
317
- }, "aria-label": "Remove kerning pair", title: "Remove kerning pair", children: jsx(NiceIcon, { name: "close", size: 14 }) })] }, i)))) })] })), jsxs("div", { style: { marginTop: 8, borderTop: '1px solid var(--nice-border, #373a40)', paddingTop: 8 }, children: [jsx("div", { style: { fontSize: 11, color: 'var(--nice-text-secondary, #868e96)', marginBottom: 4 }, children: "Preview" }), jsx("input", { value: editor.previewText, onChange: (e) => editor.setPreviewText(e.target.value), style: { width: '100%', padding: '6px 8px', ...fieldStyle, fontSize: 12 } }), jsx("div", { style: {
319
+ }, "aria-label": t('fontEditor.removeKerningPair', 'Remove kerning pair'), title: t('fontEditor.removeKerningPair', 'Remove kerning pair'), children: jsx(NiceIcon, { name: "close", size: 14 }) })] }, i)))) })] })), jsxs("div", { style: { marginTop: 8, borderTop: '1px solid var(--nice-border, #373a40)', paddingTop: 8 }, children: [jsx("div", { style: { fontSize: 11, color: 'var(--nice-text-secondary, #868e96)', marginBottom: 4 }, children: t('fontEditor.preview', 'Preview') }), jsx("input", { value: editor.previewText, onChange: (e) => editor.setPreviewText(e.target.value), style: { width: '100%', padding: '6px 8px', ...fieldStyle, fontSize: 12 } }), jsx("div", { style: {
318
320
  marginTop: 8,
319
321
  padding: 16,
320
322
  background: 'var(--nice-bg, #fff)',