kingkont 0.7.85 → 0.7.86

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.85",
3
+ "version": "0.7.86",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -1773,6 +1773,38 @@ function showNodeContextMenu(node, clientX, clientY) {
1773
1773
  menu.appendChild(b);
1774
1774
  };
1775
1775
  add(node.name ? `✏ Переименовать (${node.name})` : '✏ Переименовать', () => renameNode(node));
1776
+ // Label: пункты для смены стиля (шрифт / размер / курсив).
1777
+ // Сделано через ПКМ а не постоянный hover-тулбар, чтобы UI label-ноды
1778
+ // оставался «чистым текстом» без фурнитуры.
1779
+ if (node.type === 'label') {
1780
+ if (!node.textStyle) node.textStyle = { fontSize: 32, italic: false, fontFamily: 'pencil' };
1781
+ const ts = node.textStyle;
1782
+ const curFont = LABEL_FONTS.find(f => f.id === ts.fontFamily) || LABEL_FONTS[0];
1783
+ add(`🔤 Шрифт: ${curFont.label}`, async () => {
1784
+ const choice = await askChoice('Шрифт label-ноды', LABEL_FONTS.map(f => f.label), curFont.label);
1785
+ if (!choice) return;
1786
+ const picked = LABEL_FONTS.find(f => f.label === choice);
1787
+ if (!picked) return;
1788
+ ts.fontFamily = picked.id;
1789
+ scheduleSave();
1790
+ await refreshNodeDOM(node.id);
1791
+ });
1792
+ add(`📏 Размер: ${ts.fontSize || 32}px`, async () => {
1793
+ const choice = await askChoice('Размер шрифта', LABEL_FONT_SIZES.map(s => s + 'px'), (ts.fontSize || 32) + 'px');
1794
+ if (!choice) return;
1795
+ const n = parseInt(choice, 10);
1796
+ if (Number.isFinite(n)) {
1797
+ ts.fontSize = n;
1798
+ scheduleSave();
1799
+ await refreshNodeDOM(node.id);
1800
+ }
1801
+ });
1802
+ add(ts.italic ? '𝐼 Курсив: вкл' : '𝐼 Курсив: выкл', async () => {
1803
+ ts.italic = !ts.italic;
1804
+ scheduleSave();
1805
+ await refreshNodeDOM(node.id);
1806
+ });
1807
+ }
1776
1808
  add('📋 Логи', () => showNodeLogs(node));
1777
1809
  if (node.generated) add('⚙ Параметры', () => showNodeSettings(node));
1778
1810
  if (node.status === 'generating') {
@@ -88,73 +88,56 @@ function applyLabelStyle(ed, style) {
88
88
  function renderLabelNodeBody(node, body) {
89
89
  if (!node.textStyle) node.textStyle = { ...LABEL_DEFAULT_STYLE };
90
90
 
91
- // Тулбар форматирования (на hover, чтобы не отвлекать от чистого текста).
92
- const tb = document.createElement('div');
93
- tb.className = 'label-toolbar';
94
- tb.addEventListener('mousedown', e => e.stopPropagation());
95
-
96
- const fontSel = document.createElement('select');
97
- fontSel.title = 'Шрифт';
98
- for (const f of LABEL_FONTS) {
99
- const o = document.createElement('option');
100
- o.value = f.id; o.textContent = f.label;
101
- fontSel.appendChild(o);
102
- }
103
- fontSel.value = node.textStyle.fontFamily || 'default';
104
- fontSel.addEventListener('change', () => {
105
- node.textStyle.fontFamily = fontSel.value;
106
- applyLabelStyle(ed, node.textStyle);
107
- scheduleSave();
108
- });
109
- tb.appendChild(fontSel);
110
-
111
- const sizeSel = document.createElement('select');
112
- sizeSel.title = 'Размер шрифта';
113
- for (const s of LABEL_FONT_SIZES) {
114
- const o = document.createElement('option');
115
- o.value = String(s); o.textContent = s + 'px';
116
- sizeSel.appendChild(o);
117
- }
118
- sizeSel.value = String(node.textStyle.fontSize || 32);
119
- sizeSel.addEventListener('change', () => {
120
- node.textStyle.fontSize = parseInt(sizeSel.value, 10) || 32;
121
- applyLabelStyle(ed, node.textStyle);
122
- scheduleSave();
123
- });
124
- tb.appendChild(sizeSel);
125
-
126
- const italicBtn = document.createElement('button');
127
- italicBtn.className = 'tt-italic';
128
- italicBtn.textContent = 'I';
129
- italicBtn.title = 'Курсив';
130
- if (node.textStyle.italic) italicBtn.classList.add('active');
131
- italicBtn.addEventListener('click', e => {
132
- e.stopPropagation();
133
- node.textStyle.italic = !node.textStyle.italic;
134
- italicBtn.classList.toggle('active', node.textStyle.italic);
135
- applyLabelStyle(ed, node.textStyle);
136
- scheduleSave();
137
- });
138
- tb.appendChild(italicBtn);
139
-
140
- body.appendChild(tb);
141
-
142
- // Сам текст — contenteditable (без рамок, без скроллбаров, ровно как
143
- // плавающая надпись на холсте). plaintext-only — режем форматирование
144
- // из буфера обмена (текст копируется только как текст).
91
+ // Сам текст. По умолчанию НЕ contenteditable обычный <div>. Это значит:
92
+ // • single-click = выделение ноды (drag handler из attachDrag)
93
+ // • ПКМ = node-context-menu (ниже добавлены пункты «Шрифт/Размер/Курсив»)
94
+ // • dblclick = вход в режим редактирования
95
+ // • blur = выход из редактирования
96
+ // Шрифт/размер/курсив меняются через ПКМ (а не через постоянный hover-тулбар),
97
+ // чтобы UI label-ноды был «чистый текст без фурнитуры».
145
98
  const ed = document.createElement('div');
146
99
  ed.className = 'label-text';
147
- ed.contentEditable = 'plaintext-only';
148
100
  ed.spellcheck = false;
149
- ed.dataset.placeholder = 'Текст…';
101
+ ed.dataset.placeholder = 'Двойной клик — редактировать';
150
102
  ed.textContent = node.text || '';
151
103
  applyLabelStyle(ed, node.textStyle);
104
+
105
+ const enterEditMode = () => {
106
+ if (ed.isContentEditable) return;
107
+ ed.contentEditable = 'plaintext-only';
108
+ ed.focus();
109
+ // Курсор в конец (если контент пустой — это и есть начало).
110
+ const sel = window.getSelection();
111
+ const r = document.createRange();
112
+ r.selectNodeContents(ed);
113
+ r.collapse(false);
114
+ sel?.removeAllRanges();
115
+ sel?.addRange(r);
116
+ };
117
+ const exitEditMode = () => {
118
+ if (!ed.isContentEditable) return;
119
+ // removeAttribute полностью убирает contenteditable, чтобы
120
+ // closest('[contenteditable]') в обработчиках вне (dblclick/contextmenu)
121
+ // не матчился — и ПКМ снова открывал наш node-menu.
122
+ ed.removeAttribute('contenteditable');
123
+ };
124
+
125
+ ed.addEventListener('dblclick', e => {
126
+ e.stopPropagation();
127
+ enterEditMode();
128
+ });
129
+ ed.addEventListener('blur', exitEditMode);
152
130
  ed.addEventListener('input', () => { node.text = ed.textContent; scheduleSave(); });
153
- ed.addEventListener('mousedown', e => e.stopPropagation());
154
- // Enter — перенос строки. Esc — снять фокус.
155
131
  ed.addEventListener('keydown', e => {
156
132
  if (e.key === 'Escape') { e.preventDefault(); ed.blur(); }
157
133
  });
134
+ ed.addEventListener('mousedown', e => {
135
+ // В edit-режиме: не пускаем drag-обработчик (нужно поставить курсор/выделить текст).
136
+ if (ed.isContentEditable) e.stopPropagation();
137
+ // В read-режиме: пузырится — attachDrag-handler (повешен и на label-text)
138
+ // обработает single-click как «выбрать ноду» и подхватит drag.
139
+ });
140
+
158
141
  body.appendChild(ed);
159
142
  }
160
143
 
@@ -711,7 +694,17 @@ function attachResize(el, node, handle) {
711
694
 
712
695
  function attachDrag(el, node) {
713
696
  const header = el.querySelector('.node-header');
714
- header.addEventListener('mousedown', e => {
697
+ // Для label-нод вешаем drag и на сам текст: header скрыт до hover, а
698
+ // юзер хочет цеплять ноду с любого места видимого текста. В handler'е
699
+ // пропускаем срабатывание, если label в edit-режиме (см. условие ниже).
700
+ const sources = [header];
701
+ if (node.type === 'label') {
702
+ const lt = el.querySelector('.label-text');
703
+ if (lt) sources.push(lt);
704
+ }
705
+ const handler = e => {
706
+ // Edit-режим label — не таскаем (контекст редактирования текста).
707
+ if (e.currentTarget?.classList?.contains('label-text') && e.currentTarget.isContentEditable) return;
715
708
  if (e.target.closest('.delete')) return;
716
709
  // Multi-select c модификаторами — без drag
717
710
  if (e.metaKey || e.ctrlKey || e.shiftKey) {
@@ -848,7 +841,8 @@ function attachDrag(el, node) {
848
841
  };
849
842
  document.addEventListener('mousemove', onMove);
850
843
  document.addEventListener('mouseup', onUp);
851
- });
844
+ };
845
+ for (const src of sources) if (src) src.addEventListener('mousedown', handler);
852
846
  }
853
847
 
854
848
  // Добавить ноду как клип в указанную дорожку с ripple-вставкой по времени
@@ -485,39 +485,12 @@
485
485
  position: relative;
486
486
  }
487
487
 
488
- /* Тулбар форматирования absolute под текстом, hover-only. */
489
- .node.label-node .label-toolbar {
490
- position: absolute;
491
- top: 100%; /* под label */
492
- left: 0;
493
- margin-top: 4px;
494
- display: flex; align-items: center; gap: 4px;
495
- padding: 3px 6px; background: rgba(20,20,20,0.92);
496
- border-radius: 4px;
497
- font-size: 11px; flex-shrink: 0;
498
- opacity: 0; transition: opacity 0.15s;
499
- pointer-events: none;
500
- white-space: nowrap;
501
- z-index: 10;
502
- }
488
+ /* Тулбар (.label-toolbar) больше не используется стиль/шрифт меняются
489
+ через ПКМ. Видимый header показываем только на hover. */
503
490
  .node.label-node:hover .node-header,
504
- .node.label-node:hover .label-toolbar,
505
- .node.label-node:focus-within .node-header,
506
- .node.label-node:focus-within .label-toolbar {
491
+ .node.label-node:focus-within .node-header {
507
492
  opacity: 1; pointer-events: auto;
508
493
  }
509
- .node.label-node .label-toolbar select,
510
- .node.label-node .label-toolbar button {
511
- background: #1e1e1e; border: 1px solid #383838; color: #ccc;
512
- border-radius: 3px; padding: 2px 6px; font-size: 11px;
513
- cursor: pointer; line-height: 1.2;
514
- }
515
- .node.label-node .label-toolbar select:hover,
516
- .node.label-node .label-toolbar button:hover { background: #2c2c2c; color: #fff; }
517
- .node.label-node .label-toolbar button.active {
518
- background: #3a5a8a; color: #fff; border-color: #4a6a9a;
519
- }
520
- .node.label-node .label-toolbar .tt-italic { font-style: italic; min-width: 22px; }
521
494
 
522
495
  /* Сам текст — auto-ширина по контенту, до max-width.
523
496
  line-height: 1.5 — чтобы descender'ы рукописных шрифтов (Caveat g/p/y,
@@ -539,8 +512,20 @@
539
512
  }
540
513
  .node.label-node .label-text:empty::before {
541
514
  content: attr(data-placeholder);
542
- color: rgba(200,200,200,0.35);
515
+ color: rgba(200,200,200,0.55);
516
+ }
517
+ /* Когда label пустая — рисуем подсказывающий dashed-контур, чтобы юзер
518
+ видел что нода существует (текст-плейсхолдер мог быть не замечен).
519
+ При наличии текста — никакой рамки, чисто подпись на холсте. */
520
+ .node.label-node:has(.label-text:empty) {
521
+ outline: 1px dashed rgba(200,200,200,0.25);
522
+ outline-offset: 1px;
523
+ border-radius: 3px;
543
524
  }
525
+ /* Курсор «I-beam» над текстом — на hover показываем «текстуальную
526
+ природу» ноды, в read-mode сигнал что dblclick → редактировать. */
527
+ .node.label-node .label-text { cursor: text; }
528
+ .node.label-node:hover .label-text:not([contenteditable]) { cursor: pointer; }
544
529
  /* === Label-шрифты ===
545
530
  Все варианты — bundled woff2 (см. assets/fonts/), кириллица поддержана. */
546
531
  .node.label-node .label-text[data-font="pencil"] {