kingkont 0.8.7 → 0.9.0

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.
@@ -347,6 +347,15 @@ async function renderNodeBody(node, body) {
347
347
  if (nodeEl) { _mediaHydrationObserver.unobserve(nodeEl); nodeEl.__hydrate = null; }
348
348
 
349
349
  if (node.type === 'audio') {
350
+ // Промпт (текст TTS) виден всегда — даже после генерации. Юзер
351
+ // должен понимать что говорит голос без открытия Settings-modal'я.
352
+ const promptText = node.generated?.rawPrompt || node.generated?.prompt;
353
+ if (promptText) {
354
+ const pp = document.createElement('div');
355
+ pp.className = 'audio-prompt';
356
+ pp.textContent = promptText;
357
+ body.appendChild(pp);
358
+ }
350
359
  const fname = document.createElement('div');
351
360
  fname.className = 'filename';
352
361
  fname.textContent = node.file;
@@ -416,7 +425,20 @@ async function renderNodeBody(node, body) {
416
425
  // Раньше "metadata" грузил chunk для duration/dimensions с каждой ноды на selectBoard.
417
426
  media.preload = 'none';
418
427
  }
419
- media.addEventListener('mousedown', e => e.stopPropagation());
428
+ // Click на media-элементе = выбрать ноду (как клик по header'у).
429
+ // НЕ делаем preventDefault — иначе сломаются нативные video-controls
430
+ // (play/pause/seek). stopPropagation чтобы не запустился rubber-band
431
+ // селектор холста.
432
+ media.addEventListener('mousedown', e => {
433
+ e.stopPropagation();
434
+ if (e.metaKey || e.ctrlKey || e.shiftKey) {
435
+ toggleSelection(node.id);
436
+ } else if (!state.selectedNodeIds.has(node.id)) {
437
+ clearSelection();
438
+ state.selectedNodeIds.add(node.id);
439
+ }
440
+ renderSelection();
441
+ });
420
442
  // Заменяем placeholder ИЛИ ранее показанный thumbnail
421
443
  const target = (nodeEl && nodeEl.__thumbEl && nodeEl.__thumbEl.parentNode)
422
444
  ? nodeEl.__thumbEl
@@ -497,6 +519,23 @@ async function renderNodeBody(node, body) {
497
519
  playRow.appendChild(playBtn);
498
520
  body.appendChild(playRow);
499
521
  }
522
+ } else if (node.type === 'image' && !node.file && (node.generated?.rawPrompt || node.generated?.prompt)) {
523
+ // Fallback: image-нода с промптом, но без файла (ещё не генерилась
524
+ // и status != 'draft'). Показываем preview-блок чтобы юзер видел
525
+ // что собирается генерироваться.
526
+ const wrap = document.createElement('div');
527
+ wrap.className = 'gen-pending';
528
+ const ic = document.createElement('div');
529
+ ic.style.cssText = 'font-size:36px; opacity:0.7;';
530
+ ic.textContent = '🖼';
531
+ const st = document.createElement('div');
532
+ st.className = 'state-text';
533
+ st.textContent = 'Не сгенерировано — открой ↻ для запуска';
534
+ const pp = document.createElement('div');
535
+ pp.className = 'prompt-preview';
536
+ pp.textContent = node.generated?.rawPrompt || node.generated?.prompt || '';
537
+ wrap.append(ic, st, pp);
538
+ body.appendChild(wrap);
500
539
  }
501
540
  }
502
541
 
package/renderer/state.js CHANGED
@@ -455,6 +455,11 @@ function getFileType(file) {
455
455
 
456
456
  const state = {
457
457
  filmHandle: null,
458
+ // Облачный проект: id записи в t_spaces_server_projects_K1 (если открыт
459
+ // cloud-проект, иначе null). filmHandle всё равно валидный (cloudFs-shim).
460
+ cloudProjectId: null,
461
+ // Есть несохранённые изменения (показываем «•» возле «Сохранить на сервер»).
462
+ cloudDirty: false,
458
463
  currentBoard: null, // { kind, name, key, handle, metadata, urls }
459
464
  genKind: 'image',
460
465
  imageModel: localStorage.getItem('imageModel') || 'nano-banana-2', // 'nano-banana-2' | 'nano-banana-pro' | 'grok' | ...
@@ -230,15 +230,18 @@
230
230
  margin-bottom: 14px; text-align: center;
231
231
  flex-shrink: 0;
232
232
  }
233
- /* Recents в горизонтальную ленту со скроллом. Карточки фиксированной
234
- ширины чтобы scroll работал предсказуемо. */
233
+ /* Recents плитка (responsive grid). Карточки фиксированной ширины,
234
+ заполняют ряд слева направо, при нехватке места переносятся. Скроллится
235
+ вертикально, если карточек много. */
235
236
  .welcome-recent-grid {
236
- display: flex; flex-direction: row; gap: 16px;
237
- overflow-x: auto; overflow-y: hidden;
238
- padding: 8px 32px 24px;
239
- scroll-snap-type: x proximity;
240
- }
241
- .welcome-recent-grid::-webkit-scrollbar { height: 8px; }
237
+ display: grid;
238
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
239
+ gap: 16px;
240
+ overflow-y: auto; overflow-x: hidden;
241
+ padding: 8px 52px 24px;
242
+ align-content: start;
243
+ }
244
+ .welcome-recent-grid::-webkit-scrollbar { width: 8px; }
242
245
 
243
246
  /* === Глобальные dark-scrollbars === */
244
247
  /* Firefox */
@@ -257,8 +260,9 @@
257
260
  background: #232323; border: 1px solid #333; border-radius: 8px;
258
261
  overflow: hidden; cursor: pointer; transition: border-color 0.12s, transform 0.12s;
259
262
  display: flex; flex-direction: column;
260
- width: 240px; flex-shrink: 0;
261
- scroll-snap-align: start;
263
+ /* Grid-layout: ширина задаётся grid-column'ом (responsive),
264
+ fixed-width убран. position:relative нужен для absolute-расположенного × delete. */
265
+ position: relative;
262
266
  }
263
267
  /* Карточка «Открыть проект» — стилизована под обычную recent-карточку,
264
268
  но контент не превью, а большая иконка + надпись. */
@@ -284,6 +288,18 @@
284
288
  color: #444; font-size: 32px;
285
289
  }
286
290
  .welcome-card-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
291
+ /* ☁-бейдж в углу обложки облачного проекта. Делает cloud/folder-проекты
292
+ визуально различимыми в общем grid'е (без необходимости второго рядa). */
293
+ .welcome-card-thumb { position: relative; }
294
+ .welcome-card-cloud-badge {
295
+ position: absolute; right: 6px; bottom: 6px;
296
+ background: rgba(20, 30, 50, 0.85); color: #9cf;
297
+ border: 1px solid rgba(160, 200, 255, 0.35);
298
+ border-radius: 50%; width: 22px; height: 22px;
299
+ display: flex; align-items: center; justify-content: center;
300
+ font-size: 13px; line-height: 1;
301
+ backdrop-filter: blur(2px);
302
+ }
287
303
  .welcome-card-meta { padding: 8px 12px; }
288
304
  .welcome-card-name { font-size: 13px; color: #ddd; word-break: break-all; }
289
305
  .welcome-card-ts { font-size: 11px; color: #666; margin-top: 2px; }
@@ -731,8 +747,16 @@
731
747
  .fs-modal #fsClose {
732
748
  position: absolute; top: 16px; right: 16px;
733
749
  background: rgba(0,0,0,0.6); border: 1px solid #444; color: #fff;
734
- font-size: 22px; line-height: 1; padding: 4px 12px; border-radius: 4px;
750
+ font-size: 22px; line-height: 1;
751
+ /* Увеличен hit-area: квадрат 40×40, центрированный «×». Раньше padding
752
+ был 4px 12px → размер ~30×30, и нижне-правые пиксели оказывались под
753
+ <img>/<video> в .fs-stage (одна стека, нет z-index). z-index:1100
754
+ гарантирует что × ВСЕГДА сверху всего внутри fs-modal. */
755
+ width: 40px; height: 40px;
756
+ display: flex; align-items: center; justify-content: center;
757
+ padding: 0; border-radius: 4px;
735
758
  cursor: pointer;
759
+ z-index: 1100;
736
760
  }
737
761
  .fs-modal #fsClose:hover { background: rgba(255,68,68,0.7); }
738
762
 
@@ -779,6 +803,7 @@
779
803
  font-size: 36px; line-height: 1; padding: 4px 14px; border-radius: 6px;
780
804
  cursor: pointer; user-select: none;
781
805
  transition: background 0.12s;
806
+ z-index: 1050;
782
807
  }
783
808
  .fs-modal .fs-nav:hover { background: rgba(0,0,0,0.85); }
784
809
  .fs-modal .fs-nav:disabled { opacity: 0.3; cursor: default; }
@@ -945,6 +970,44 @@
945
970
  .preview-panel .timeline-preview-img.hidden,
946
971
  .preview-panel .timeline-preview.hidden { display: none; }
947
972
 
973
+ /* Overlay-контролы поверх preview-стейджа. Видимы пока есть клипы.
974
+ Полу-прозрачный фон + лёгкий blur — читаемо поверх любой картинки.
975
+ Контейнер не переносит на новую строку (white-space:nowrap) даже на
976
+ узком preview — лучше горизонтальный скролл, чем разорванный таймер. */
977
+ .preview-controls {
978
+ position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%);
979
+ display: flex; align-items: center; gap: 10px;
980
+ padding: 8px 14px; border-radius: 999px;
981
+ background: rgba(0,0,0,0.55); border: 1px solid rgba(255,255,255,0.1);
982
+ backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
983
+ z-index: 10; opacity: 0.45; transition: opacity 0.15s;
984
+ white-space: nowrap; flex-wrap: nowrap;
985
+ max-width: calc(100% - 16px);
986
+ }
987
+ .preview-stage:hover .preview-controls,
988
+ .preview-controls.is-playing { opacity: 1; }
989
+ .preview-controls.hidden { display: none; }
990
+ .preview-controls button {
991
+ background: transparent; border: none; color: #fff;
992
+ font-size: 24px; line-height: 1;
993
+ width: 32px; height: 32px;
994
+ display: flex; align-items: center; justify-content: center;
995
+ padding: 0; cursor: pointer; border-radius: 50%;
996
+ flex-shrink: 0;
997
+ }
998
+ .preview-controls button:hover { background: rgba(255,255,255,0.15); }
999
+ .preview-controls .preview-time {
1000
+ font-family: ui-monospace, 'SF Mono', monospace;
1001
+ font-size: 15px; color: #fff;
1002
+ min-width: 90px; text-align: center;
1003
+ white-space: nowrap; flex-shrink: 0;
1004
+ }
1005
+ /* «×» закрытия — чуть тоньше play-кнопки чтобы не доминировал визуально. */
1006
+ .preview-controls #previewCloseBtn {
1007
+ font-size: 20px; opacity: 0.75;
1008
+ }
1009
+ .preview-controls #previewCloseBtn:hover { opacity: 1; background: rgba(255,68,68,0.55); }
1010
+
948
1011
  /* === Таймлайн (нижняя панель, full-width) === */
949
1012
  .timeline-panel {
950
1013
  border-top: 1px solid #333; background: #1c1c1c;
@@ -1237,8 +1300,20 @@
1237
1300
  background: #141414;
1238
1301
  }
1239
1302
  .gen-pending .prompt-preview {
1240
- font-size: 11px; color: #aaa; line-height: 1.4; text-align: center;
1241
- max-height: 60px; overflow-y: auto; word-break: break-word;
1303
+ font-size: 12px; color: #bbb; line-height: 1.4; text-align: center;
1304
+ /* Большой max-height на нодах побольше виден весь промпт; на маленьких
1305
+ — обрезается со скроллом. Раньше было 60px (~3 строки) — мало. */
1306
+ max-height: 240px; overflow-y: auto; word-break: break-word;
1307
+ width: 100%;
1308
+ }
1309
+ /* Промпт под filename аудио-ноды (постоянно — даже после генерации).
1310
+ Для TTS юзеру важно видеть текст без открытия Settings-modal'я. */
1311
+ .node-body .audio-prompt {
1312
+ font-size: 11px; color: #aaa; line-height: 1.4;
1313
+ margin: 4px 0 6px;
1314
+ max-height: 80px; overflow-y: auto; word-break: break-word;
1315
+ background: #1a1a1a; padding: 6px 8px; border-radius: 4px;
1316
+ border: 1px solid #2a2a2a;
1242
1317
  }
1243
1318
  .gen-pending .state-text { font-size: 11px; color: #888; }
1244
1319
  .gen-error {
@@ -1248,15 +1323,20 @@
1248
1323
  }
1249
1324
  .gen-error button { margin-top: 8px; }
1250
1325
 
1251
- .empty {
1326
+ /* Эта empty-overlay только для #emptyState (заглушка на пустом холсте).
1327
+ Раньше селектор был `.empty` (глобально) — он ловил, например,
1328
+ `.balance-info.empty` (модификатор «баланс=0») и делал её
1329
+ position:absolute; inset:0 — pill растекалась во весь viewport.
1330
+ Теперь специфично через id, конфликта нет. */
1331
+ #emptyState {
1252
1332
  position: absolute; inset: 0;
1253
1333
  display: flex; align-items: center; justify-content: center;
1254
1334
  color: #666; font-size: 15px; flex-direction: column; gap: 14px;
1255
1335
  text-align: center; padding: 24px; pointer-events: none;
1256
1336
  }
1257
- .empty h3 { color: #aaa; font-size: 18px; }
1258
- .empty p { max-width: 420px; line-height: 1.6; }
1259
- .empty.hidden { display: none; }
1337
+ #emptyState h3 { color: #aaa; font-size: 18px; }
1338
+ #emptyState p { max-width: 420px; line-height: 1.6; }
1339
+ #emptyState.hidden { display: none; }
1260
1340
 
1261
1341
  .unsupported { margin: auto; max-width: 540px; padding: 32px; background: #2a2a2a; border-radius: 12px; border: 1px solid #444; }
1262
1342
  .unsupported h1 { margin-bottom: 12px; }