kingkont 0.8.0 → 0.8.1

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.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -1285,7 +1285,7 @@ async function copySelectedNodes() {
1285
1285
  const n = state.currentBoard.metadata.nodes.find(x => x.id === id);
1286
1286
  if (!n) continue;
1287
1287
  let blob = null, textContent = null;
1288
- if (n.type === 'text') {
1288
+ if (n.type === 'text' || n.type === 'label') {
1289
1289
  textContent = n.text || '';
1290
1290
  } else if (n.file) {
1291
1291
  try {
@@ -1296,13 +1296,16 @@ async function copySelectedNodes() {
1296
1296
  state.clipboard.push({ node: { ...n }, blob, textContent });
1297
1297
  }
1298
1298
  console.log(`Скопировано в буфер: ${state.clipboard.length} нод`);
1299
+ // Дублируем в системный буфер обмена — чтобы Cmd+V в других приложениях
1300
+ // (браузер, Telegram, image-editor) тоже что-то получали.
1301
+ await writeNodesToSystemClipboard(state.clipboard);
1299
1302
  }
1300
1303
 
1301
1304
  // Скопировать одну ноду в буфер (для ПКМ → "Скопировать")
1302
1305
  async function copyNodeToClipboard(node) {
1303
1306
  if (!state.currentBoard) return;
1304
1307
  let blob = null, textContent = null;
1305
- if (node.type === 'text') {
1308
+ if (node.type === 'text' || node.type === 'label') {
1306
1309
  textContent = node.text || '';
1307
1310
  } else if (node.file) {
1308
1311
  try {
@@ -1314,6 +1317,67 @@ async function copyNodeToClipboard(node) {
1314
1317
  state.clipboardPasteCount = 0;
1315
1318
  state.clipboardSourceBoardKey = state.currentBoard.key;
1316
1319
  console.log(`В буфер: ${node.name || node.id}`);
1320
+ await writeNodesToSystemClipboard(state.clipboard);
1321
+ }
1322
+
1323
+ // Кладёт ноды в системный буфер обмена (где это имеет смысл):
1324
+ // • image — image/png (конвертим из jpg/webp через canvas)
1325
+ // • text/label — text/plain (если несколько — соединяем через \n\n)
1326
+ // • video/audio — пропускаем (Clipboard API binary-MIME'ов не держит)
1327
+ // Ошибки глотаем — основной путь это внутренний state.clipboard, system
1328
+ // clipboard это бонус. Если permission denied / focus lost / unsupported
1329
+ // MIME — внутренний работает как раньше.
1330
+ async function writeNodesToSystemClipboard(items) {
1331
+ if (!items?.length || !navigator.clipboard) return;
1332
+ try {
1333
+ // Один image-ноду — пишем как картинку.
1334
+ if (items.length === 1 && items[0].blob && items[0].node?.type === 'image') {
1335
+ const blob = items[0].blob;
1336
+ const pngBlob = blob.type === 'image/png' ? blob : await blobToPng(blob);
1337
+ if (pngBlob && typeof ClipboardItem === 'function') {
1338
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })]);
1339
+ return;
1340
+ }
1341
+ }
1342
+ // Текстовые ноды (text + label) — собираем тексты, пишем plain.
1343
+ const texts = items
1344
+ .filter(it => (it.node?.type === 'text' || it.node?.type === 'label') && typeof it.textContent === 'string')
1345
+ .map(it => it.textContent);
1346
+ if (texts.length) {
1347
+ await navigator.clipboard.writeText(texts.join('\n\n'));
1348
+ return;
1349
+ }
1350
+ // video/audio в одиночку — кладём имя файла как fallback (юзер хотя
1351
+ // бы pasted'ит куда-то осмысленный текст).
1352
+ const single = items[0]?.node;
1353
+ if (single?.file) {
1354
+ await navigator.clipboard.writeText(single.name || single.file.split('/').pop() || '');
1355
+ }
1356
+ } catch (e) {
1357
+ // Permission / async-context потерян — internal clipboard всё равно
1358
+ // отработал, юзер заметит при Cmd+V внутри редактора.
1359
+ console.warn('system clipboard write failed:', e?.message || e);
1360
+ }
1361
+ }
1362
+
1363
+ // Конвертирует image-blob (jpg/webp/heic/etc) в PNG через canvas —
1364
+ // system clipboard принимает только image/png универсально.
1365
+ async function blobToPng(blob) {
1366
+ const url = URL.createObjectURL(blob);
1367
+ try {
1368
+ const img = await new Promise((res, rej) => {
1369
+ const i = new Image();
1370
+ i.onload = () => res(i);
1371
+ i.onerror = rej;
1372
+ i.src = url;
1373
+ });
1374
+ const canvas = document.createElement('canvas');
1375
+ canvas.width = img.naturalWidth;
1376
+ canvas.height = img.naturalHeight;
1377
+ canvas.getContext('2d').drawImage(img, 0, 0);
1378
+ return await new Promise(res => canvas.toBlob(res, 'image/png'));
1379
+ } catch { return null; }
1380
+ finally { URL.revokeObjectURL(url); }
1317
1381
  }
1318
1382
 
1319
1383
  // Заменить контент текущей ноды содержимым из буфера (текущее уходит в history)
@@ -639,8 +639,14 @@
639
639
  transparent 0%, transparent 55%,
640
640
  #777 55%, #777 65%, transparent 65%,
641
641
  transparent 75%, #777 75%, #777 85%, transparent 85%);
642
- opacity: 0.45; border-bottom-right-radius: 8px;
642
+ /* Прячем по умолчанию — handle видно только на hover ноды. Раньше
643
+ был opacity:0.45, и на image-нодах после v0.7.97 (square corners)
644
+ полоски стали хорошо видны против картинки и выглядели как
645
+ посторонний UI-элемент. */
646
+ opacity: 0; transition: opacity 0.15s;
647
+ border-bottom-right-radius: 8px;
643
648
  }
649
+ .node:hover .resize-handle { opacity: 0.55; }
644
650
  .resize-handle:hover { opacity: 1; }
645
651
 
646
652
  .node-footer {
@@ -1282,18 +1288,33 @@
1282
1288
  .seg-control .seg:first-child { border-radius: 4px 0 0 4px; }
1283
1289
  .seg-control .seg:last-child { border-radius: 0 4px 4px 0; border-left: none; }
1284
1290
  .seg-control .seg.active { background: #3a5a8a; border-color: #4a6a9a; color: #fff; }
1291
+ /* Hover ТОЛЬКО на наведённую кнопку — синеватый оттенок, чтобы видно
1292
+ было «вот это сейчас выберется», а не серая «обычный hover» (которая
1293
+ раньше шла от глобального button:hover и выглядела одинаково с
1294
+ disabled-состоянием). */
1295
+ .seg-control .seg:hover:not(.active):not(:disabled) {
1296
+ background: #2a3854; border-color: #3a5a8a; color: #e8e8e8;
1297
+ }
1298
+ .seg-control .seg.active:hover:not(:disabled) {
1299
+ background: #4a6aa0;
1300
+ }
1285
1301
  .status { font-size: 12px; color: #aaa; display: flex; align-items: center; gap: 8px; }
1286
1302
  .status.error { color: #e07a6a; }
1287
1303
  .spinner { width: 14px; height: 14px; border: 2px solid #444; border-top-color: #6a8aaa; border-radius: 50%; animation: spin 0.9s linear infinite; }
1288
1304
  .spinner.lg { width: 32px; height: 32px; border-width: 3px; }
1289
1305
  @keyframes spin { to { transform: rotate(360deg); } }
1290
1306
 
1291
- /* mention popup */
1307
+ /* mention popup — overlay, чтобы появление @-списка не двигало modal-actions
1308
+ вниз (модалка не менялась бы по высоте). Позиционируется absolute
1309
+ относительно <label>-родителя (см. правило ниже). */
1310
+ .modal-card label:has(> .mention-popup) { position: relative; }
1292
1311
  .mention-popup {
1312
+ position: absolute; left: 0; right: 0; top: 100%;
1293
1313
  background: #1e1e1e; border: 1px solid #444; border-radius: 6px;
1294
- box-shadow: 0 4px 12px rgba(0,0,0,0.5);
1314
+ box-shadow: 0 8px 24px rgba(0,0,0,0.7);
1295
1315
  max-height: 240px; overflow-y: auto;
1296
- margin-top: 6px;
1316
+ margin-top: 4px;
1317
+ z-index: 100;
1297
1318
  }
1298
1319
  .mention-popup.hidden { display: none; }
1299
1320
  .mention-popup .mit {