kingkont 0.7.87 → 0.7.89

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.87",
3
+ "version": "0.7.89",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -123,6 +123,25 @@ function renderLabelNodeBody(node, body) {
123
123
  ed.textContent = node.text || '';
124
124
  applyLabelStyle(ed, node.textStyle);
125
125
 
126
+ // Принудительно держим класс is-empty синхронизированным с textContent.
127
+ // CSS-селектор :empty не годится — после стирания всего текста браузер
128
+ // часто оставляет <br> внутри contenteditable, и :empty перестаёт
129
+ // матчить → плейсхолдер пропадал, инлайн-блок схлопывался по ширине,
130
+ // юзер кликал мимо и терял фокус. Класс на основе textContent надёжнее.
131
+ const syncEmpty = () => {
132
+ const empty = !ed.textContent || ed.textContent.length === 0;
133
+ ed.classList.toggle('is-empty', empty);
134
+ // Если контент стал «пустым», но в DOM остался шум (br/пустые text-node) —
135
+ // чистим innerHTML, чтобы курсор спокойно стоял в начале без артефактов.
136
+ // НЕ делаем если уже пусто — иначе зацикливание input-события.
137
+ if (empty && ed.innerHTML !== '' && ed.isContentEditable) {
138
+ // Сохраняем фокус: clearing innerHTML на сфокусированном
139
+ // contenteditable не должен дёргать blur, но сохраним ranged selection.
140
+ ed.innerHTML = '';
141
+ }
142
+ };
143
+ syncEmpty();
144
+
126
145
  const enterEditMode = () => {
127
146
  if (ed.isContentEditable) return;
128
147
  ed.contentEditable = 'plaintext-only';
@@ -141,6 +160,7 @@ function renderLabelNodeBody(node, body) {
141
160
  // closest('[contenteditable]') в обработчиках вне (dblclick/contextmenu)
142
161
  // не матчился — и ПКМ снова открывал наш node-menu.
143
162
  ed.removeAttribute('contenteditable');
163
+ syncEmpty();
144
164
  };
145
165
 
146
166
  ed.addEventListener('dblclick', e => {
@@ -148,7 +168,11 @@ function renderLabelNodeBody(node, body) {
148
168
  enterEditMode();
149
169
  });
150
170
  ed.addEventListener('blur', exitEditMode);
151
- ed.addEventListener('input', () => { node.text = ed.textContent; scheduleSave(); });
171
+ ed.addEventListener('input', () => {
172
+ node.text = ed.textContent;
173
+ syncEmpty();
174
+ scheduleSave();
175
+ });
152
176
  ed.addEventListener('keydown', e => {
153
177
  if (e.key === 'Escape') { e.preventDefault(); ed.blur(); }
154
178
  });
@@ -727,6 +751,12 @@ function attachDrag(el, node) {
727
751
  // Edit-режим label — не таскаем (контекст редактирования текста).
728
752
  if (e.currentTarget?.classList?.contains('label-text') && e.currentTarget.isContentEditable) return;
729
753
  if (e.target.closest('.delete')) return;
754
+ // Источник mousedown'а: header или label-text. Для label-text НЕ
755
+ // вызываем preventDefault и canvas.appendChild на mousedown — они
756
+ // ломают браузерный счётчик кликов и dblclick на label не срабатывает.
757
+ // Z-promote и preventDefault выполнятся ниже, в onMove, когда drag
758
+ // реально начнётся (перешёл порог 4px).
759
+ const sourceIsLabelText = e.currentTarget?.classList?.contains('label-text');
730
760
  // Multi-select c модификаторами — без drag
731
761
  if (e.metaKey || e.ctrlKey || e.shiftKey) {
732
762
  e.preventDefault();
@@ -741,16 +771,18 @@ function attachDrag(el, node) {
741
771
  state.selectedNodeIds.add(node.id);
742
772
  renderSelection();
743
773
  }
744
- e.preventDefault();
774
+ if (!sourceIsLabelText) e.preventDefault();
745
775
  const startX = e.clientX, startY = e.clientY;
746
776
  const origX = node.x, origY = node.y;
747
777
  el.classList.add('selected');
748
- canvas.appendChild(el);
749
778
  const arr = state.currentBoard.metadata.nodes;
750
- const idx = arr.indexOf(node);
751
- if (idx >= 0 && idx < arr.length - 1) {
752
- arr.splice(idx, 1);
753
- arr.push(node);
779
+ if (!sourceIsLabelText) {
780
+ canvas.appendChild(el);
781
+ const idx = arr.indexOf(node);
782
+ if (idx >= 0 && idx < arr.length - 1) {
783
+ arr.splice(idx, 1);
784
+ arr.push(node);
785
+ }
754
786
  }
755
787
 
756
788
  // Multi-drag: если выделено несколько нод и кликнули по одной из них —
@@ -780,6 +812,18 @@ function attachDrag(el, node) {
780
812
  const onMove = ev => {
781
813
  const dx = ev.clientX - startX, dy = ev.clientY - startY;
782
814
  if (!dragInitialized && Math.hypot(dx, dy) < 4) return;
815
+ // Первый раз перешли порог — для label-text z-promote отложен сюда,
816
+ // чтобы не ломать dblclick. Также гасим случайно стартовавший
817
+ // text-selection в read-режиме.
818
+ if (!dragInitialized && sourceIsLabelText) {
819
+ canvas.appendChild(el);
820
+ const idx = arr.indexOf(node);
821
+ if (idx >= 0 && idx < arr.length - 1) {
822
+ arr.splice(idx, 1);
823
+ arr.push(node);
824
+ }
825
+ try { window.getSelection()?.removeAllRanges(); } catch {}
826
+ }
783
827
  dragInitialized = true;
784
828
  // Проверяем — курсор над таймлайном?
785
829
  const tlTrackEl = document.elementsFromPoint(ev.clientX, ev.clientY)
@@ -510,15 +510,23 @@
510
510
  всё равно имеет высоту хотя бы одной строки выбранного шрифта. */
511
511
  min-height: 1.5em;
512
512
  }
513
- .node.label-node .label-text:empty::before {
513
+ /* Плейсхолдер показываем по классу is-empty (а не :empty pseudo) —
514
+ selector :empty ломается когда браузер оставляет <br> в contenteditable
515
+ после Cmd+A/Backspace. JS ставит/снимает класс по textContent. */
516
+ .node.label-node .label-text.is-empty::before {
514
517
  content: attr(data-placeholder);
515
518
  color: rgba(200,200,200,0.55);
516
519
  }
520
+ /* Гарантированный размер пустой ноды — чтобы юзер не терял её визуально
521
+ (кликабельная область + плейсхолдер). */
522
+ .node.label-node .label-text.is-empty {
523
+ min-width: 240px;
524
+ }
517
525
  /* Когда label пустая — рисуем подсказывающий dashed-контур, чтобы юзер
518
526
  видел что нода существует (текст-плейсхолдер мог быть не замечен).
519
527
  При наличии текста — никакой рамки, чисто подпись на холсте. */
520
- .node.label-node:has(.label-text:empty) {
521
- outline: 1px dashed rgba(200,200,200,0.25);
528
+ .node.label-node:has(.label-text.is-empty) {
529
+ outline: 1px dashed rgba(200,200,200,0.35);
522
530
  outline-offset: 1px;
523
531
  border-radius: 3px;
524
532
  }
@@ -526,6 +534,17 @@
526
534
  природу» ноды, в read-mode сигнал что dblclick → редактировать. */
527
535
  .node.label-node .label-text { cursor: text; }
528
536
  .node.label-node:hover .label-text:not([contenteditable]) { cursor: pointer; }
537
+ /* В read-режиме блокируем text-selection — чтобы single-click не выделял
538
+ слово, и mousedown без preventDefault (для работы dblclick) не давал
539
+ браузеру стартовать selection-drag. В edit-режиме выделение разрешено. */
540
+ .node.label-node .label-text:not([contenteditable]) {
541
+ user-select: none;
542
+ -webkit-user-select: none;
543
+ }
544
+ .node.label-node .label-text[contenteditable] {
545
+ user-select: text;
546
+ -webkit-user-select: text;
547
+ }
529
548
  /* === Label-шрифты ===
530
549
  Все варианты — bundled woff2 (см. assets/fonts/), кириллица поддержана. */
531
550
  .node.label-node .label-text[data-font="pencil"] {