@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
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceTutorialButton, resolveTutorial, NiceAppShell } 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 UI_DESIGNER_TOUR = [
@@ -310,6 +311,7 @@ function updateInTree(nodes, id, updates) {
310
311
  /** Visual drag & drop UI builder that generates React code. */
311
312
  const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
312
313
  var _a, _b, _c, _d, _e;
314
+ const { t } = useNiceTranslation();
313
315
  const designer = useUIDesigner();
314
316
  const [breakpoint, setBreakpoint] = React.useState('desktop');
315
317
  const [dragType, setDragType] = React.useState(null);
@@ -393,7 +395,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
393
395
  color: 'var(--nice-text-secondary, #868e96)',
394
396
  fontSize: 11,
395
397
  gap: 4,
396
- }, children: [jsx(NiceIcon, { name: "image", size: 14 }), " Image"] })) : node.type === 'List' ? (jsx("ul", { style: { margin: 0, ...node.style }, children: ((_d = node.props.items) !== null && _d !== void 0 ? _d : []).map((item, i) => (jsx("li", { children: item }, i))) })) : node.type === 'Divider' ? (jsx("hr", { style: {
398
+ }, children: [jsx(NiceIcon, { name: "image", size: 14 }), " ", t('uiDesigner.image', 'Image')] })) : node.type === 'List' ? (jsx("ul", { style: { margin: 0, ...node.style }, children: ((_d = node.props.items) !== null && _d !== void 0 ? _d : []).map((item, i) => (jsx("li", { children: item }, i))) })) : node.type === 'Divider' ? (jsx("hr", { style: {
397
399
  margin: 0,
398
400
  border: 'none',
399
401
  borderTop: '1px solid var(--nice-border, #dee2e6)',
@@ -419,7 +421,9 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
419
421
  display: 'flex',
420
422
  alignItems: 'center',
421
423
  gap: 4,
422
- background: designer.selectedId === node.id ? 'rgba(51,154,240,0.2)' : 'transparent',
424
+ background: designer.selectedId === node.id
425
+ ? 'color-mix(in srgb, var(--nice-color-primary, #339af0) 20%, transparent)'
426
+ : 'transparent',
423
427
  borderRadius: 2,
424
428
  }, children: [(def === null || def === void 0 ? void 0 : def.icon) && jsx(NiceIcon, { name: def.icon, size: 12 }), jsxs("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: [node.type, node.props.text ? `: ${String(node.props.text).slice(0, 15)}` : ''] })] }), node.children.map((c) => renderTreeItem(c, depth + 1))] }, node.id));
425
429
  };
@@ -433,7 +437,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
433
437
  height: '100%',
434
438
  overflowY: 'auto',
435
439
  boxSizing: 'border-box',
436
- }, children: [jsxs("div", { children: [jsx("strong", { style: { fontSize: 12, display: 'block', marginBottom: 4 }, children: "Components" }), jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 2 }, children: COMPONENT_DEFS.map((def) => (jsxs("button", { draggable: true, onDragStart: () => setDragType(def.type), onDragEnd: () => setDragType(null), onClick: () => { var _a; return designer.addNode(def.type, (_a = designer.selectedId) !== null && _a !== void 0 ? _a : undefined); }, style: {
440
+ }, children: [jsxs("div", { children: [jsx("strong", { style: { fontSize: 12, display: 'block', marginBottom: 4 }, children: t('uiDesigner.components', 'Components') }), jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 2 }, children: COMPONENT_DEFS.map((def) => (jsxs("button", { draggable: true, onDragStart: () => setDragType(def.type), onDragEnd: () => setDragType(null), onClick: () => { var _a; return designer.addNode(def.type, (_a = designer.selectedId) !== null && _a !== void 0 ? _a : undefined); }, style: {
437
441
  display: 'flex',
438
442
  alignItems: 'center',
439
443
  gap: 4,
@@ -445,8 +449,8 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
445
449
  cursor: 'grab',
446
450
  textAlign: 'left',
447
451
  fontSize: 11,
448
- }, children: [jsx(NiceIcon, { name: def.icon, size: 14 }), " ", def.label] }, def.type))) })] }), jsxs("div", { children: [jsx("strong", { style: { fontSize: 12, display: 'block', marginBottom: 4 }, children: "Tree" }), jsx("div", { style: { maxHeight: 200, overflowY: 'auto' }, children: designer.tree.length === 0 ? (jsx("div", { style: { color: 'var(--nice-text-secondary, #5c5f66)', padding: 4 }, children: "Empty" })) : (designer.tree.map((n) => renderTreeItem(n))) })] })] }));
449
- const toolbarContent = (jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center', fontSize: 11, padding: '4px 8px' }, children: [jsx("button", { onClick: () => designer.undo(), disabled: !designer.canUndo, style: { ...btnStyle(false), opacity: designer.canUndo ? 1 : 0.4 }, title: "Undo (Ctrl+Z)", "aria-label": "Undo", children: jsx(NiceIcon, { name: "reply", size: 14 }) }), jsx("button", { onClick: () => designer.redo(), disabled: !designer.canRedo, style: { ...btnStyle(false), opacity: designer.canRedo ? 1 : 0.4 }, title: "Redo (Ctrl+Y)", "aria-label": "Redo", children: jsx(NiceIcon, { name: "forward", size: 14 }) }), jsx("div", { style: { width: 1, height: 16, background: 'var(--nice-border, #373a40)' } }), jsxs("button", { onClick: () => setBreakpoint('desktop'), style: btnStyle(breakpoint === 'desktop'), children: [jsx(NiceIcon, { name: "monitor", size: 14 }), " Desktop"] }), jsxs("button", { onClick: () => setBreakpoint('tablet'), style: btnStyle(breakpoint === 'tablet'), children: [jsx(NiceIcon, { name: "window", size: 14 }), " Tablet"] }), jsxs("button", { onClick: () => setBreakpoint('mobile'), style: btnStyle(breakpoint === 'mobile'), children: [jsx(NiceIcon, { name: "phone", size: 14 }), " Mobile"] }), jsx("div", { style: { flex: 1 } }), designer.selectedId && (jsxs("button", { onClick: () => designer.removeNode(designer.selectedId), style: { ...btnStyle(false), color: 'var(--nice-danger, #ff6b6b)' }, children: [jsx(NiceIcon, { name: "trash", size: 14 }), " Delete"] })), jsxs("button", { onClick: () => setShowCode((v) => !v), style: btnStyle(showCode), children: ['</>', " Code"] }), jsx("button", { onClick: () => onExport === null || onExport === void 0 ? void 0 : onExport(designer.exportCode()), style: {
452
+ }, children: [jsx(NiceIcon, { name: def.icon, size: 14 }), " ", def.label] }, def.type))) })] }), jsxs("div", { children: [jsx("strong", { style: { fontSize: 12, display: 'block', marginBottom: 4 }, children: t('uiDesigner.tree', 'Tree') }), jsx("div", { style: { maxHeight: 200, overflowY: 'auto' }, children: designer.tree.length === 0 ? (jsx("div", { style: { color: 'var(--nice-text-secondary, #5c5f66)', padding: 4 }, children: t('uiDesigner.empty', 'Empty') })) : (designer.tree.map((n) => renderTreeItem(n))) })] })] }));
453
+ const toolbarContent = (jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center', fontSize: 11, padding: '4px 8px' }, children: [jsx("button", { onClick: () => designer.undo(), disabled: !designer.canUndo, style: { ...btnStyle(false), opacity: designer.canUndo ? 1 : 0.4 }, title: t('uiDesigner.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('uiDesigner.undo', 'Undo'), children: jsx(NiceIcon, { name: "reply", size: 14 }) }), jsx("button", { onClick: () => designer.redo(), disabled: !designer.canRedo, style: { ...btnStyle(false), opacity: designer.canRedo ? 1 : 0.4 }, title: t('uiDesigner.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('uiDesigner.redo', 'Redo'), children: jsx(NiceIcon, { name: "forward", size: 14 }) }), jsx("div", { style: { width: 1, height: 16, background: 'var(--nice-border, #373a40)' } }), jsxs("button", { onClick: () => setBreakpoint('desktop'), style: btnStyle(breakpoint === 'desktop'), children: [jsx(NiceIcon, { name: "monitor", size: 14 }), " ", t('uiDesigner.desktop', 'Desktop')] }), jsxs("button", { onClick: () => setBreakpoint('tablet'), style: btnStyle(breakpoint === 'tablet'), children: [jsx(NiceIcon, { name: "window", size: 14 }), " ", t('uiDesigner.tablet', 'Tablet')] }), jsxs("button", { onClick: () => setBreakpoint('mobile'), style: btnStyle(breakpoint === 'mobile'), children: [jsx(NiceIcon, { name: "phone", size: 14 }), " ", t('uiDesigner.mobile', 'Mobile')] }), jsx("div", { style: { flex: 1 } }), designer.selectedId && (jsxs("button", { onClick: () => designer.removeNode(designer.selectedId), style: { ...btnStyle(false), color: 'var(--nice-danger, #ff6b6b)' }, children: [jsx(NiceIcon, { name: "trash", size: 14 }), " ", t('uiDesigner.delete', 'Delete')] })), jsxs("button", { onClick: () => setShowCode((v) => !v), style: btnStyle(showCode), children: ['</>', " ", t('uiDesigner.code', 'Code')] }), jsx("button", { onClick: () => onExport === null || onExport === void 0 ? void 0 : onExport(designer.exportCode()), style: {
450
454
  padding: '4px 8px',
451
455
  background: 'var(--nice-primary, #228be6)',
452
456
  color: 'var(--nice-text-on-primary, #fff)',
@@ -454,7 +458,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
454
458
  borderRadius: 3,
455
459
  cursor: 'pointer',
456
460
  fontSize: 11,
457
- }, children: "Export" })] }));
461
+ }, children: t('uiDesigner.export', 'Export') })] }));
458
462
  const mainContent = (jsx("div", { style: {
459
463
  flex: 1,
460
464
  display: 'flex',
@@ -470,7 +474,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
470
474
  margin: 0,
471
475
  overflow: 'auto',
472
476
  fontSize: 11,
473
- color: '#a6e3a1',
477
+ color: 'var(--nice-color-success, #a6e3a1)',
474
478
  fontFamily: 'monospace',
475
479
  }, children: designer.exportCode() })) : (jsx("div", { style: {
476
480
  flex: 1,
@@ -487,7 +491,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
487
491
  textAlign: 'center',
488
492
  padding: 40,
489
493
  color: 'var(--nice-text-secondary, #868e96)',
490
- }, children: "Drag or click a component to add it" })) : (designer.tree.map((n) => renderNode(n))) })) }));
494
+ }, children: t('uiDesigner.dragOrClickAComponent', 'Drag or click a component to add it') })) : (designer.tree.map((n) => renderNode(n))) })) }));
491
495
  const rightPanel = (jsxs("div", { style: {
492
496
  width: 200,
493
497
  fontSize: 11,
@@ -498,7 +502,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
498
502
  overflowY: 'auto',
499
503
  padding: 8,
500
504
  boxSizing: 'border-box',
501
- }, children: [jsx("strong", { style: { fontSize: 12 }, children: "Properties" }), selectedNode ? (jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [jsxs("div", { style: {
505
+ }, children: [jsx("strong", { style: { fontSize: 12 }, children: t('uiDesigner.properties', 'Properties') }), selectedNode ? (jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [jsxs("div", { style: {
502
506
  color: 'var(--nice-text-secondary, #868e96)',
503
507
  display: 'flex',
504
508
  alignItems: 'center',
@@ -507,13 +511,13 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
507
511
  var _a;
508
512
  const di = (_a = COMPONENT_DEFS.find((d) => d.type === selectedNode.type)) === null || _a === void 0 ? void 0 : _a.icon;
509
513
  return di ? jsx(NiceIcon, { name: di, size: 14 }) : null;
510
- })(), ' ', selectedNode.type] }), selectedNode.props.text !== undefined && (jsxs("div", { children: [jsx("label", { children: "Text" }), jsx("input", { value: String(selectedNode.props.text), onChange: (e) => designer.updateNode(selectedNode.id, { props: { text: e.target.value } }), style: fieldStyle })] })), selectedNode.props.placeholder !== undefined && (jsxs("div", { children: [jsx("label", { children: "Placeholder" }), jsx("input", { value: String(selectedNode.props.placeholder), onChange: (e) => designer.updateNode(selectedNode.id, { props: { placeholder: e.target.value } }), style: fieldStyle })] })), selectedNode.props.level !== undefined && (jsxs("div", { children: [jsx("label", { children: "Heading Level" }), jsx("select", { value: Number(selectedNode.props.level), onChange: (e) => designer.updateNode(selectedNode.id, {
514
+ })(), ' ', selectedNode.type] }), selectedNode.props.text !== undefined && (jsxs("div", { children: [jsx("label", { children: t('uiDesigner.text', 'Text') }), jsx("input", { value: String(selectedNode.props.text), onChange: (e) => designer.updateNode(selectedNode.id, { props: { text: e.target.value } }), style: fieldStyle })] })), selectedNode.props.placeholder !== undefined && (jsxs("div", { children: [jsx("label", { children: t('uiDesigner.placeholder', 'Placeholder') }), jsx("input", { value: String(selectedNode.props.placeholder), onChange: (e) => designer.updateNode(selectedNode.id, { props: { placeholder: e.target.value } }), style: fieldStyle })] })), selectedNode.props.level !== undefined && (jsxs("div", { children: [jsx("label", { children: t('uiDesigner.headingLevel', 'Heading Level') }), jsx("select", { value: Number(selectedNode.props.level), onChange: (e) => designer.updateNode(selectedNode.id, {
511
515
  props: { level: Number(e.target.value) },
512
- }), style: fieldStyle, children: [1, 2, 3, 4, 5, 6].map((v) => (jsxs("option", { value: v, children: ["H", v] }, v))) })] })), selectedNode.props.variant !== undefined && (jsxs("div", { children: [jsx("label", { children: "Variant" }), jsx("select", { value: String(selectedNode.props.variant), onChange: (e) => designer.updateNode(selectedNode.id, { props: { variant: e.target.value } }), style: fieldStyle, children: ['primary', 'secondary', 'outline', 'ghost'].map((v) => (jsx("option", { value: v, children: v }, v))) })] })), selectedNode.props.src !== undefined && (jsxs("div", { children: [jsx("label", { children: "Image URL" }), jsx("input", { value: String(selectedNode.props.src), onChange: (e) => designer.updateNode(selectedNode.id, { props: { src: e.target.value } }), style: fieldStyle })] })), selectedNode.props.alt !== undefined && (jsxs("div", { children: [jsx("label", { children: "Alt Text" }), jsx("input", { value: String(selectedNode.props.alt), onChange: (e) => designer.updateNode(selectedNode.id, { props: { alt: e.target.value } }), style: fieldStyle })] })), jsx("hr", { style: {
516
+ }), style: fieldStyle, children: [1, 2, 3, 4, 5, 6].map((v) => (jsxs("option", { value: v, children: ["H", v] }, v))) })] })), selectedNode.props.variant !== undefined && (jsxs("div", { children: [jsx("label", { children: t('uiDesigner.variant', 'Variant') }), jsx("select", { value: String(selectedNode.props.variant), onChange: (e) => designer.updateNode(selectedNode.id, { props: { variant: e.target.value } }), style: fieldStyle, children: ['primary', 'secondary', 'outline', 'ghost'].map((v) => (jsx("option", { value: v, children: v }, v))) })] })), selectedNode.props.src !== undefined && (jsxs("div", { children: [jsx("label", { children: t('uiDesigner.imageUrl', 'Image URL') }), jsx("input", { value: String(selectedNode.props.src), onChange: (e) => designer.updateNode(selectedNode.id, { props: { src: e.target.value } }), style: fieldStyle })] })), selectedNode.props.alt !== undefined && (jsxs("div", { children: [jsx("label", { children: t('uiDesigner.altText', 'Alt Text') }), jsx("input", { value: String(selectedNode.props.alt), onChange: (e) => designer.updateNode(selectedNode.id, { props: { alt: e.target.value } }), style: fieldStyle })] })), jsx("hr", { style: {
513
517
  border: 'none',
514
518
  borderTop: '1px solid var(--nice-border, #373a40)',
515
519
  margin: '4px 0',
516
- } }), jsx("strong", { style: { color: 'var(--nice-text-secondary, #868e96)' }, children: "Style" }), ['padding', 'margin', 'gap', 'borderRadius'].map((prop) => (jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [jsx("label", { style: { minWidth: 60 }, children: prop }), jsx("input", { type: "number", value: typeof selectedNode.style[prop] === 'number'
520
+ } }), jsx("strong", { style: { color: 'var(--nice-text-secondary, #868e96)' }, children: t('uiDesigner.style', 'Style') }), ['padding', 'margin', 'gap', 'borderRadius'].map((prop) => (jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [jsx("label", { style: { minWidth: 60 }, children: prop }), jsx("input", { type: "number", value: typeof selectedNode.style[prop] === 'number'
517
521
  ? selectedNode.style[prop]
518
522
  : 0, onChange: (e) => designer.updateNode(selectedNode.id, {
519
523
  style: { [prop]: Number(e.target.value) },
@@ -543,7 +547,7 @@ const NiceUIDesigner = ({ onChange, onExport, className = '', tutorial, }) => {
543
547
  color: 'var(--nice-text-secondary, #5c5f66)',
544
548
  padding: 8,
545
549
  textAlign: 'center',
546
- }, children: "Select an element" }))] }));
550
+ }, children: t('uiDesigner.selectAnElement', 'Select an element') }))] }));
547
551
  return (jsxs("div", { className: `nice-ui-designer ${className}`, style: { position: 'relative', height: '100%' }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UI_DESIGNER_TOUR) }) }), jsx(NiceAppShell, { flush: true, toolbar: toolbarContent, sidebar: leftPanel, rightSidebar: rightPanel, children: mainContent })] }));
548
552
  };
549
553
 
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui-core';
3
3
  import React, { useState, useCallback } from 'react';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
 
5
6
  /* ── Default config ── */
6
7
  const DEFAULT_CONFIG = {
@@ -417,6 +418,7 @@ function useSvgToCode() {
417
418
  }
418
419
  /* ── Component ── */
419
420
  const NiceSvgToCode = ({ initialSvg = '', onConvert, className = '', }) => {
421
+ const { t } = useNiceTranslation();
420
422
  const { svgInput, setSvgInput, config, updateConfig, convert, result } = useSvgToCode();
421
423
  // Initialize with provided SVG
422
424
  React.useEffect(() => {
@@ -435,21 +437,21 @@ const NiceSvgToCode = ({ initialSvg = '', onConvert, className = '', }) => {
435
437
  navigator.clipboard.writeText(result.code);
436
438
  }
437
439
  };
438
- return (jsxs("div", { className: `nice-svg-to-code ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " SVG \u2192 Code"] }), jsx("textarea", { value: svgInput, onChange: (e) => setSvgInput(e.target.value), placeholder: "Paste SVG markup here\u2026", rows: 6, style: {
440
+ return (jsxs("div", { className: `nice-svg-to-code ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " ", t('svgToCode.svgCode', 'SVG → Code')] }), jsx("textarea", { value: svgInput, onChange: (e) => setSvgInput(e.target.value), placeholder: t('svgToCode.pasteSvgMarkupHere', 'Paste SVG markup here…'), rows: 6, style: {
439
441
  width: '100%',
440
442
  fontFamily: 'monospace',
441
443
  fontSize: 11,
442
444
  padding: 6,
443
445
  borderRadius: 4,
444
446
  border: '1px solid var(--nice-border, #e2e8f0)',
445
- } }), jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Framework:", jsxs("select", { value: config.framework, onChange: (e) => updateConfig({ framework: e.target.value }), style: { width: '100%' }, children: [jsx("option", { value: "react", children: "React (JSX/TSX)" }), jsx("option", { value: "vue", children: "Vue (SFC)" }), jsx("option", { value: "web-component", children: "Web Component" }), jsx("option", { value: "typescript", children: "TypeScript (utility)" })] })] }), jsxs("label", { children: ["Component name:", 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' } })] }), jsxs("div", { style: { display: 'flex', gap: 8, flexWrap: 'wrap', fontSize: 11 }, children: [jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.typescript, onChange: (e) => updateConfig({ typescript: e.target.checked }) }), ' ', "TypeScript"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.optimize, onChange: (e) => updateConfig({ optimize: e.target.checked }) }), ' ', "Optimize"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.sizeProps, onChange: (e) => updateConfig({ sizeProps: e.target.checked }) }), ' ', "Size props"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.useCurrentColor, onChange: (e) => updateConfig({ useCurrentColor: e.target.checked }) }), ' ', "currentColor"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.memo, onChange: (e) => updateConfig({ memo: e.target.checked }) }), ' ', "memo"] })] })] }), jsxs("button", { type: "button", onClick: handleConvert, disabled: !svgInput.trim(), style: {
447
+ } }), jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: [t('svgToCode.framework', 'Framework:'), jsxs("select", { value: config.framework, onChange: (e) => updateConfig({ framework: e.target.value }), style: { width: '100%' }, children: [jsx("option", { value: "react", children: "React (JSX/TSX)" }), jsx("option", { value: "vue", children: "Vue (SFC)" }), jsx("option", { value: "web-component", children: t('svgToCode.webComponent', 'Web Component') }), jsx("option", { value: "typescript", children: t('svgToCode.typescriptUtility', 'TypeScript (utility)') })] })] }), jsxs("label", { children: [t('svgToCode.componentName', 'Component name:'), 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' } })] }), jsxs("div", { style: { display: 'flex', gap: 8, flexWrap: 'wrap', fontSize: 11 }, children: [jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.typescript, onChange: (e) => updateConfig({ typescript: e.target.checked }) }), ' ', t('svgToCode.typescript', 'TypeScript')] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.optimize, onChange: (e) => updateConfig({ optimize: e.target.checked }) }), ' ', t('svgToCode.optimize', 'Optimize')] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.sizeProps, onChange: (e) => updateConfig({ sizeProps: e.target.checked }) }), ' ', t('svgToCode.sizeProps', 'Size props')] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.useCurrentColor, onChange: (e) => updateConfig({ useCurrentColor: e.target.checked }) }), ' ', "currentColor"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.memo, onChange: (e) => updateConfig({ memo: e.target.checked }) }), ' ', "memo"] })] })] }), jsxs("button", { type: "button", onClick: handleConvert, disabled: !svgInput.trim(), style: {
446
448
  background: 'var(--nice-color-accent, #6366f1)',
447
449
  color: 'var(--nice-text-on-primary, #fff)',
448
450
  border: 'none',
449
451
  borderRadius: 4,
450
452
  padding: '8px 12px',
451
453
  cursor: 'pointer',
452
- }, children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " Convert to", ' ', config.framework.charAt(0).toUpperCase() + config.framework.slice(1)] }), result && (jsxs("div", { children: [jsxs("div", { style: {
454
+ }, children: [jsx(NiceIcon, { name: "refresh", size: 14 }), ' ', t('svgToCode.convertTo', 'Convert to {framework}').replace('{framework}', config.framework.charAt(0).toUpperCase() + config.framework.slice(1))] }), result && (jsxs("div", { children: [jsxs("div", { style: {
453
455
  display: 'flex',
454
456
  justifyContent: 'space-between',
455
457
  alignItems: 'center',
@@ -461,7 +463,7 @@ const NiceSvgToCode = ({ initialSvg = '', onConvert, className = '', }) => {
461
463
  borderRadius: 3,
462
464
  border: '1px solid var(--nice-border, #e2e8f0)',
463
465
  background: 'transparent',
464
- }, children: [jsx(NiceIcon, { name: "clipboard", size: 12 }), " Copy"] })] }), jsx("pre", { style: {
466
+ }, children: [jsx(NiceIcon, { name: "clipboard", size: 12 }), " ", t('svgToCode.copy', 'Copy')] })] }), jsx("pre", { style: {
465
467
  background: 'var(--nice-code-bg, #1e293b)',
466
468
  color: 'var(--nice-code-color, #e2e8f0)',
467
469
  padding: 8,
@@ -470,7 +472,7 @@ const NiceSvgToCode = ({ initialSvg = '', onConvert, className = '', }) => {
470
472
  overflow: 'auto',
471
473
  maxHeight: 300,
472
474
  whiteSpace: 'pre-wrap',
473
- }, children: result.code }), result.extractedColors.length > 0 && (jsxs("div", { style: { fontSize: 11, marginTop: 4 }, children: ["Kolory:", ' ', result.extractedColors.map((c) => (jsx("span", { style: {
475
+ }, children: result.code }), result.extractedColors.length > 0 && (jsxs("div", { style: { fontSize: 11, marginTop: 4 }, children: [t('svgToCode.colors', 'Colors:'), ' ', result.extractedColors.map((c) => (jsx("span", { style: {
474
476
  display: 'inline-block',
475
477
  width: 12,
476
478
  height: 12,
@@ -2,6 +2,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceButton, NiceAppShell, NiceModal, NiceTextArea } from '@nice2dev/ui';
3
3
  import { NiceIcon } from '@nice2dev/ui-core';
4
4
  import { useRef, useState, useCallback, useEffect } from 'react';
5
+ import { EditorAskDialog } from '../core/editorAsk.js';
5
6
  import { useNiceTranslation } from '../core/i18n.js';
6
7
  import { useVectorEditor } from './useVectorEditor.js';
7
8
  import { frameToTransform, sampleAnimation } from './vectorAnimation.js';
@@ -63,7 +64,7 @@ const NiceVectorEditor = ({ width: artW = 512, height: artH = 512, className, on
63
64
  }, [initialIcon, artW, artH, api]);
64
65
  const { shapes, selId, tool, zoom, px, py, dlgSvg, svgText, gridSize, boxRef, fInp, animations, previewTime, isPlaying, onDown, onMove, onUp, onWheel, onDblClick, openFile, switchTool, importSVGString, setDlgSvg, setSvgText, } = api;
65
66
  const toolRail = (jsx("div", { className: styles.toolbar, children: TOOLS.map((t) => (jsx(NiceButton, { className: `${styles.toolBtn} ${tool === t.id ? styles.toolBtnActive : ''}`, onClick: () => switchTool(t.id), "aria-label": `${t.label} (${t.key})`, variant: tool === t.id ? 'primary' : 'ghost', size: "sm", children: jsx(NiceIcon, { name: t.icon, size: "sm" }) }, t.id))) }));
66
- return (jsxs(Fragment, { children: [jsx("input", { ref: fInp, type: "file", accept: ".svg", style: { display: 'none' }, onChange: (e) => {
67
+ return (jsxs(Fragment, { children: [jsx(EditorAskDialog, { ask: api.pendingAsk }), jsx("input", { ref: fInp, type: "file", accept: ".svg", style: { display: 'none' }, onChange: (e) => {
67
68
  var _a;
68
69
  const f = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
69
70
  if (f) {
@@ -109,7 +110,7 @@ const NiceVectorEditor = ({ width: artW = 512, height: artH = 512, className, on
109
110
  }
110
111
  const node = (jsx(ShapeRenderer, { shape: s, selected: s.id === selId, zoom: zoom }, s.id));
111
112
  return previewTr ? (jsx("g", { transform: previewTr, children: node }, s.id)) : (node);
112
- })] }) }) }), jsxs(NiceModal, { open: dlgSvg, onClose: () => setDlgSvg(false), title: "Paste SVG", children: [jsx("label", { children: "Paste SVG source code below:" }), jsx(NiceTextArea, { value: svgText, onChange: (val) => setSvgText(val), placeholder: '<svg xmlns="http://www.w3.org/2000/svg" ...>' }), jsxs("div", { className: styles.dialogActions, children: [jsx(NiceButton, { variant: "ghost", onClick: () => setDlgSvg(false), children: "Cancel" }), jsx(NiceButton, { variant: "primary", onClick: () => importSVGString(svgText), children: "Import" })] })] }), jsxs(NiceModal, { open: snippetOpen, onClose: () => setSnippetOpen(false), title: t('vectorEditor.snippet.title', 'Icon snippet copied to clipboard'), children: [jsxs("p", { style: { marginTop: 0 }, children: [t('vectorEditor.snippet.pasteHint', 'Paste into'), ' ', jsxs("code", { children: ["packages/icons/src/icons/", iconCategory ? `${iconCategory.toLowerCase()}` : '<category>', ".tsx"] }), ":"] }), jsx(NiceTextArea, { value: snippetText, onChange: (val) => setSnippetText(val), rows: 16 }), jsxs("div", { className: styles.dialogActions, children: [jsx(NiceButton, { variant: "primary", onClick: () => {
113
+ })] }) }) }), jsxs(NiceModal, { open: dlgSvg, onClose: () => setDlgSvg(false), title: t('vectorEditor.pasteSvg', 'Paste SVG'), children: [jsx("label", { children: t('vectorEditor.pasteSvgSourceCodeBelow', 'Paste SVG source code below:') }), jsx(NiceTextArea, { value: svgText, onChange: (val) => setSvgText(val), placeholder: t('vectorEditor.svgXmlnsHttpWwwW3', '<svg xmlns="http://www.w3.org/2000/svg" ...>') }), jsxs("div", { className: styles.dialogActions, children: [jsx(NiceButton, { variant: "ghost", onClick: () => setDlgSvg(false), children: t('vectorEditor.cancel', 'Cancel') }), jsx(NiceButton, { variant: "primary", onClick: () => importSVGString(svgText), children: t('vectorEditor.import', 'Import') })] })] }), jsxs(NiceModal, { open: snippetOpen, onClose: () => setSnippetOpen(false), title: t('vectorEditor.snippet.title', 'Icon snippet copied to clipboard'), children: [jsxs("p", { style: { marginTop: 0 }, children: [t('vectorEditor.snippet.pasteHint', 'Paste into'), ' ', jsxs("code", { children: ["packages/icons/src/icons/", iconCategory ? `${iconCategory.toLowerCase()}` : '<category>', ".tsx"] }), ":"] }), jsx(NiceTextArea, { value: snippetText, onChange: (val) => setSnippetText(val), rows: 16 }), jsxs("div", { className: styles.dialogActions, children: [jsx(NiceButton, { variant: "primary", onClick: () => {
113
114
  if (typeof navigator !== 'undefined' && navigator.clipboard) {
114
115
  navigator.clipboard.writeText(snippetText).catch(() => { });
115
116
  }
@@ -262,7 +262,13 @@ const VectorEditorMenuBar$1 = ({ api, hasOnSaveToLibrary, onExportIconSnippet })
262
262
  { value: '', label: t('vectorEditor.menu.secondShape', '2nd Shape') },
263
263
  ...shapes
264
264
  .filter((s) => s.id !== selId)
265
- .map((s, i) => { var _a; return ({ value: s.id, label: (_a = s.name) !== null && _a !== void 0 ? _a : `Shape ${i + 1}` }); }),
265
+ .map((s, i) => {
266
+ var _a;
267
+ return ({
268
+ value: s.id,
269
+ label: (_a = s.name) !== null && _a !== void 0 ? _a : t('vectorEditor.shape', 'Shape {i}').replace('{i}', String(i + 1)),
270
+ });
271
+ }),
266
272
  ], style: { minWidth: 100 } })), jsxs("label", { className: styles.menuLabel, children: [jsx(NiceCheckbox, { checked: snapGrid, onChange: () => setSnapGrid((v) => !v) }), t('vectorEditor.menu.snap', 'Snap')] }), onExportIconSnippet && (jsxs(Fragment, { children: [jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "primary", size: "sm", onClick: onExportIconSnippet, children: [jsx(NiceIcon, { name: "clipboard", size: 14 }), ' ', t('vectorEditor.menu.exportIcon', 'Export Icon')] })] }))] }));
267
273
  };
268
274
  var VectorEditorMenuBar = React.memo(VectorEditorMenuBar$1);
@@ -44,7 +44,7 @@ const VectorEditorRightPanel$1 = ({ api, library, artW = 512, artH = 512 }) => {
44
44
  border: (selectedGradient === null || selectedGradient === void 0 ? void 0 : selectedGradient.id) === grad.id
45
45
  ? '2px solid var(--nice-bg, #FFF)'
46
46
  : '1px solid var(--nice-border-color, #444)',
47
- }, onClick: () => applyGradient(grad), title: `${grad.type} gradient` }, i))) }), jsx("div", { className: styles.propRow, children: jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", style: { width: '100%', fontSize: 10 }, onClick: () => setShowGradientEditor(!showGradientEditor), children: [jsx(NiceIcon, { name: showGradientEditor ? 'chevron-down' : 'chevron-right', size: 14 }), ' ', showGradientEditor
47
+ }, onClick: () => applyGradient(grad), title: t('vectorEditor.gradient', '{type} gradient').replace('{type}', String(grad.type)) }, i))) }), jsx("div", { className: styles.propRow, children: jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", style: { width: '100%', fontSize: 10 }, onClick: () => setShowGradientEditor(!showGradientEditor), children: [jsx(NiceIcon, { name: showGradientEditor ? 'chevron-down' : 'chevron-right', size: 14 }), ' ', showGradientEditor
48
48
  ? t('vectorEditor.properties.hideEditor', 'Hide Editor')
49
49
  : t('vectorEditor.properties.editGradient', 'Edit Gradient')] }) }), showGradientEditor && selectedGradient && (jsxs("div", { style: {
50
50
  padding: 4,
@@ -6,7 +6,9 @@ const ShapeRenderer = ({ shape: s, selected, zoom }) => {
6
6
  if (s.hidden) {
7
7
  return null;
8
8
  }
9
- const selStroke = selected ? 'rgba(85,102,204,0.6)' : undefined;
9
+ const selStroke = selected
10
+ ? 'color-mix(in srgb, var(--nice-color-primary, #5566cc) 60%, transparent)'
11
+ : undefined;
10
12
  const gProps = {};
11
13
  if (s.opacity < 1) {
12
14
  gProps.opacity = s.opacity;
@@ -1,4 +1,5 @@
1
1
  import { useState, useRef, useMemo, useCallback, useEffect } from 'react';
2
+ import { useEditorAsk } from '../core/editorAsk.js';
2
3
  import { booleanOperation, flattenToOutline } from './vectorBooleanOps.js';
3
4
  import { buildSVGString, exportSVGFile, exportPNGFile, exportJPGFile, exportWebPFile, exportBMPFile } from './vectorEditorExport.js';
4
5
  import { parseSVGToShapes } from './vectorEditorImport.js';
@@ -10,10 +11,13 @@ import { cloneShapes, sid, snapToGrid } from './vectorEditorTypes.js';
10
11
  * so the component file is pure layout.
11
12
  */
12
13
  function useVectorEditor({ artW, artH, onSaveToLibrary }) {
14
+ // the library's dialogs for the text tool and the library name (not window.prompt)
15
+ const { pendingAsk, ask } = useEditorAsk();
13
16
  /* ── state ── */
14
17
  const [shapes, setShapes] = useState([]);
15
18
  const [selId, setSelId] = useState(null);
16
19
  const [tool, setTool] = useState('pointer');
20
+ // audit-allow: literal-color — the first fill of a new shape - document data
17
21
  const [fillCol, setFillCol] = useState('#5566cc');
18
22
  const [strokeCol, setStrokeCol] = useState('#000000');
19
23
  const [strokeW, setStrokeW] = useState(2);
@@ -176,28 +180,30 @@ function useVectorEditor({ artW, artH, onSaveToLibrary }) {
176
180
  return;
177
181
  }
178
182
  if (tool === 'text') {
179
- const txt = prompt('Enter text:', 'Hello');
180
- if (!txt) {
181
- return;
182
- }
183
- const ns = {
184
- id: sid(),
185
- type: 'text',
186
- x: p.x,
187
- y: p.y,
188
- w: txt.length * fontSize * 0.6,
189
- h: fontSize,
190
- ...defaultBase,
191
- stroke: 'none',
192
- strokeWidth: 0,
193
- text: txt,
194
- fontSize,
195
- fontFamily,
196
- };
197
- const next = [...shapes, ns];
198
- snap(next);
199
- setShapes(next);
200
- setSelId(ns.id);
183
+ // the text comes from a dialog: the shape is added once it is entered
184
+ void ask('prompt', 'Enter text:', 'Hello').then((txt) => {
185
+ if (!txt) {
186
+ return;
187
+ }
188
+ const ns = {
189
+ id: sid(),
190
+ type: 'text',
191
+ x: p.x,
192
+ y: p.y,
193
+ w: txt.length * fontSize * 0.6,
194
+ h: fontSize,
195
+ ...defaultBase,
196
+ stroke: 'none',
197
+ strokeWidth: 0,
198
+ text: txt,
199
+ fontSize,
200
+ fontFamily,
201
+ };
202
+ const next = [...shapes, ns];
203
+ snap(next);
204
+ setShapes(next);
205
+ setSelId(ns.id);
206
+ });
201
207
  return;
202
208
  }
203
209
  if (tool === 'polygon') {
@@ -283,6 +289,7 @@ function useVectorEditor({ artW, artH, onSaveToLibrary }) {
283
289
  starPoints2,
284
290
  starInnerRatio,
285
291
  cornerRadius,
292
+ ask,
286
293
  ]);
287
294
  const onMove = useCallback((e) => {
288
295
  if (panning.current) {
@@ -697,15 +704,15 @@ function useVectorEditor({ artW, artH, onSaveToLibrary }) {
697
704
  const blob = new Blob([svg], { type: 'image/svg+xml' });
698
705
  const reader = new FileReader();
699
706
  reader.onload = () => {
700
- const defaultName = 'drawing.svg';
701
- const name = window.prompt('Save to library as:', defaultName);
702
- if (!name || !name.trim()) {
703
- return;
704
- }
705
- onSaveToLibrary(reader.result, name.trim(), 'image/svg+xml');
707
+ const dataUrl = reader.result;
708
+ void ask('prompt', 'Save to library as:', 'drawing.svg').then((name) => {
709
+ if (name && name.trim()) {
710
+ onSaveToLibrary(dataUrl, name.trim(), 'image/svg+xml');
711
+ }
712
+ });
706
713
  };
707
714
  reader.readAsDataURL(blob);
708
- }, [shapes, artW, artH, onSaveToLibrary]);
715
+ }, [shapes, artW, artH, onSaveToLibrary, ask]);
709
716
  /* ── export shortcuts ── */
710
717
  const exportSVG = useCallback(() => exportSVGFile(shapes, artW, artH, animations), [shapes, artW, artH, animations]);
711
718
  const exportPNG = useCallback(() => exportPNGFile(shapes, artW, artH), [shapes, artW, artH]);
@@ -791,6 +798,8 @@ function useVectorEditor({ artW, artH, onSaveToLibrary }) {
791
798
  /* ── return ── */
792
799
  return {
793
800
  // state
801
+ /** The question the editor waits on; render it with `EditorAskDialog`. */
802
+ pendingAsk,
794
803
  shapes,
795
804
  selId,
796
805
  tool,
@@ -16,6 +16,7 @@ function parseSVGToShapes(raw) {
16
16
  const elems = doc.querySelectorAll('rect, ellipse, circle, line, polyline, polygon, text, path');
17
17
  elems.forEach((el) => {
18
18
  const tag = el.tagName.toLowerCase();
19
+ // audit-allow: literal-color — an imported shape without a fill gets a visible default - document data
19
20
  const fill = el.getAttribute('fill') || (tag === 'line' ? 'none' : '#cccccc');
20
21
  const stroke = el.getAttribute('stroke') || 'none';
21
22
  const sw = parseFloat(el.getAttribute('stroke-width') || '1');
@@ -37,6 +37,7 @@ function iconComponentToShapes(Icon, options) {
37
37
  // Force a generous size and visible colors so the parser sees concrete strokes/fills.
38
38
  const renderProps = {
39
39
  size: 'lg',
40
+ // audit-allow: literal-color +2 — an imported icon's default colours - document data
40
41
  primaryColor: '#5566cc',
41
42
  secondaryColor: '#a29bfe',
42
43
  strokeWidth: 2,
@@ -16,6 +16,5 @@ export interface RangeInputProps extends Omit<React.InputHTMLAttributes<HTMLInpu
16
16
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
17
17
  }
18
18
  export declare const RangeInput: React.FC<RangeInputProps>;
19
- export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
20
- }
19
+ export type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
21
20
  export declare const TextArea: React.FC<TextAreaProps>;
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ /** A question an editor hook waits on; {@link EditorAskDialog} shows it. */
3
+ export interface EditorAsk {
4
+ /** `prompt` asks for a value, `confirm` for a yes / no. */
5
+ kind: 'prompt' | 'confirm';
6
+ /** The prompt's title, or the question to confirm. */
7
+ text: string;
8
+ /** Initial value of a prompt. */
9
+ defaultValue?: string;
10
+ /** Settles it: the entered value (`''` for an accepted confirm), or `null` when cancelled. */
11
+ answer: (value: string | null) => void;
12
+ }
13
+ /**
14
+ * The library's dialogs in place of `window.prompt` / `window.confirm` for the editor hooks,
15
+ * which hold state but render nothing: `ask` resolves with the answer (`null` when cancelled)
16
+ * and `pendingAsk` is the open question, for the editor component to pass to
17
+ * {@link EditorAskDialog}. One question at a time: a new one settles the open one as cancelled.
18
+ */
19
+ export declare function useEditorAsk(): {
20
+ pendingAsk: EditorAsk | null;
21
+ ask: (kind: EditorAsk["kind"], text: string, defaultValue?: string) => Promise<string | null>;
22
+ };
23
+ /** Shows the question {@link useEditorAsk} is waiting on, if any. */
24
+ export declare const EditorAskDialog: React.FC<{
25
+ ask: EditorAsk | null;
26
+ }>;
@@ -1,26 +1,10 @@
1
- /**
2
- * i18n — Lightweight translation context for @nice2dev/ui-graphics.
3
- *
4
- * Drop-in replacement for react-i18next's useTranslation, requires NO
5
- * external dependencies. Consumers can optionally wrap with NiceI18nProvider
6
- * to supply their own translation function. Falls back to the default
7
- * (English key strings) when no provider is present.
8
- *
9
- * Usage in components:
10
- * const { t } = useNiceTranslation();
11
- * <button>{t('actions.save', 'Save')}</button>
12
- *
13
- * Usage in apps:
14
- * <NiceI18nProvider t={(key, fallback) => myI18n(key) ?? fallback}>
15
- * <App />
16
- * </NiceI18nProvider>
17
- */
18
1
  import React from 'react';
19
2
  export type NiceTranslateFn = (key: string, defaultValue: string) => string;
20
3
  export interface NiceI18nProviderProps {
21
4
  t: NiceTranslateFn;
22
5
  children: React.ReactNode;
23
6
  }
7
+ /** Overrides the app's global translator for the graphics controls below it. */
24
8
  export declare const NiceI18nProvider: React.FC<NiceI18nProviderProps>;
25
9
  export declare function useNiceTranslation(): {
26
10
  t: NiceTranslateFn;
@@ -2,6 +2,7 @@
2
2
  * usePixelEditor — custom hook encapsulating ALL state, refs, callbacks,
3
3
  * side-effects and memos for the PixelEditor component.
4
4
  */
5
+ import { type EditorAsk } from '../core/editorAsk';
5
6
  import { type Tool, type Layer, type BlendMode, type Selection } from './pixelEditorTypes';
6
7
  import { type BrushConfig, type SymmetryMode } from './pixelEditorUtils';
7
8
  export interface UsePixelEditorProps {
@@ -81,6 +82,8 @@ export interface PixelEditorAPI {
81
82
  exportSVG: () => void;
82
83
  exportSheet: () => void;
83
84
  saveToLibrary: () => void;
85
+ /** The question the editor waits on (library name); render it with `EditorAskDialog`. */
86
+ pendingAsk: EditorAsk | null;
84
87
  addLayer: () => void;
85
88
  rmLayer: () => void;
86
89
  toggleVis: (i: number) => void;
@@ -11,6 +11,8 @@ export interface UseVectorEditorOptions {
11
11
  onSaveToLibrary?: (dataUrl: string, name: string, mimeType: string) => void;
12
12
  }
13
13
  export declare function useVectorEditor({ artW, artH, onSaveToLibrary }: UseVectorEditorOptions): {
14
+ /** The question the editor waits on; render it with `EditorAskDialog`. */
15
+ readonly pendingAsk: import("../core/editorAsk").EditorAsk | null;
14
16
  readonly shapes: VShape[];
15
17
  readonly selId: string | null;
16
18
  readonly tool: VTool;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nice2dev/ui-graphics",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Nice2Dev Graphics Editors — Pixel, Vector, Photo and Animation editors for React",
5
5
  "author": "NiceToDev <contact@nicetodev.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -64,15 +64,15 @@
64
64
  "nicetodev"
65
65
  ],
66
66
  "peerDependencies": {
67
- "@nice2dev/ui-core": "^1.0.20",
68
- "@nice2dev/ui": "^1.0.20",
67
+ "@nice2dev/ui-core": "^1.2.0",
68
+ "@nice2dev/ui": "^1.2.0",
69
69
  "framer-motion": ">=10.0.0",
70
70
  "react": ">=17.0.0",
71
71
  "react-dom": ">=17.0.0"
72
72
  },
73
73
  "devDependencies": {
74
- "@nice2dev/ui": "^1.2.0",
75
- "@nice2dev/ui-core": "^1.2.0",
74
+ "@nice2dev/ui": "^1.2.2",
75
+ "@nice2dev/ui-core": "^1.2.2",
76
76
  "@rollup/plugin-commonjs": "^29.0.3",
77
77
  "@rollup/plugin-node-resolve": "^16.0.3",
78
78
  "@rollup/plugin-typescript": "^12.3.0",