kingkont 0.20.61 → 0.20.63

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/index.html CHANGED
@@ -51,7 +51,7 @@
51
51
  </div>
52
52
 
53
53
  <div class="sidebar-section kk-edit-only">
54
- <h3>Общее <button id="newLocation" disabled title="Создать общую доску">+</button></h3>
54
+ <h3>Ресурсы <button id="newLocation" disabled title="Создать общую доску">+</button></h3>
55
55
  <div class="sidebar-list" id="locationList"></div>
56
56
  </div>
57
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.20.61",
3
+ "version": "0.20.63",
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,105 +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
- // Если description (placeholder ИЛИ filled) спатиально перекрылся бы с
612
- // bbox'ом другой ноды — не показываем его. Юзер: «блок описания наезжает
613
- // на ноду которая лежит выше». Текст остаётся в node.description, виден
614
- // через ПКМ-меню / edit-модалку.
615
- if (Array.isArray(state?.currentBoard?.metadata?.nodes)) {
616
- const footerHGuess = 28;
617
- const descHGuess = isPlaceholder ? 29 : 50; // filled может быть выше
618
- const descTop = node.y + node.height - footerHGuess - descHGuess - 2;
619
- const descBot = node.y + node.height;
620
- const descLeft = node.x;
621
- const descRight = node.x + (node.width || 280);
622
- const overlapsOther = state.currentBoard.metadata.nodes.some(m => {
623
- if (m.id === node.id) return false;
624
- const mW = m.width || 280, mH = m.height || 200;
625
- return descLeft < m.x + mW && descRight > m.x
626
- && descTop < m.y + mH && descBot > m.y;
627
- });
628
- if (overlapsOther) {
629
- const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
630
- if (existing) existing.remove();
631
- return null;
632
- }
633
- }
634
- const sel = `.node-description-outside[data-desc-for="${node.id}"]`;
635
- let el = canvasEl.querySelector(sel);
636
- if (!el) {
637
- el = document.createElement('div');
638
- el.className = 'node-description-outside';
639
- el.dataset.descFor = node.id;
640
- canvasEl.appendChild(el);
641
- el.addEventListener('dblclick', e => {
642
- e.stopPropagation();
643
- e.preventDefault();
644
- if (typeof window.editNodeDescription === 'function') {
645
- window.editNodeDescription(node);
646
- }
647
- });
648
- el.addEventListener('click', e => {
649
- if (!el.classList.contains('empty')) return;
650
- e.stopPropagation();
651
- e.preventDefault();
652
- if (typeof window.editNodeDescription === 'function') {
653
- window.editNodeDescription(node);
654
- }
655
- });
656
- }
657
- if (isPlaceholder) {
658
- el.classList.add('empty');
659
- if (el.textContent !== 'Добавить описание') el.textContent = 'Добавить описание';
660
- } else {
661
- el.classList.remove('empty');
662
- if (el.textContent !== text) el.textContent = text;
663
- }
664
- positionNodeDescriptionEl(node, el, nodeElHint);
665
- // Если в этот момент .node ещё не вставлен в canvas — querySelector
666
- // вернёт null → footerH=0 → placeholder сел поверх footer'а. Через rAF
667
- // делаем повторный pass: к этому моменту caller сделал appendChild,
668
- // footer измерим, sidecar сдвинется чуть выше.
669
- requestAnimationFrame(() => positionNodeDescriptionEl(node, el));
670
- return el;
671
- }
672
- function positionNodeDescriptionEl(node, el, nodeElHint) {
673
- if (!el) el = document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
674
- if (!el) return;
675
- const w = node.width || 280;
676
- const h = node.height || 200;
677
- // У image/video с node.generated есть .node-footer (regen-btn, история ←/→)
678
- // — она у нижней грани. Поднимаем sidecar НАД footer'ом на footerH+2
679
- // выше — иначе placeholder «Добавить описание» перекрывал бы regen-кнопки
680
- // (юзер: «после появления кнопки добавить описание из ноды пропал ряд
681
- // кнопок который делал перегенерацию»).
682
- const nodeEl = nodeElHint || document.querySelector(`.node[data-id="${node.id}"]`);
683
- const footerH = nodeEl?.querySelector('.node-footer')?.offsetHeight || 0;
684
- const descH = el.offsetHeight || 0;
685
- el.style.left = node.x + 'px';
686
- el.style.top = (node.y + h - footerH - descH - 2) + 'px';
687
- el.style.width = w + 'px';
598
+ removeNodeDescriptionEl(node.id);
599
+ return null;
688
600
  }
601
+ // Legacy: no-op (inline-блок позиционируется flex-layout'ом).
602
+ function positionNodeDescriptionEl(/* node, el, nodeElHint */) { }
689
603
  function removeNodeDescriptionEl(nodeId) {
690
604
  const el = document.querySelector(`.node-description-outside[data-desc-for="${nodeId}"]`);
691
605
  if (el) el.remove();
@@ -700,36 +614,21 @@ function fitNodeHeightToMedia(node, nodeEl, mediaEl) {
700
614
  const natW = mediaEl.naturalWidth || mediaEl.videoWidth || 0;
701
615
  const natH = mediaEl.naturalHeight || mediaEl.videoHeight || 0;
702
616
  if (!natW || !natH) return;
703
- // Chrome — header + footer (если есть). image-node padding=0, ничего не отъедает.
617
+ // Chrome — header + inline-description + footer (всё что НЕ image-body).
618
+ // image-node padding=0, ничего не отъедает.
704
619
  const headerH = nodeEl.querySelector('.node-header')?.offsetHeight || 0;
705
620
  const footerH = nodeEl.querySelector('.node-footer')?.offsetHeight || 0;
706
- const chromeH = headerH + footerH;
621
+ const descInlineH = nodeEl.querySelector('.node-description-inline')?.offsetHeight || 0;
622
+ const chromeH = headerH + descInlineH + footerH;
707
623
  // Берём актуальную width — node.width если задано, иначе текущий offsetWidth.
708
624
  const w = node.width || nodeEl.offsetWidth;
709
625
  if (!w) return;
710
- // Высота описания учитывается ТОЛЬКО когда у ноды есть реальный текст-
711
- // описание. Для placeholder'а «Добавить описание» bbox не расширяем —
712
- // иначе нижняя грань ноды лезла бы в соседнюю ноду снизу (юзер: «блок
713
- // описания все еще наезжает на ноду которая лежит выше»). Placeholder
714
- // сидит у нижней грани в любом случае, но без расширения — слегка
715
- // перекрывает картинку, что OK для подсказки.
716
- const hasRealDesc = !!(node.description && node.description.trim());
717
- const descEl = hasRealDesc
718
- ? document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`)
719
- : null;
720
- const descH = descEl ? descEl.offsetHeight + 4 : 0; // +4 чтобы зрительно отделять
721
- const newH = Math.round(w * (natH / natW) + chromeH + descH);
626
+ const newH = Math.round(w * (natH / natW) + chromeH);
722
627
  // 2px tolerance — не шумим в save/connections при суб-пиксельных колебаниях.
723
- if (Math.abs(newH - (node.height || 0)) <= 2) {
724
- // Высоту не меняем, но позицию sidecar'а всё равно обновим — а вдруг
725
- // описание появилось/изменилось до того как media догрузилось.
726
- positionNodeDescriptionEl(node, null, nodeEl);
727
- return;
728
- }
628
+ if (Math.abs(newH - (node.height || 0)) <= 2) return;
729
629
  node.height = newH;
730
630
  nodeEl.style.height = newH + 'px';
731
631
  if (typeof renderConnections === 'function') renderConnections();
732
- positionNodeDescriptionEl(node, null, nodeEl);
733
632
  scheduleSave();
734
633
  }
735
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