kingkont 0.7.85 → 0.7.87

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.87",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -1111,25 +1111,35 @@ function askName(title, placeholder = '', initialValue = '', opts = {}) {
1111
1111
  // Возвращает выбранную строку или null если юзер закрыл.
1112
1112
  // window.prompt() в Electron renderer молча возвращает null — поэтому
1113
1113
  // нельзя использовать его для select-подобных диалогов.
1114
- function askChoice(title, options, currentValue) {
1114
+ // askChoice(title, options, currentValue, opts?)
1115
+ // opts:
1116
+ // vertical: bool — стэкаем кнопки вертикально (по умолчанию — flex-wrap)
1117
+ // optionStyles: { [opt]: 'css-string' } — кастомный CSS на кнопку (для preview-эффектов,
1118
+ // напр. рендер опции «Карандашом» в самом шрифте Karandashom)
1119
+ function askChoice(title, options, currentValue, opts = {}) {
1115
1120
  return new Promise(resolve => {
1116
1121
  const overlay = document.createElement('div');
1117
1122
  overlay.className = 'modal';
1118
1123
  overlay.style.cssText = 'position:fixed; inset:0; background:rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center; z-index:9999;';
1119
1124
  const box = document.createElement('div');
1120
- box.style.cssText = 'background:#222; border:1px solid #444; border-radius:8px; padding:18px 20px; min-width:360px; box-shadow:0 8px 32px rgba(0,0,0,0.6);';
1125
+ box.style.cssText = 'background:#222; border:1px solid #444; border-radius:8px; padding:18px 20px; min-width:360px; max-height:80vh; overflow-y:auto; box-shadow:0 8px 32px rgba(0,0,0,0.6);';
1121
1126
  const h = document.createElement('h3');
1122
1127
  h.textContent = title;
1123
1128
  h.style.cssText = 'margin:0 0 12px; font-size:14px; color:#e0e0e0;';
1124
1129
  box.append(h);
1125
1130
  const grid = document.createElement('div');
1126
- grid.style.cssText = 'display:flex; flex-wrap:wrap; gap:6px; margin-bottom:14px;';
1131
+ grid.style.cssText = opts.vertical
1132
+ ? 'display:flex; flex-direction:column; gap:6px; margin-bottom:14px;'
1133
+ : 'display:flex; flex-wrap:wrap; gap:6px; margin-bottom:14px;';
1127
1134
  const close = (val) => { overlay.remove(); resolve(val); };
1128
1135
  for (const opt of options) {
1129
1136
  const b = document.createElement('button');
1130
1137
  b.textContent = opt;
1131
- b.style.cssText = 'padding:6px 12px; background:#1a1a1a; color:#e0e0e0; border:1px solid #444; border-radius:4px; font-size:13px; cursor:pointer;' +
1132
- (opt === currentValue ? 'border-color:#7c3aed; background:#2a1a3a;' : '');
1138
+ let css = 'padding:6px 12px; background:#1a1a1a; color:#e0e0e0; border:1px solid #444; border-radius:4px; font-size:13px; cursor:pointer;';
1139
+ if (opts.vertical) css += 'text-align:left;';
1140
+ if (opt === currentValue) css += 'border-color:#7c3aed; background:#2a1a3a;';
1141
+ if (opts.optionStyles?.[opt]) css += opts.optionStyles[opt];
1142
+ b.style.cssText = css;
1133
1143
  b.addEventListener('click', () => close(opt));
1134
1144
  grid.append(b);
1135
1145
  }
@@ -1773,6 +1783,61 @@ function showNodeContextMenu(node, clientX, clientY) {
1773
1783
  menu.appendChild(b);
1774
1784
  };
1775
1785
  add(node.name ? `✏ Переименовать (${node.name})` : '✏ Переименовать', () => renameNode(node));
1786
+ // Label: пункты для смены стиля (шрифт / размер / курсив).
1787
+ // Сделано через ПКМ а не постоянный hover-тулбар, чтобы UI label-ноды
1788
+ // оставался «чистым текстом» без фурнитуры.
1789
+ if (node.type === 'label') {
1790
+ if (!node.textStyle) node.textStyle = { fontSize: 32, italic: false, fontFamily: 'pencil' };
1791
+ const ts = node.textStyle;
1792
+ const curFont = LABEL_FONTS.find(f => f.id === ts.fontFamily) || LABEL_FONTS[0];
1793
+ add(`🔤 Шрифт: ${curFont.label}`, async () => {
1794
+ // Каждая опция в picker'е рендерится в собственно её шрифте — юзер
1795
+ // сразу видит как «Карандашом» / «Прописью» / «Кистью» выглядят.
1796
+ const fontStyles = {};
1797
+ for (const f of LABEL_FONTS) {
1798
+ fontStyles[f.label] = `font-family: ${getLabelFontFamily(f.id)}; font-size: 22px; padding: 8px 14px; line-height: 1.3;`;
1799
+ }
1800
+ const choice = await askChoice(
1801
+ 'Шрифт label-ноды',
1802
+ LABEL_FONTS.map(f => f.label),
1803
+ curFont.label,
1804
+ { vertical: true, optionStyles: fontStyles },
1805
+ );
1806
+ if (!choice) return;
1807
+ const picked = LABEL_FONTS.find(f => f.label === choice);
1808
+ if (!picked) return;
1809
+ ts.fontFamily = picked.id;
1810
+ scheduleSave();
1811
+ await refreshNodeDOM(node.id);
1812
+ });
1813
+ add(`📏 Размер: ${ts.fontSize || 32}px`, async () => {
1814
+ // Каждый размер в picker'е рендерится в реальном размере (с лимитом),
1815
+ // чтобы юзер чувствовал «насколько крупно». Также вертикально.
1816
+ const sizeStyles = {};
1817
+ for (const s of LABEL_FONT_SIZES) {
1818
+ const previewSize = Math.min(s, 36); // не растим окно — лимит превью
1819
+ sizeStyles[s + 'px'] = `font-size: ${previewSize}px; line-height: 1.2; padding: 6px 14px;`;
1820
+ }
1821
+ const choice = await askChoice(
1822
+ 'Размер шрифта',
1823
+ LABEL_FONT_SIZES.map(s => s + 'px'),
1824
+ (ts.fontSize || 32) + 'px',
1825
+ { vertical: true, optionStyles: sizeStyles },
1826
+ );
1827
+ if (!choice) return;
1828
+ const n = parseInt(choice, 10);
1829
+ if (Number.isFinite(n)) {
1830
+ ts.fontSize = n;
1831
+ scheduleSave();
1832
+ await refreshNodeDOM(node.id);
1833
+ }
1834
+ });
1835
+ add(ts.italic ? '𝐼 Курсив: вкл' : '𝐼 Курсив: выкл', async () => {
1836
+ ts.italic = !ts.italic;
1837
+ scheduleSave();
1838
+ await refreshNodeDOM(node.id);
1839
+ });
1840
+ }
1776
1841
  add('📋 Логи', () => showNodeLogs(node));
1777
1842
  if (node.generated) add('⚙ Параметры', () => showNodeSettings(node));
1778
1843
  if (node.status === 'generating') {
@@ -85,76 +85,80 @@ function applyLabelStyle(ed, style) {
85
85
  ed.dataset.font = style.fontFamily || 'default';
86
86
  }
87
87
 
88
+ // Соответствие label-font-id → CSS font-family stack. Используется для
89
+ // preview-кнопок в picker'e шрифта (askChoice показывает каждую опцию
90
+ // в собственно её шрифте, чтобы юзер видел как выглядит). Семейства
91
+ // должны совпадать с теми, что заданы в .label-text[data-font="..."]
92
+ // в renderer/styles.css.
93
+ function getLabelFontFamily(id) {
94
+ const map = {
95
+ default: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
96
+ pencil: "'KK Neucha', 'Marker Felt', 'Comic Sans MS', cursive",
97
+ handwritten: "'KK Caveat', 'Caveat', 'Marker Felt', cursive",
98
+ brush: "'KK BadScript', 'Bad Script', 'Snell Roundhand', cursive",
99
+ marker: "'KK Pacifico', 'Pacifico', 'Marker Felt', cursive",
100
+ display: "'KK Lobster', 'Lobster', Georgia, serif",
101
+ elegant: "'KK YesevaOne', 'Yeseva One', Georgia, serif",
102
+ rounded: "'KK Comfortaa', 'Comfortaa', -apple-system, sans-serif",
103
+ serif: "'KK PTSerif', 'PT Serif', Georgia, serif",
104
+ mono: "'KK PTMono', 'PT Mono', ui-monospace, Menlo, monospace",
105
+ };
106
+ return map[id] || map.default;
107
+ }
108
+
88
109
  function renderLabelNodeBody(node, body) {
89
110
  if (!node.textStyle) node.textStyle = { ...LABEL_DEFAULT_STYLE };
90
111
 
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
- // из буфера обмена (текст копируется только как текст).
112
+ // Сам текст. По умолчанию НЕ contenteditable обычный <div>. Это значит:
113
+ // • single-click = выделение ноды (drag handler из attachDrag)
114
+ // • ПКМ = node-context-menu (ниже добавлены пункты «Шрифт/Размер/Курсив»)
115
+ // • dblclick = вход в режим редактирования
116
+ // • blur = выход из редактирования
117
+ // Шрифт/размер/курсив меняются через ПКМ (а не через постоянный hover-тулбар),
118
+ // чтобы UI label-ноды был «чистый текст без фурнитуры».
145
119
  const ed = document.createElement('div');
146
120
  ed.className = 'label-text';
147
- ed.contentEditable = 'plaintext-only';
148
121
  ed.spellcheck = false;
149
- ed.dataset.placeholder = 'Текст…';
122
+ ed.dataset.placeholder = 'Двойной клик — редактировать';
150
123
  ed.textContent = node.text || '';
151
124
  applyLabelStyle(ed, node.textStyle);
125
+
126
+ const enterEditMode = () => {
127
+ if (ed.isContentEditable) return;
128
+ ed.contentEditable = 'plaintext-only';
129
+ ed.focus();
130
+ // Курсор в конец (если контент пустой — это и есть начало).
131
+ const sel = window.getSelection();
132
+ const r = document.createRange();
133
+ r.selectNodeContents(ed);
134
+ r.collapse(false);
135
+ sel?.removeAllRanges();
136
+ sel?.addRange(r);
137
+ };
138
+ const exitEditMode = () => {
139
+ if (!ed.isContentEditable) return;
140
+ // removeAttribute полностью убирает contenteditable, чтобы
141
+ // closest('[contenteditable]') в обработчиках вне (dblclick/contextmenu)
142
+ // не матчился — и ПКМ снова открывал наш node-menu.
143
+ ed.removeAttribute('contenteditable');
144
+ };
145
+
146
+ ed.addEventListener('dblclick', e => {
147
+ e.stopPropagation();
148
+ enterEditMode();
149
+ });
150
+ ed.addEventListener('blur', exitEditMode);
152
151
  ed.addEventListener('input', () => { node.text = ed.textContent; scheduleSave(); });
153
- ed.addEventListener('mousedown', e => e.stopPropagation());
154
- // Enter — перенос строки. Esc — снять фокус.
155
152
  ed.addEventListener('keydown', e => {
156
153
  if (e.key === 'Escape') { e.preventDefault(); ed.blur(); }
157
154
  });
155
+ ed.addEventListener('mousedown', e => {
156
+ // В edit-режиме: не пускаем drag-обработчик (нужно поставить курсор/выделить текст).
157
+ if (ed.isContentEditable) e.stopPropagation();
158
+ // В read-режиме: пузырится — attachDrag-handler (повешен и на label-text)
159
+ // обработает single-click как «выбрать ноду» и подхватит drag.
160
+ });
161
+
158
162
  body.appendChild(ed);
159
163
  }
160
164
 
@@ -711,7 +715,17 @@ function attachResize(el, node, handle) {
711
715
 
712
716
  function attachDrag(el, node) {
713
717
  const header = el.querySelector('.node-header');
714
- header.addEventListener('mousedown', e => {
718
+ // Для label-нод вешаем drag и на сам текст: header скрыт до hover, а
719
+ // юзер хочет цеплять ноду с любого места видимого текста. В handler'е
720
+ // пропускаем срабатывание, если label в edit-режиме (см. условие ниже).
721
+ const sources = [header];
722
+ if (node.type === 'label') {
723
+ const lt = el.querySelector('.label-text');
724
+ if (lt) sources.push(lt);
725
+ }
726
+ const handler = e => {
727
+ // Edit-режим label — не таскаем (контекст редактирования текста).
728
+ if (e.currentTarget?.classList?.contains('label-text') && e.currentTarget.isContentEditable) return;
715
729
  if (e.target.closest('.delete')) return;
716
730
  // Multi-select c модификаторами — без drag
717
731
  if (e.metaKey || e.ctrlKey || e.shiftKey) {
@@ -848,7 +862,8 @@ function attachDrag(el, node) {
848
862
  };
849
863
  document.addEventListener('mousemove', onMove);
850
864
  document.addEventListener('mouseup', onUp);
851
- });
865
+ };
866
+ for (const src of sources) if (src) src.addEventListener('mousedown', handler);
852
867
  }
853
868
 
854
869
  // Добавить ноду как клип в указанную дорожку с 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"] {