kingkont 0.20.60 → 0.20.62

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.20.60",
3
+ "version": "0.20.62",
4
4
  "description": "KingKont \u00b7 Chatium \u2014 \u043d\u043e\u0434-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0441\u0446\u0435\u043d \u0441 AI-\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0435\u0439 (\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438/\u0432\u0438\u0434\u0435\u043e/\u0433\u043e\u043b\u043e\u0441/SFX/\u043c\u0443\u0437\u044b\u043a\u0430/\u0442\u0435\u043a\u0441\u0442)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -2603,10 +2603,13 @@ async function editNodeDescription(node) {
2603
2603
  if (next == null) return;
2604
2604
  node.description = next;
2605
2605
  scheduleSave();
2606
- if (typeof ensureNodeDescriptionEl === 'function') ensureNodeDescriptionEl(node);
2607
- // Перерасчёт node.height — описание участвует в bbox'е (см.
2608
- // fitNodeHeightToMedia в settings.js → расширяет node.height на descH).
2606
+ // Обновляем inline-description внутри ноды.
2609
2607
  const el = canvas.querySelector(`.node[data-id="${node.id}"]`);
2608
+ const desc = el?.querySelector('.node-description-inline');
2609
+ if (desc) refreshInlineDescription(desc, node);
2610
+ // Подчищаем legacy outside-sidecar (если остался от старых версий).
2611
+ if (typeof removeNodeDescriptionEl === 'function') removeNodeDescriptionEl(node.id);
2612
+ // Перерасчёт node.height — inline-description участвует в chrome'е.
2610
2613
  const media = el?.querySelector('img, video');
2611
2614
  if (typeof fitNodeHeightToMedia === 'function' && el && media) {
2612
2615
  fitNodeHeightToMedia(node, el, media);
@@ -2614,6 +2617,36 @@ async function editNodeDescription(node) {
2614
2617
  }
2615
2618
  window.editNodeDescription = editNodeDescription;
2616
2619
 
2620
+ // Inline-description: HTMLElement-helper. Видим только когда у юзера canModify
2621
+ // — placeholder «Добавить описание» имеет смысл только если он может его
2622
+ // заполнить (в view-only-template-режиме скрываем через CSS body.view-only-
2623
+ // mode .node-description-inline.empty).
2624
+ function refreshInlineDescription(el, node) {
2625
+ const text = (node.description || '').trim();
2626
+ if (text) {
2627
+ el.classList.remove('empty');
2628
+ if (el.textContent !== text) el.textContent = text;
2629
+ } else {
2630
+ el.classList.add('empty');
2631
+ if (el.textContent !== 'Добавить описание') el.textContent = 'Добавить описание';
2632
+ }
2633
+ }
2634
+ function attachInlineDescription(el, node) {
2635
+ refreshInlineDescription(el, node);
2636
+ el.addEventListener('mousedown', e => e.stopPropagation());
2637
+ el.addEventListener('click', e => {
2638
+ if (!el.classList.contains('empty')) return;
2639
+ e.stopPropagation();
2640
+ e.preventDefault();
2641
+ editNodeDescription(node);
2642
+ });
2643
+ el.addEventListener('dblclick', e => {
2644
+ e.stopPropagation();
2645
+ e.preventDefault();
2646
+ editNodeDescription(node);
2647
+ });
2648
+ }
2649
+
2617
2650
  // Скопировать / вставить настройки сцены (aspectRatio + defaultPrompts).
2618
2651
  // Хранение — in-memory state.clipboardSceneSettings (живёт до closeProject).
2619
2652
  // Юзер: «сделай возможность скопировать настройки из одной сцены в другую».
@@ -4020,6 +4053,19 @@ async function createNodeEl(node) {
4020
4053
  e.stopPropagation();
4021
4054
  deleteNode(node, el);
4022
4055
  });
4056
+ // Описание для image/video — теперь ВНУТРИ .node, как отдельная строка
4057
+ // между body и footer'ом. Раньше был outside-sidecar (.node-description-
4058
+ // outside) на canvas'е, который позиционировался у нижней грани bbox'а —
4059
+ // но при overlap'е соседних нод он визуально naehzhal на соседку (юзер:
4060
+ // «блок описания все еще наезжает на ноду которая лежит выше»). Inline-
4061
+ // вариант подчиняется flex-layout'у .node так же как image-body и
4062
+ // footer-row → не может вылезти за bbox.
4063
+ if (node.type === 'image' || node.type === 'video') {
4064
+ const desc = document.createElement('div');
4065
+ desc.className = 'node-description-inline';
4066
+ attachInlineDescription(desc, node);
4067
+ el.appendChild(desc);
4068
+ }
4023
4069
  if (['image', 'video', 'audio'].includes(node.type) && node.generated) {
4024
4070
  const footer = document.createElement('div');
4025
4071
  footer.className = 'node-footer';
@@ -4115,14 +4161,10 @@ async function createNodeEl(node) {
4115
4161
  e.stopPropagation();
4116
4162
  showNodeContextMenu(node, e.clientX, e.clientY);
4117
4163
  });
4118
- // Sidecar-описание для image/video отдельный элемент на canvas'е
4119
- // (ensureNodeDescriptionEl идемпотентно создаёт/обновляет/удаляет).
4120
- // Вызывается ЗДЕСЬ, чтобы покрыть все вызовы createNodeEl: renderCanvas
4121
- // / generate.js (новые ноды) / timeline.js / drawings.js / chat.js.
4122
- // Передаём el-hint — .node ещё не в canvas DOM (caller сделает appendChild
4123
- // после return), и без hint'а measure'а .node-footer не сработает →
4124
- // placeholder перекрыл бы regen-кнопки.
4125
- if (typeof ensureNodeDescriptionEl === 'function') ensureNodeDescriptionEl(node, el);
4164
+ // Подчищаем legacy outside-sidecar (мог остаться от старой версии этой
4165
+ // же сцены). Текущий рендер описания — inline-блок внутри .node (см.
4166
+ // attachInlineDescription выше; добавляется между body и footer).
4167
+ if (typeof removeNodeDescriptionEl === 'function') removeNodeDescriptionEl(node.id);
4126
4168
  return el;
4127
4169
  }
4128
4170
  // =================== Контекстное меню ноды (ПКМ) ===================
@@ -587,103 +587,19 @@ async function renderNodeBody(node, body) {
587
587
  // «попробуем показать описание ВНЕ этой ноды, но так чтобы оно с ней
588
588
  // перемещалось… совпадало с ней по ширине, а высота — сколько займет текст».
589
589
  // Идемпотентно: пересоздаёт DOM, если описание изменилось/добавилось/удалилось.
590
- function ensureNodeDescriptionEl(node, nodeElHint) {
590
+ // Legacy: outside-sidecar (`.node-description-outside`). Заменён на inline-
591
+ // description ВНУТРИ .node (см. board.js: attachInlineDescription). Сохраняем
592
+ // функцию как no-op (cleanup существующих) для обратной совместимости —
593
+ // другие модули могут вызывать. Inline-блок подчиняется flex-layout'у .node,
594
+ // не выходит за bbox и не наезжает на соседние ноды (юзер: «блок описания все
595
+ // еще наезжает на ноду которая лежит выше»).
596
+ function ensureNodeDescriptionEl(node /* , nodeElHint */) {
591
597
  if (!node) return null;
592
- const canvasEl = document.getElementById('canvas');
593
- if (!canvasEl) return null;
594
- if (node.type !== 'image' && node.type !== 'video') {
595
- const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
596
- if (existing) existing.remove();
597
- return null;
598
- }
599
- const text = (node.description || '').trim();
600
- // В view-only-режиме (share-link template) placeholder «Добавить описание»
601
- // не показываем — он имеет смысл только если юзер может редактировать.
602
- const canModify = (typeof state !== 'undefined' && state)
603
- ? (state.cloudCanModify !== false)
604
- : true;
605
- const isPlaceholder = !text;
606
- if (isPlaceholder && !canModify) {
607
- const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
608
- if (existing) existing.remove();
609
- return null;
610
- }
611
- // Если placeholder спатиально перекрылся бы с bbox'ом другой ноды на
612
- // холсте — не показываем его. Юзер: «блок описания наезжает на ноду
613
- // которая лежит выше». Filled-описания показываем всегда — текст важнее.
614
- if (isPlaceholder && Array.isArray(state?.currentBoard?.metadata?.nodes)) {
615
- const footerHGuess = 28, descHGuess = 29;
616
- const descTop = node.y + node.height - footerHGuess - descHGuess - 2;
617
- const descBot = node.y + node.height;
618
- const descLeft = node.x;
619
- const descRight = node.x + (node.width || 280);
620
- const overlapsOther = state.currentBoard.metadata.nodes.some(m => {
621
- if (m.id === node.id) return false;
622
- const mW = m.width || 280, mH = m.height || 200;
623
- return descLeft < m.x + mW && descRight > m.x
624
- && descTop < m.y + mH && descBot > m.y;
625
- });
626
- if (overlapsOther) {
627
- const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
628
- if (existing) existing.remove();
629
- return null;
630
- }
631
- }
632
- const sel = `.node-description-outside[data-desc-for="${node.id}"]`;
633
- let el = canvasEl.querySelector(sel);
634
- if (!el) {
635
- el = document.createElement('div');
636
- el.className = 'node-description-outside';
637
- el.dataset.descFor = node.id;
638
- canvasEl.appendChild(el);
639
- el.addEventListener('dblclick', e => {
640
- e.stopPropagation();
641
- e.preventDefault();
642
- if (typeof window.editNodeDescription === 'function') {
643
- window.editNodeDescription(node);
644
- }
645
- });
646
- el.addEventListener('click', e => {
647
- if (!el.classList.contains('empty')) return;
648
- e.stopPropagation();
649
- e.preventDefault();
650
- if (typeof window.editNodeDescription === 'function') {
651
- window.editNodeDescription(node);
652
- }
653
- });
654
- }
655
- if (isPlaceholder) {
656
- el.classList.add('empty');
657
- if (el.textContent !== 'Добавить описание') el.textContent = 'Добавить описание';
658
- } else {
659
- el.classList.remove('empty');
660
- if (el.textContent !== text) el.textContent = text;
661
- }
662
- positionNodeDescriptionEl(node, el, nodeElHint);
663
- // Если в этот момент .node ещё не вставлен в canvas — querySelector
664
- // вернёт null → footerH=0 → placeholder сел поверх footer'а. Через rAF
665
- // делаем повторный pass: к этому моменту caller сделал appendChild,
666
- // footer измерим, sidecar сдвинется чуть выше.
667
- requestAnimationFrame(() => positionNodeDescriptionEl(node, el));
668
- return el;
669
- }
670
- function positionNodeDescriptionEl(node, el, nodeElHint) {
671
- if (!el) el = document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
672
- if (!el) return;
673
- const w = node.width || 280;
674
- const h = node.height || 200;
675
- // У image/video с node.generated есть .node-footer (regen-btn, история ←/→)
676
- // — она у нижней грани. Поднимаем sidecar НАД footer'ом на footerH+2
677
- // выше — иначе placeholder «Добавить описание» перекрывал бы regen-кнопки
678
- // (юзер: «после появления кнопки добавить описание из ноды пропал ряд
679
- // кнопок который делал перегенерацию»).
680
- const nodeEl = nodeElHint || document.querySelector(`.node[data-id="${node.id}"]`);
681
- const footerH = nodeEl?.querySelector('.node-footer')?.offsetHeight || 0;
682
- const descH = el.offsetHeight || 0;
683
- el.style.left = node.x + 'px';
684
- el.style.top = (node.y + h - footerH - descH - 2) + 'px';
685
- el.style.width = w + 'px';
598
+ removeNodeDescriptionEl(node.id);
599
+ return null;
686
600
  }
601
+ // Legacy: no-op (inline-блок позиционируется flex-layout'ом).
602
+ function positionNodeDescriptionEl(/* node, el, nodeElHint */) { }
687
603
  function removeNodeDescriptionEl(nodeId) {
688
604
  const el = document.querySelector(`.node-description-outside[data-desc-for="${nodeId}"]`);
689
605
  if (el) el.remove();
@@ -698,36 +614,21 @@ function fitNodeHeightToMedia(node, nodeEl, mediaEl) {
698
614
  const natW = mediaEl.naturalWidth || mediaEl.videoWidth || 0;
699
615
  const natH = mediaEl.naturalHeight || mediaEl.videoHeight || 0;
700
616
  if (!natW || !natH) return;
701
- // Chrome — header + footer (если есть). image-node padding=0, ничего не отъедает.
617
+ // Chrome — header + inline-description + footer (всё что НЕ image-body).
618
+ // image-node padding=0, ничего не отъедает.
702
619
  const headerH = nodeEl.querySelector('.node-header')?.offsetHeight || 0;
703
620
  const footerH = nodeEl.querySelector('.node-footer')?.offsetHeight || 0;
704
- const chromeH = headerH + footerH;
621
+ const descInlineH = nodeEl.querySelector('.node-description-inline')?.offsetHeight || 0;
622
+ const chromeH = headerH + descInlineH + footerH;
705
623
  // Берём актуальную width — node.width если задано, иначе текущий offsetWidth.
706
624
  const w = node.width || nodeEl.offsetWidth;
707
625
  if (!w) return;
708
- // Высота описания учитывается ТОЛЬКО когда у ноды есть реальный текст-
709
- // описание. Для placeholder'а «Добавить описание» bbox не расширяем —
710
- // иначе нижняя грань ноды лезла бы в соседнюю ноду снизу (юзер: «блок
711
- // описания все еще наезжает на ноду которая лежит выше»). Placeholder
712
- // сидит у нижней грани в любом случае, но без расширения — слегка
713
- // перекрывает картинку, что OK для подсказки.
714
- const hasRealDesc = !!(node.description && node.description.trim());
715
- const descEl = hasRealDesc
716
- ? document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`)
717
- : null;
718
- const descH = descEl ? descEl.offsetHeight + 4 : 0; // +4 чтобы зрительно отделять
719
- const newH = Math.round(w * (natH / natW) + chromeH + descH);
626
+ const newH = Math.round(w * (natH / natW) + chromeH);
720
627
  // 2px tolerance — не шумим в save/connections при суб-пиксельных колебаниях.
721
- if (Math.abs(newH - (node.height || 0)) <= 2) {
722
- // Высоту не меняем, но позицию sidecar'а всё равно обновим — а вдруг
723
- // описание появилось/изменилось до того как media догрузилось.
724
- positionNodeDescriptionEl(node, null, nodeEl);
725
- return;
726
- }
628
+ if (Math.abs(newH - (node.height || 0)) <= 2) return;
727
629
  node.height = newH;
728
630
  nodeEl.style.height = newH + 'px';
729
631
  if (typeof renderConnections === 'function') renderConnections();
730
- positionNodeDescriptionEl(node, null, nodeEl);
731
632
  scheduleSave();
732
633
  }
733
634
 
@@ -110,45 +110,50 @@
110
110
  width: 36px; height: 36px; flex-shrink: 0; object-fit: contain;
111
111
  background: #1a1a1a; border-radius: 8px; padding: 4px;
112
112
  }
113
- /* Описание под image/video нодой. Раньше пробовали рендерить ВНУТРИ .node,
114
- но image/video задают фиксированный aspect-ratio высоту, а .node клипит
115
- overflow (content-visibility:auto + paint-containment) текст ниже не
116
- был виден. Теперь описание отдельный sibling-элемент на canvas'е
117
- (`.node-description-outside`), позиционируется в (node.x, node.y+h+gap),
118
- ширина = node.width, высота = auto по контенту. См. ensureNodeDescriptionEl
119
- в settings.js (вызывается из renderCanvas / drag / resize / ПКМ-меню). */
120
- .node-description-outside {
121
- position: absolute;
122
- padding: 6px 8px;
113
+ /* Описание image/video-ноды отдельная строка ВНУТРИ .node между
114
+ image-body и footer'ом. Раньше был outside-sidecar на canvas'е, но при
115
+ overlap'е соседних нод он визуально naehzhal на соседку (юзер: «блок
116
+ описания все еще наезжает на ноду которая лежит выше»). Inline-блок
117
+ подчиняется flex-layout'у .node так же как image-body и footer-row
118
+ никогда не выходит за bbox.
119
+ fitNodeHeightToMedia учитывает .node-description-inline в chromeH
120
+ (header + descInline + footer), так что node.height = aspect-image-H +
121
+ chromeH. Положение в DOM: header → body → description-inline → footer. */
122
+ .node-description-inline {
123
+ padding: 5px 8px;
123
124
  background: #1e1e1e; color: #ccc;
124
- border-left: 2px solid #4a6a9a; border-radius: 2px;
125
- font-size: 12px; line-height: 1.4;
125
+ border-top: 1px solid #383838;
126
+ font-size: 12px; line-height: 1.3;
126
127
  white-space: pre-wrap; word-wrap: break-word;
127
128
  box-sizing: border-box;
128
129
  user-select: text; -webkit-user-select: text; cursor: text;
129
- box-shadow: 0 2px 6px rgba(0,0,0,0.4);
130
- z-index: 2;
131
- pointer-events: auto;
130
+ flex-shrink: 0;
131
+ /* Если real-описание длиннее, чем разумно для inline-блока — обрезаем
132
+ и показываем ellipsis. Полный текст — через ПКМ → «Изменить описание»
133
+ или edit-модалку. */
134
+ max-height: 4.2em;
135
+ overflow: hidden;
136
+ text-overflow: ellipsis;
132
137
  }
133
- /* Placeholder: описание отсутствует, подсказка «Добавить описание».
134
- Юзер: «если описания в ноде нет показывай блок с описанием с текстом
135
- 'добавить описание'». Стиль приглушённый, чтобы не отвлекать; клик/
136
- дабл-клик открывает редактор. В view-only-режиме placeholder не
137
- показываем (см. ensureNodeDescriptionEl). */
138
- .node-description-outside.empty {
138
+ /* Placeholder «Добавить описание» приглушённый italic. В view-only-
139
+ template-режиме скрываем (placeholder имеет смысл только когда юзер
140
+ может его заполнить). */
141
+ .node-description-inline.empty {
139
142
  color: #6a6a6a;
140
143
  font-style: italic;
141
- border-left-color: #3a3a3a;
142
- background: #1a1a1a;
143
- box-shadow: none;
144
+ background: transparent;
144
145
  cursor: pointer;
145
- transition: color 0.12s, border-left-color 0.12s, background 0.12s;
146
+ transition: color 0.12s, background 0.12s;
146
147
  }
147
- .node-description-outside.empty:hover {
148
+ .node-description-inline.empty:hover {
148
149
  color: #aac8e6;
149
- border-left-color: #4a6a9a;
150
150
  background: #1e1e1e;
151
151
  }
152
+ body.view-only-mode .node-description-inline.empty { display: none; }
153
+ /* Legacy: outside-sidecar теперь не создаётся, но если остался от старой
154
+ сессии в DOM — прячем. settings.js removeNodeDescriptionEl его удаляет;
155
+ это дополнительная страховка на случай race condition'ов. */
156
+ .node-description-outside { display: none !important; }
152
157
  /* Legacy: оставляем класс для совместимости (вдруг где-то ещё ссылается). */
153
158
  .node .node-description { display: none; }
154
159