@sequent-org/moodboard 1.4.56 → 1.4.58

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/core/commands/PasteObjectCommand.js +7 -1
  3. package/src/core/commands/UpdateContentCommand.js +95 -0
  4. package/src/core/flows/ObjectLifecycleFlow.js +10 -1
  5. package/src/initNoBundler.js +28 -4
  6. package/src/services/text/TextBoxMetrics.js +133 -0
  7. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +98 -7
  8. package/src/tools/object-tools/selection/SelectInputRouter.js +56 -3
  9. package/src/tools/object-tools/selection/TextEditorCaretService.js +113 -0
  10. package/src/tools/object-tools/selection/TextEditorDomFactory.js +37 -21
  11. package/src/tools/object-tools/selection/TextEditorInteractionController.js +111 -12
  12. package/src/tools/object-tools/selection/TextEditorPositioningService.js +19 -1
  13. package/src/tools/object-tools/selection/TextEditorSyncService.js +2 -1
  14. package/src/tools/object-tools/selection/TextInlineEditorController.js +70 -45
  15. package/src/tools/object-tools/selection/TransformInteractionController.js +7 -1
  16. package/src/ui/FramePropertiesPanel.js +7 -0
  17. package/src/ui/HtmlTextLayer.js +174 -64
  18. package/src/ui/ImagePropertiesPanel.js +7 -0
  19. package/src/ui/TextPropertiesPanel.js +305 -2
  20. package/src/ui/handles/HandlesDomRenderer.js +2 -0
  21. package/src/ui/handles/HandlesInteractionController.js +40 -0
  22. package/src/ui/styles/panels.css +240 -19
  23. package/src/ui/styles/workspace.css +94 -7
  24. package/src/ui/text-properties/TextFormatControls.js +132 -21
  25. package/src/ui/text-properties/TextLinkControl.js +255 -0
  26. package/src/ui/text-properties/TextLockMoreControls.js +173 -0
  27. package/src/ui/text-properties/TextPropertiesPanelBindings.js +45 -0
  28. package/src/ui/text-properties/TextPropertiesPanelMapper.js +52 -0
  29. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +263 -26
  30. package/src/ui/text-properties/TextPropertiesPanelState.js +6 -0
@@ -0,0 +1,113 @@
1
+ const propertiesToCopy = [
2
+ 'direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY',
3
+ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderStyle',
4
+ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
5
+ 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily',
6
+ 'textAlign', 'textTransform', 'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing',
7
+ 'tabSize', 'MozTabSize', 'whiteSpace', 'wordBreak', 'wordWrap'
8
+ ];
9
+
10
+ let mirrorDiv = null;
11
+
12
+ export function getCaretCoordinates(element, position) {
13
+ if (!mirrorDiv) {
14
+ mirrorDiv = document.createElement('div');
15
+ document.body.appendChild(mirrorDiv);
16
+ }
17
+
18
+ const style = mirrorDiv.style;
19
+ const computed = window.getComputedStyle(element);
20
+
21
+ style.whiteSpace = 'pre-wrap';
22
+ if (element.nodeName !== 'INPUT') {
23
+ style.wordWrap = 'break-word';
24
+ }
25
+
26
+ style.position = 'absolute';
27
+ style.top = '-9999px';
28
+ style.left = '-9999px';
29
+ style.visibility = 'hidden';
30
+
31
+ propertiesToCopy.forEach(prop => {
32
+ style[prop] = computed[prop];
33
+ });
34
+
35
+ if (window.mozInnerScreenX != null) {
36
+ if (element.scrollHeight > parseInt(computed.height)) {
37
+ style.overflowY = 'scroll';
38
+ }
39
+ } else {
40
+ style.overflow = 'hidden';
41
+ }
42
+
43
+ mirrorDiv.textContent = element.value.substring(0, position);
44
+
45
+ if (element.nodeName === 'INPUT') {
46
+ mirrorDiv.textContent = mirrorDiv.textContent.replace(/\s/g, '\u00a0');
47
+ }
48
+
49
+ const span = document.createElement('span');
50
+ span.textContent = element.value.substring(position) || '.';
51
+ mirrorDiv.appendChild(span);
52
+
53
+ const coordinates = {
54
+ top: span.offsetTop + parseInt(computed['borderTopWidth'] || 0),
55
+ left: span.offsetLeft + parseInt(computed['borderLeftWidth'] || 0),
56
+ height: parseInt(computed['lineHeight']) || span.offsetHeight
57
+ };
58
+
59
+ return coordinates;
60
+ }
61
+
62
+ export function updateCustomCaret(textarea, caretEl) {
63
+ if (!textarea || !caretEl) return;
64
+
65
+ if (document.activeElement !== textarea) {
66
+ caretEl.style.display = 'none';
67
+ return;
68
+ }
69
+
70
+ if (textarea.selectionStart !== textarea.selectionEnd) {
71
+ caretEl.style.display = 'none';
72
+ return;
73
+ }
74
+
75
+ caretEl.style.display = 'block';
76
+
77
+ const pos = textarea.selectionStart;
78
+ const coords = getCaretCoordinates(textarea, pos);
79
+
80
+ // Adjust for scroll
81
+ const top = coords.top - textarea.scrollTop;
82
+ const left = coords.left - textarea.scrollLeft;
83
+
84
+ // Calculate width based on font size to match stroke thickness
85
+ const computed = window.getComputedStyle(textarea);
86
+ const fontSize = parseFloat(computed.fontSize) || 16;
87
+ // Caveat has thicker strokes, Arial is standard. A good heuristic is ~0.06 to 0.08 of font size, min 2px.
88
+ const caretWidth = Math.max(2, Math.round(fontSize * 0.07));
89
+
90
+ // Цвет текста в textarea теперь прозрачный (видимый текст рисует backdrop-слой),
91
+ // поэтому цвет каретки берём из backdrop, иначе она станет невидимой.
92
+ let caretColor = computed.color;
93
+ if (!caretColor || caretColor === 'transparent' || caretColor === 'rgba(0, 0, 0, 0)') {
94
+ const backdrop = caretEl.parentElement
95
+ ? caretEl.parentElement.querySelector('.moodboard-text-backdrop')
96
+ : null;
97
+ const backdropColor = backdrop ? window.getComputedStyle(backdrop).color : '';
98
+ caretColor = (backdropColor && backdropColor !== 'transparent' && backdropColor !== 'rgba(0, 0, 0, 0)')
99
+ ? backdropColor
100
+ : '#111';
101
+ }
102
+
103
+ caretEl.style.top = `${top - 2}px`;
104
+ caretEl.style.left = `${left - Math.floor(caretWidth / 2)}px`;
105
+ caretEl.style.height = `${coords.height}px`;
106
+ caretEl.style.width = `${caretWidth}px`;
107
+ caretEl.style.backgroundColor = caretColor;
108
+
109
+ // Reset animation to keep it visible while typing
110
+ caretEl.style.animation = 'none';
111
+ void caretEl.offsetWidth; // trigger reflow
112
+ caretEl.style.animation = 'mb-caret-blink 1s step-end infinite';
113
+ }
@@ -1,7 +1,18 @@
1
+ import { applyEditorSizing, applyTextStyles, computeLineHeightPx } from '../../../services/text/TextBoxMetrics.js';
2
+
1
3
  export function createTextEditorWrapper() {
2
4
  const wrapper = document.createElement('div');
3
5
  wrapper.className = 'moodboard-text-editor';
4
- return wrapper;
6
+
7
+ const backdrop = document.createElement('div');
8
+ backdrop.className = 'moodboard-text-backdrop';
9
+ wrapper.appendChild(backdrop);
10
+
11
+ const caret = document.createElement('div');
12
+ caret.className = 'mb-custom-caret';
13
+ wrapper.appendChild(caret);
14
+
15
+ return { wrapper, caret, backdrop };
5
16
  }
6
17
 
7
18
  export function createTextEditorTextarea(content) {
@@ -13,27 +24,32 @@ export function createTextEditorTextarea(content) {
13
24
  }
14
25
 
15
26
  export function computeTextEditorLineHeightPx(fontSizePx) {
16
- if (fontSizePx <= 12) return Math.round(fontSizePx * 1.40);
17
- if (fontSizePx <= 18) return Math.round(fontSizePx * 1.34);
18
- if (fontSizePx <= 36) return Math.round(fontSizePx * 1.26);
19
- if (fontSizePx <= 48) return Math.round(fontSizePx * 1.24);
20
- if (fontSizePx <= 72) return Math.round(fontSizePx * 1.22);
21
- if (fontSizePx <= 96) return Math.round(fontSizePx * 1.20);
22
- return Math.round(fontSizePx * 1.18);
27
+ return computeLineHeightPx(fontSizePx);
23
28
  }
24
29
 
25
- export function applyInitialTextEditorTextareaStyles(textarea, { effectiveFontPx, lineHeightPx }) {
26
- textarea.style.fontSize = `${effectiveFontPx}px`;
27
- textarea.style.lineHeight = `${lineHeightPx}px`;
28
-
29
- const initialHeightPx = Math.max(1, lineHeightPx);
30
- textarea.style.minHeight = `${initialHeightPx}px`;
31
- textarea.style.height = `${initialHeightPx}px`;
32
- textarea.setAttribute('rows', '1');
33
- textarea.style.overflowY = 'hidden';
34
- textarea.style.whiteSpace = 'pre';
35
- textarea.style.letterSpacing = '0px';
36
- textarea.style.fontKerning = 'normal';
30
+ /**
31
+ * Применяет все текстовые параметры на textarea/backdrop через общий applyTextStyles,
32
+ * и дополнительно выставляет sizing-параметры поля ввода (height, rows, overflow, white-space).
33
+ *
34
+ * fontSizePx — отображаемый размер в px (с учётом зума);
35
+ * baseFontSizePx — базовый без зума (для выбора line-height ratio).
36
+ * Эти два значения разделены, чтобы line-height ratio вычислялся по тому же baseFontSizePx,
37
+ * что и в HtmlTextLayer — строки совпадают при любом уровне зума.
38
+ */
39
+ export function applyInitialTextEditorTextareaStyles(textarea, {
40
+ effectiveFontPx,
41
+ baseFontSizePx,
42
+ fontFamily,
43
+ properties,
44
+ lineHeightPx,
45
+ }) {
46
+ applyTextStyles(textarea, {
47
+ fontSizePx: effectiveFontPx,
48
+ baseFontSizePx: baseFontSizePx || effectiveFontPx,
49
+ fontFamily: fontFamily || textarea.style.fontFamily || '',
50
+ properties: properties || {},
51
+ });
52
+ applyEditorSizing(textarea, lineHeightPx);
37
53
  }
38
54
 
39
55
  export function measureTextEditorPlaceholderWidth(textarea, placeholder = 'Напишите что-нибудь') {
@@ -57,7 +73,7 @@ export function attachTextEditorPlaceholderStyle(textarea, { effectiveFontPx, is
57
73
 
58
74
  const styleEl = document.createElement('style');
59
75
  const placeholderOpacity = isNote ? '0.4' : '0.6';
60
- styleEl.textContent = `.${uid}::placeholder{font-size:${effectiveFontPx}px;opacity:${placeholderOpacity};line-height:${computeTextEditorLineHeightPx(effectiveFontPx)}px;white-space:nowrap;}`;
76
+ styleEl.textContent = `.${uid}::placeholder{font-size:${effectiveFontPx}px;opacity:${placeholderOpacity};line-height:${computeTextEditorLineHeightPx(effectiveFontPx)}px;white-space:nowrap;color:#111;-webkit-text-fill-color:#111;}`;
61
77
  document.head.appendChild(styleEl);
62
78
 
63
79
  return styleEl;
@@ -1,5 +1,7 @@
1
1
  import { Events } from '../../../core/events/Events.js';
2
2
  import { alignStaticTextToEditorCssPosition } from './TextEditorPositioningService.js';
3
+ import { updateCustomCaret } from './TextEditorCaretService.js';
4
+ import { buildHtmlWithRanges } from '../../../ui/HtmlTextLayer.js';
3
5
  import {
4
6
  cleanupActiveTextEditor,
5
7
  showNotePixiText,
@@ -92,7 +94,9 @@ export function createTextEditorFinalize(controller, {
92
94
  const scaleX = worldLayerRef?.scale?.x || 1;
93
95
  const viewResLocal = (controller.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
94
96
  const wPx = Math.max(1, wrapper.offsetWidth);
95
- const hPx = Math.max(1, wrapper.offsetHeight);
97
+ const _taStyle = typeof window !== 'undefined' ? window.getComputedStyle(textarea) : null;
98
+ const _taPaddingV = _taStyle ? (parseFloat(_taStyle.paddingTop) || 0) + (parseFloat(_taStyle.paddingBottom) || 0) : 0;
99
+ const hPx = Math.max(1, wrapper.offsetHeight - _taPaddingV);
96
100
  const newW = Math.max(1, Math.round(wPx * viewResLocal / scaleX));
97
101
  const newH = Math.max(1, Math.round(hPx * viewResLocal / scaleX));
98
102
  const sizeReq = { objectId, size: null };
@@ -135,14 +139,22 @@ export function createTextEditorFinalize(controller, {
135
139
  const worldLayerRef = controller.textEditor.world || (controller.app?.stage);
136
140
  const scaleX = worldLayerRef?.scale?.x || 1;
137
141
  const wPx = Math.max(1, wrapper.offsetWidth);
138
- const hPx = Math.max(1, wrapper.offsetHeight);
142
+ const _taStyle2 = typeof window !== 'undefined' ? window.getComputedStyle(textarea) : null;
143
+ const _taPaddingV2 = _taStyle2 ? (parseFloat(_taStyle2.paddingTop) || 0) + (parseFloat(_taStyle2.paddingBottom) || 0) : 0;
144
+ const hPx = Math.max(1, wrapper.offsetHeight - _taPaddingV2);
139
145
  const wWorld = Math.max(1, Math.round(wPx * viewRes / scaleX));
140
146
  const hWorld = Math.max(1, Math.round(hPx * viewRes / scaleX));
141
147
  controller.eventBus.emit(Events.UI.ToolbarAction, {
142
148
  type: objectType,
143
149
  id: objectType,
144
150
  position: { x: position.x, y: position.y },
145
- properties: { content: value, fontSize, width: wWorld, height: hWorld },
151
+ properties: {
152
+ content: value,
153
+ fontSize,
154
+ width: wWorld,
155
+ height: hWorld,
156
+ highlightColor: properties.highlightColor
157
+ },
146
158
  });
147
159
  } else {
148
160
  if (isNewCreation) {
@@ -180,6 +192,20 @@ function _isInsideToolbarUI(target) {
180
192
  return !!(target && typeof target.closest === 'function' && target.closest(UI_BLOCK_SELECTOR));
181
193
  }
182
194
 
195
+ /**
196
+ * Возвращает true, если элемент находится внутри панели свойств текста (форматирование).
197
+ */
198
+ function _isInsideTextPropertiesPanel(target) {
199
+ return !!(target && typeof target.closest === 'function' && target.closest('.text-properties-layer'));
200
+ }
201
+
202
+ /**
203
+ * Нативный color-input открывает системную палитру по mousedown; preventDefault его ломает.
204
+ */
205
+ function _isNativeColorInput(target) {
206
+ return !!(target && target.tagName === 'INPUT' && target.type === 'color');
207
+ }
208
+
183
209
  export function bindTextEditorInteractions(controller, {
184
210
  textarea,
185
211
  isNewCreation,
@@ -219,6 +245,15 @@ export function bindTextEditorInteractions(controller, {
219
245
  if (e.target === textarea) return;
220
246
  if (!_isInsideToolbarUI(e.target)) return;
221
247
 
248
+ // Клик по панели свойств текста (например, выбор цвета подсветки выделенного
249
+ // фрагмента): сохраняем фокус и выделение в textarea, но НЕ глушим сам click —
250
+ // он должен дойти до кнопки/дропдауна. Иначе blur закроет редактор, и выделение
251
+ // пропадёт ещё до выбора цвета.
252
+ if (_isInsideTextPropertiesPanel(e.target) && !_isNativeColorInput(e.target)) {
253
+ e.preventDefault();
254
+ return;
255
+ }
256
+
222
257
  const value = (textarea.value || '').trim();
223
258
  if (!isNewCreation || value.length > 0) return;
224
259
 
@@ -292,14 +327,74 @@ export function bindTextEditorInteractions(controller, {
292
327
  inputHandler = autoSize;
293
328
  }
294
329
 
330
+ const syncBackdrop = () => {
331
+ if (!controller.textEditor) return;
332
+ const wrapper = controller.textEditor.wrapper;
333
+ const objectId = controller.textEditor.objectId;
334
+ const editorProps = controller.textEditor.properties || {};
335
+
336
+ const backdrop = wrapper ? wrapper.querySelector('.moodboard-text-backdrop') : null;
337
+ if (!backdrop) return;
338
+
339
+ // Актуальные highlights/links берём из _mb.properties объекта (обновляются
340
+ // через Events.Object.StateChanged), с fallback на свойства редактора.
341
+ let currentHighlights = editorProps.highlights || null;
342
+ let currentLinks = editorProps.links || null;
343
+ if (objectId) {
344
+ try {
345
+ const pixiReq = { objectId, pixiObject: null };
346
+ controller.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
347
+ const props = pixiReq.pixiObject && pixiReq.pixiObject._mb
348
+ ? pixiReq.pixiObject._mb.properties
349
+ : null;
350
+ if (props) {
351
+ if (Array.isArray(props.highlights)) currentHighlights = props.highlights;
352
+ if (Array.isArray(props.links)) currentLinks = props.links;
353
+ }
354
+ } catch (_) {}
355
+ }
356
+
357
+ const content = textarea.value;
358
+ backdrop.innerHTML = buildHtmlWithRanges(content, currentLinks, currentHighlights);
359
+ };
360
+
361
+ const caretUpdateHandler = () => {
362
+ if (controller.textEditor && controller.textEditor.caret) {
363
+ updateCustomCaret(textarea, controller.textEditor.caret);
364
+ }
365
+ };
366
+
295
367
  textarea.addEventListener('blur', blurHandler);
296
368
  textarea.addEventListener('keydown', keydownHandler);
297
- textarea.addEventListener('input', inputHandler);
369
+ textarea.addEventListener('input', (e) => {
370
+ inputHandler(e);
371
+ syncBackdrop();
372
+ });
373
+
374
+ textarea.addEventListener('input', caretUpdateHandler);
375
+ textarea.addEventListener('keydown', () => setTimeout(caretUpdateHandler, 0));
376
+ textarea.addEventListener('keyup', caretUpdateHandler);
377
+ textarea.addEventListener('click', caretUpdateHandler);
378
+ textarea.addEventListener('focus', caretUpdateHandler);
379
+ document.addEventListener('selectionchange', caretUpdateHandler);
380
+
381
+ // Initial caret and backdrop update
382
+ setTimeout(() => {
383
+ caretUpdateHandler();
384
+ syncBackdrop();
385
+ }, 0);
298
386
 
299
387
  const removeDomListeners = () => {
300
388
  textarea.removeEventListener('blur', blurHandler);
301
389
  textarea.removeEventListener('keydown', keydownHandler);
302
390
  textarea.removeEventListener('input', inputHandler);
391
+
392
+ textarea.removeEventListener('input', caretUpdateHandler);
393
+ textarea.removeEventListener('keyup', caretUpdateHandler);
394
+ textarea.removeEventListener('click', caretUpdateHandler);
395
+ textarea.removeEventListener('focus', caretUpdateHandler);
396
+ document.removeEventListener('selectionchange', caretUpdateHandler);
397
+
303
398
  document.removeEventListener('mousedown', mousedownCaptureHandler, true);
304
399
  if (_pendingClickBlocker) {
305
400
  document.removeEventListener('click', _pendingClickBlocker, true);
@@ -377,14 +472,18 @@ export function closeTextEditorFromState(controller, commit) {
377
472
  }
378
473
  return;
379
474
  }
380
- if (objectId == null) {
381
- controller.eventBus.emit(Events.UI.ToolbarAction, {
382
- type: objectType,
383
- id: objectType,
384
- position: { x: position.x, y: position.y },
385
- properties: { content: value, fontSize: properties.fontSize },
386
- });
387
- } else {
475
+ if (objectId == null) {
476
+ controller.eventBus.emit(Events.UI.ToolbarAction, {
477
+ type: objectType,
478
+ id: objectType,
479
+ position: { x: position.x, y: position.y },
480
+ properties: {
481
+ content: value,
482
+ fontSize: properties.fontSize,
483
+ highlightColor: properties.highlightColor
484
+ },
485
+ });
486
+ } else {
388
487
  if (isNewCreation) {
389
488
  const oldContent = typeof initialContent === 'string' ? initialContent : '';
390
489
  controller.eventBus.emit(Events.Object.ContentChange, {
@@ -42,6 +42,12 @@ export function positionRegularTextEditor({
42
42
 
43
43
  let baseLeftPx = screenPos.x;
44
44
  let baseTopPx = screenPos.y;
45
+ // Computed padding-top of the static .mb-text element (e.g. 0.3em for list/markdown types).
46
+ // The static element top edge ≠ text glyph top: glyphs start paddingTop pixels lower.
47
+ // The editor wrapper must be shifted down by the same amount so text doesn't jump.
48
+ let staticPadTop = 0;
49
+ let baseWidthPx = null;
50
+ let baseHeightPx = null;
45
51
  try {
46
52
  if (!create && objectId && typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
47
53
  const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
@@ -50,6 +56,16 @@ export function positionRegularTextEditor({
50
56
  const cssTop = parseFloat(el.style.top || 'NaN');
51
57
  if (isFinite(cssLeft)) baseLeftPx = cssLeft;
52
58
  if (isFinite(cssTop)) baseTopPx = cssTop;
59
+ const rect = el.getBoundingClientRect?.();
60
+ if (rect && isFinite(rect.width) && rect.width > 0) baseWidthPx = rect.width;
61
+ if (rect && isFinite(rect.height) && rect.height > 0) baseHeightPx = rect.height;
62
+ if (typeof window.getComputedStyle === 'function') {
63
+ try {
64
+ const cs = window.getComputedStyle(el);
65
+ const pt = parseFloat(cs.paddingTop);
66
+ if (isFinite(pt) && pt > 0) staticPadTop = pt;
67
+ } catch (_) {}
68
+ }
53
69
  }
54
70
  }
55
71
  } catch (_) {}
@@ -57,7 +73,7 @@ export function positionRegularTextEditor({
57
73
  const leftPx = Math.round(baseLeftPx - padLeft);
58
74
  const topPx = create
59
75
  ? Math.round(baseTopPx - padTop - (lineHeightPx / 2))
60
- : Math.round(baseTopPx - padTop);
76
+ : Math.round(baseTopPx + staticPadTop - padTop + 1);
61
77
 
62
78
  wrapper.style.left = `${leftPx}px`;
63
79
  wrapper.style.top = `${topPx}px`;
@@ -70,6 +86,8 @@ export function positionRegularTextEditor({
70
86
  lineHeightPx,
71
87
  baseLeftPx,
72
88
  baseTopPx,
89
+ baseWidthPx,
90
+ baseHeightPx,
73
91
  };
74
92
  }
75
93
 
@@ -1,5 +1,6 @@
1
1
  import { Events } from '../../../core/events/Events.js';
2
2
  import { registerEditorListeners } from './InlineEditorListenersRegistry.js';
3
+ import { computeTextRightPadPx } from '../../../services/text/TextBoxMetrics.js';
3
4
 
4
5
  export function createRegularTextAutoSize({
5
6
  textarea,
@@ -13,7 +14,7 @@ export function createRegularTextAutoSize({
13
14
  textarea.style.height = 'auto';
14
15
 
15
16
  const fontPx = parseFloat(textarea.style.fontSize) || 16;
16
- const rightPad = Math.ceil(fontPx * 0.7) + 6;
17
+ const rightPad = computeTextRightPadPx(fontPx);
17
18
  const naturalW = Math.max(1, Math.ceil(textarea.scrollWidth + rightPad));
18
19
  const targetW = Math.round(Math.max(minWBound, naturalW));
19
20
  textarea.style.width = `${targetW}px`;