kingkont 0.7.81 → 0.7.83

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.
@@ -0,0 +1,19 @@
1
+ # Bundled fonts
2
+
3
+ All fonts in this directory are licensed under the SIL Open Font License v1.1
4
+ (<https://openfontlicense.org/>) and bundled here so KingKont can render
5
+ beautiful Cyrillic-aware label nodes without needing a network connection.
6
+
7
+ | Font | Author / Foundry | Source |
8
+ |---------------|----------------------------------------|--------------------------------------------------|
9
+ | Caveat | Pablo Impallari | <https://fonts.google.com/specimen/Caveat> |
10
+ | Bad Script | Roman Shchyukin (Gaslight) | <https://fonts.google.com/specimen/Bad+Script> |
11
+ | Pacifico | Vernon Adams | <https://fonts.google.com/specimen/Pacifico> |
12
+ | Lobster | Pablo Impallari | <https://fonts.google.com/specimen/Lobster> |
13
+ | Yeseva One | Jovanny Lemonad | <https://fonts.google.com/specimen/Yeseva+One> |
14
+ | Comfortaa | Johan Aakerlund | <https://fonts.google.com/specimen/Comfortaa> |
15
+ | PT Serif | ParaType | <https://fonts.google.com/specimen/PT+Serif> |
16
+ | PT Mono | ParaType | <https://fonts.google.com/specimen/PT+Mono> |
17
+
18
+ Files were downloaded from the @fontsource jsDelivr mirror in the
19
+ `cyrillic-400-normal` and `latin-400-normal` subsets.
package/index.html CHANGED
@@ -227,6 +227,7 @@
227
227
  <!-- ===== Меню «что добавить» (двойной клик на пустом месте холста) ===== -->
228
228
  <div class="add-menu hidden" id="addMenu">
229
229
  <button data-act="text">✏️ Написать</button>
230
+ <button data-act="label">🅰 Подпись</button>
230
231
  <button data-act="gen-text">📝 Сгенерировать текст</button>
231
232
  <button data-act="audio">🎙 Сгенерировать голос</button>
232
233
  <button data-act="image">🖼 Сгенерировать картинку</button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.81",
3
+ "version": "0.7.83",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -1634,6 +1634,7 @@ async function createNodeEl(node) {
1634
1634
  const el = document.createElement('div');
1635
1635
  el.className = 'node';
1636
1636
  if (node.type === 'text') el.classList.add('text-node');
1637
+ if (node.type === 'label') el.classList.add('label-node');
1637
1638
  el.dataset.id = node.id;
1638
1639
  el.style.left = node.x + 'px';
1639
1640
  el.style.top = node.y + 'px';
@@ -1693,7 +1694,7 @@ async function createNodeEl(node) {
1693
1694
  attachDrag(el, node);
1694
1695
 
1695
1696
  el.addEventListener('dblclick', e => {
1696
- if (e.target.closest('textarea, input, video, button, .delete, .anchor, .resize-handle')) return;
1697
+ if (e.target.closest('textarea, input, video, button, .delete, .anchor, .resize-handle, [contenteditable]')) return;
1697
1698
  if (node.type === 'audio' && node.file) {
1698
1699
  regenerateNode(node);
1699
1700
  return;
@@ -1705,7 +1706,7 @@ async function createNodeEl(node) {
1705
1706
  if (node.generated) showNodeSettings(node);
1706
1707
  });
1707
1708
  el.addEventListener('contextmenu', e => {
1708
- if (e.target.closest('textarea, input, .anchor, .resize-handle')) return;
1709
+ if (e.target.closest('textarea, input, .anchor, .resize-handle, [contenteditable]')) return;
1709
1710
  e.preventDefault();
1710
1711
  e.stopPropagation();
1711
1712
  showNodeContextMenu(node, e.clientX, e.clientY);
@@ -153,6 +153,11 @@ document.querySelectorAll('#addMenu button').forEach(btn => {
153
153
  state.addMenuPos = null;
154
154
  return;
155
155
  }
156
+ if (act === 'label') {
157
+ await addLabelAt(state.addMenuPos);
158
+ state.addMenuPos = null;
159
+ return;
160
+ }
156
161
  if (act === 'gen-text') {
157
162
  // открываем text-gen modal; если есть fromNode — подставляем @ref в промпт
158
163
  if (!await ensureApiKey('text')) return;
@@ -194,6 +199,42 @@ async function addTextAt(pos) {
194
199
  scheduleSave();
195
200
  }
196
201
 
202
+ // Label-нода: лёгкая надпись поверх холста, без файла. Текст хранится
203
+ // прямо в scene.json (см. saveBoardMetadata: для type='label' n.text НЕ
204
+ // удаляется при сериализации).
205
+ async function addLabelAt(pos) {
206
+ if (!state.currentBoard) return;
207
+ const rect = canvas.getBoundingClientRect();
208
+ const x = pos ? (pos.clientX - rect.left) / state.zoom : canvasWrap.scrollLeft / state.zoom + 80;
209
+ const y = pos ? (pos.clientY - rect.top) / state.zoom : canvasWrap.scrollTop / state.zoom + 80;
210
+ const node = {
211
+ id: crypto.randomUUID(),
212
+ type: 'label',
213
+ text: '',
214
+ textStyle: { fontSize: 32, italic: false, fontFamily: 'handwritten' },
215
+ x, y,
216
+ width: 260, height: 90,
217
+ };
218
+ state.currentBoard.metadata.nodes.push(node);
219
+ const el = await createNodeEl(node);
220
+ canvas.appendChild(el);
221
+ scheduleSave();
222
+ // Авто-фокус в редактируемый текст, чтобы юзер сразу начал печатать.
223
+ setTimeout(() => {
224
+ const ed = el.querySelector('.label-text');
225
+ if (ed) {
226
+ ed.focus();
227
+ // Курсор в конец (если контент пустой — это всё равно начало).
228
+ const sel = window.getSelection();
229
+ const r = document.createRange();
230
+ r.selectNodeContents(ed);
231
+ r.collapse(false);
232
+ sel?.removeAllRanges();
233
+ sel?.addRange(r);
234
+ }
235
+ }, 30);
236
+ }
237
+
197
238
  // =================== Generate (фоновая, не блокирующая) ===================
198
239
  // === Preferences modal (API-ключи) ===
199
240
  async function openSettings(initialFocus) {
@@ -52,76 +52,76 @@ $('settingsRegen').addEventListener('click', () => {
52
52
  regenerateNode(node); // там редактируются ВСЕ поля (промпт, модель, голос)
53
53
  });
54
54
 
55
- // =================== Text-node body (с форматированием) ===================
56
- // Шрифтыsystem-only, чтобы не тащить Google Fonts CDN (приложение
57
- // должно работать офлайн). На macOS все четыре варианта выглядят отлично;
58
- // на Windows/Linux fallback'и держат смысл (cursive/fantasy/serif/mono).
59
- const TEXT_NODE_FONTS = [
60
- { id: 'default', label: 'Обычный' },
61
- { id: 'handwritten', label: 'Карандашом' },
62
- { id: 'marker', label: 'Краской' },
63
- { id: 'serif', label: засечками' },
64
- { id: 'mono', label: 'Моноширинный' },
55
+ // =================== Label-node body (свободный текст-аннотация) ===================
56
+ // Label-нодаэто просто текст «поверх» холста: без фона, без рамки,
57
+ // без header/footer. Используется для подписей, заголовков, заметок
58
+ // «карандашом/краской» в отличие от text-ноды, которая хранит длинный
59
+ // .md-документ с обычным textarea.
60
+ //
61
+ // Шрифты — system-only, без Google Fonts CDN (приложение офлайновое).
62
+ // На macOS все варианты выглядят естественно; на Win/Linux fallback'и
63
+ // (cursive/fantasy/serif/monospace) сохраняют смысл.
64
+ // id хранится в node.textStyle.fontFamily, поэтому переименовывать существующие
65
+ // id (`handwritten` / `marker` / `serif` / `mono`) нельзя — это сломает
66
+ // сохранённые сцены. Новые красивые шрифты добавляем под новыми id.
67
+ const LABEL_FONTS = [
68
+ { id: 'default', label: 'Обычный' },
69
+ { id: 'handwritten', label: 'Карандашом' }, // Caveat
70
+ { id: 'brush', label: 'Скорописью' }, // Bad Script
71
+ { id: 'marker', label: 'Кистью' }, // Pacifico
72
+ { id: 'display', label: 'Декор' }, // Lobster
73
+ { id: 'elegant', label: 'Элегант' }, // Yeseva One
74
+ { id: 'rounded', label: 'Округлый' }, // Comfortaa
75
+ { id: 'serif', label: 'С засечками' }, // PT Serif
76
+ { id: 'mono', label: 'Моноширинный' }, // PT Mono
65
77
  ];
66
- const TEXT_NODE_FONT_SIZES = [12, 14, 18, 24, 32, 48, 64];
67
- const TEXT_NODE_DEFAULT_STYLE = { fontSize: 14, italic: false, fontFamily: 'default' };
68
-
69
- function getTextNodeStyle(node) {
70
- return { ...TEXT_NODE_DEFAULT_STYLE, ...(node.textStyle || {}) };
71
- }
78
+ const LABEL_FONT_SIZES = [12, 14, 18, 24, 32, 48, 64, 96];
79
+ const LABEL_DEFAULT_STYLE = { fontSize: 32, italic: false, fontFamily: 'handwritten' };
72
80
 
73
- function applyTextNodeStyle(ta, style) {
74
- ta.style.fontSize = (style.fontSize || 14) + 'px';
75
- ta.style.fontStyle = style.italic ? 'italic' : 'normal';
76
- ta.dataset.font = style.fontFamily || 'default';
81
+ function applyLabelStyle(ed, style) {
82
+ ed.style.fontSize = (style.fontSize || 32) + 'px';
83
+ ed.style.fontStyle = style.italic ? 'italic' : 'normal';
84
+ ed.dataset.font = style.fontFamily || 'default';
77
85
  }
78
86
 
79
- function renderTextNodeBody(node, body) {
80
- // Гарантируем что у ноды есть textStyle (без spread реальный объект).
81
- if (!node.textStyle) node.textStyle = { ...TEXT_NODE_DEFAULT_STYLE };
87
+ function renderLabelNodeBody(node, body) {
88
+ if (!node.textStyle) node.textStyle = { ...LABEL_DEFAULT_STYLE };
82
89
 
83
- // Тулбар форматирования: шрифт, размер, italic.
90
+ // Тулбар форматирования (на hover, чтобы не отвлекать от чистого текста).
84
91
  const tb = document.createElement('div');
85
- tb.className = 'text-toolbar';
86
- // Не блокируем drag-mousedown по тулбару — но клики по контролам
87
- // не должны таскать ноду. stopPropagation на mousedown решает это.
92
+ tb.className = 'label-toolbar';
88
93
  tb.addEventListener('mousedown', e => e.stopPropagation());
89
94
 
90
- // Селект шрифта.
91
95
  const fontSel = document.createElement('select');
92
96
  fontSel.title = 'Шрифт';
93
- for (const f of TEXT_NODE_FONTS) {
97
+ for (const f of LABEL_FONTS) {
94
98
  const o = document.createElement('option');
95
- o.value = f.id;
96
- o.textContent = f.label;
99
+ o.value = f.id; o.textContent = f.label;
97
100
  fontSel.appendChild(o);
98
101
  }
99
102
  fontSel.value = node.textStyle.fontFamily || 'default';
100
103
  fontSel.addEventListener('change', () => {
101
104
  node.textStyle.fontFamily = fontSel.value;
102
- applyTextNodeStyle(ta, node.textStyle);
105
+ applyLabelStyle(ed, node.textStyle);
103
106
  scheduleSave();
104
107
  });
105
108
  tb.appendChild(fontSel);
106
109
 
107
- // Селект размера.
108
110
  const sizeSel = document.createElement('select');
109
111
  sizeSel.title = 'Размер шрифта';
110
- for (const s of TEXT_NODE_FONT_SIZES) {
112
+ for (const s of LABEL_FONT_SIZES) {
111
113
  const o = document.createElement('option');
112
- o.value = String(s);
113
- o.textContent = s + 'px';
114
+ o.value = String(s); o.textContent = s + 'px';
114
115
  sizeSel.appendChild(o);
115
116
  }
116
- sizeSel.value = String(node.textStyle.fontSize || 14);
117
+ sizeSel.value = String(node.textStyle.fontSize || 32);
117
118
  sizeSel.addEventListener('change', () => {
118
- node.textStyle.fontSize = parseInt(sizeSel.value, 10) || 14;
119
- applyTextNodeStyle(ta, node.textStyle);
119
+ node.textStyle.fontSize = parseInt(sizeSel.value, 10) || 32;
120
+ applyLabelStyle(ed, node.textStyle);
120
121
  scheduleSave();
121
122
  });
122
123
  tb.appendChild(sizeSel);
123
124
 
124
- // Italic toggle.
125
125
  const italicBtn = document.createElement('button');
126
126
  italicBtn.className = 'tt-italic';
127
127
  italicBtn.textContent = 'I';
@@ -131,20 +131,30 @@ function renderTextNodeBody(node, body) {
131
131
  e.stopPropagation();
132
132
  node.textStyle.italic = !node.textStyle.italic;
133
133
  italicBtn.classList.toggle('active', node.textStyle.italic);
134
- applyTextNodeStyle(ta, node.textStyle);
134
+ applyLabelStyle(ed, node.textStyle);
135
135
  scheduleSave();
136
136
  });
137
137
  tb.appendChild(italicBtn);
138
138
 
139
139
  body.appendChild(tb);
140
140
 
141
- const ta = document.createElement('textarea');
142
- ta.value = node.text || '';
143
- ta.placeholder = 'Текст ноды...';
144
- applyTextNodeStyle(ta, node.textStyle);
145
- ta.addEventListener('input', () => { node.text = ta.value; scheduleSave(); });
146
- ta.addEventListener('mousedown', e => e.stopPropagation());
147
- body.appendChild(ta);
141
+ // Сам текст — contenteditable (без рамок, без скроллбаров, ровно как
142
+ // плавающая надпись на холсте). plaintext-only — режем форматирование
143
+ // из буфера обмена (текст копируется только как текст).
144
+ const ed = document.createElement('div');
145
+ ed.className = 'label-text';
146
+ ed.contentEditable = 'plaintext-only';
147
+ ed.spellcheck = false;
148
+ ed.dataset.placeholder = 'Текст…';
149
+ ed.textContent = node.text || '';
150
+ applyLabelStyle(ed, node.textStyle);
151
+ ed.addEventListener('input', () => { node.text = ed.textContent; scheduleSave(); });
152
+ ed.addEventListener('mousedown', e => e.stopPropagation());
153
+ // Enter — перенос строки. Esc — снять фокус.
154
+ ed.addEventListener('keydown', e => {
155
+ if (e.key === 'Escape') { e.preventDefault(); ed.blur(); }
156
+ });
157
+ body.appendChild(ed);
148
158
  }
149
159
 
150
160
  async function renderNodeBody(node, body) {
@@ -281,7 +291,14 @@ async function renderNodeBody(node, body) {
281
291
  }
282
292
 
283
293
  if (node.type === 'text') {
284
- renderTextNodeBody(node, body);
294
+ const ta = document.createElement('textarea');
295
+ ta.value = node.text || '';
296
+ ta.placeholder = 'Текст ноды...';
297
+ ta.addEventListener('input', () => { node.text = ta.value; scheduleSave(); });
298
+ ta.addEventListener('mousedown', e => e.stopPropagation());
299
+ body.appendChild(ta);
300
+ } else if (node.type === 'label') {
301
+ renderLabelNodeBody(node, body);
285
302
  } else if (['video','audio','image'].includes(node.type) && node.file) {
286
303
  const nodeEl = body.closest('.node');
287
304
  nodeEl?.classList.toggle('image-node', node.type === 'image');
package/renderer/state.js CHANGED
@@ -354,7 +354,9 @@ async function saveBoardMetadata(boardHandle, meta) {
354
354
  }
355
355
  const nodesForJson = meta.nodes.map(n => {
356
356
  const out = { ...n };
357
- delete out.text;
357
+ // Для text-нод текст в отдельном .md, в JSON его не пишем.
358
+ // Для label-нод — текст inline в JSON (короткие подписи, без файла).
359
+ if (n.type === 'text') delete out.text;
358
360
  return out;
359
361
  });
360
362
  const payload = {
@@ -1,3 +1,88 @@
1
+ /* === Bundled fonts (для label-нод, поддерживают кириллицу) ===
2
+ Все шрифты — OFL, лежат в assets/fonts/, см. NOTICE.md.
3
+ Для каждого семейства подключены два subset-файла (Latin + Cyrillic)
4
+ с unicode-range — браузер скачивает только тот блок, который нужен. */
5
+ @font-face {
6
+ font-family: 'KK Caveat'; font-style: normal; font-weight: 400; font-display: block;
7
+ src: url('../assets/fonts/caveat-cyrillic-400.woff2') format('woff2');
8
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
9
+ }
10
+ @font-face {
11
+ font-family: 'KK Caveat'; font-style: normal; font-weight: 400; font-display: block;
12
+ src: url('../assets/fonts/caveat-latin-400.woff2') format('woff2');
13
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
14
+ }
15
+ @font-face {
16
+ font-family: 'KK BadScript'; font-style: normal; font-weight: 400; font-display: block;
17
+ src: url('../assets/fonts/bad-script-cyrillic-400.woff2') format('woff2');
18
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
19
+ }
20
+ @font-face {
21
+ font-family: 'KK BadScript'; font-style: normal; font-weight: 400; font-display: block;
22
+ src: url('../assets/fonts/bad-script-latin-400.woff2') format('woff2');
23
+ unicode-range: U+0000-00FF;
24
+ }
25
+ @font-face {
26
+ font-family: 'KK Pacifico'; font-style: normal; font-weight: 400; font-display: block;
27
+ src: url('../assets/fonts/pacifico-cyrillic-400.woff2') format('woff2');
28
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
29
+ }
30
+ @font-face {
31
+ font-family: 'KK Pacifico'; font-style: normal; font-weight: 400; font-display: block;
32
+ src: url('../assets/fonts/pacifico-latin-400.woff2') format('woff2');
33
+ unicode-range: U+0000-00FF;
34
+ }
35
+ @font-face {
36
+ font-family: 'KK Lobster'; font-style: normal; font-weight: 400; font-display: block;
37
+ src: url('../assets/fonts/lobster-cyrillic-400.woff2') format('woff2');
38
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
39
+ }
40
+ @font-face {
41
+ font-family: 'KK Lobster'; font-style: normal; font-weight: 400; font-display: block;
42
+ src: url('../assets/fonts/lobster-latin-400.woff2') format('woff2');
43
+ unicode-range: U+0000-00FF;
44
+ }
45
+ @font-face {
46
+ font-family: 'KK YesevaOne'; font-style: normal; font-weight: 400; font-display: block;
47
+ src: url('../assets/fonts/yeseva-one-cyrillic-400.woff2') format('woff2');
48
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
49
+ }
50
+ @font-face {
51
+ font-family: 'KK YesevaOne'; font-style: normal; font-weight: 400; font-display: block;
52
+ src: url('../assets/fonts/yeseva-one-latin-400.woff2') format('woff2');
53
+ unicode-range: U+0000-00FF;
54
+ }
55
+ @font-face {
56
+ font-family: 'KK Comfortaa'; font-style: normal; font-weight: 400; font-display: block;
57
+ src: url('../assets/fonts/comfortaa-cyrillic-400.woff2') format('woff2');
58
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
59
+ }
60
+ @font-face {
61
+ font-family: 'KK Comfortaa'; font-style: normal; font-weight: 400; font-display: block;
62
+ src: url('../assets/fonts/comfortaa-latin-400.woff2') format('woff2');
63
+ unicode-range: U+0000-00FF;
64
+ }
65
+ @font-face {
66
+ font-family: 'KK PTSerif'; font-style: normal; font-weight: 400; font-display: block;
67
+ src: url('../assets/fonts/pt-serif-cyrillic-400.woff2') format('woff2');
68
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
69
+ }
70
+ @font-face {
71
+ font-family: 'KK PTSerif'; font-style: normal; font-weight: 400; font-display: block;
72
+ src: url('../assets/fonts/pt-serif-latin-400.woff2') format('woff2');
73
+ unicode-range: U+0000-00FF;
74
+ }
75
+ @font-face {
76
+ font-family: 'KK PTMono'; font-style: normal; font-weight: 400; font-display: block;
77
+ src: url('../assets/fonts/pt-mono-cyrillic-400.woff2') format('woff2');
78
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
79
+ }
80
+ @font-face {
81
+ font-family: 'KK PTMono'; font-style: normal; font-weight: 400; font-display: block;
82
+ src: url('../assets/fonts/pt-mono-latin-400.woff2') format('woff2');
83
+ unicode-range: U+0000-00FF;
84
+ }
85
+
1
86
  * { box-sizing: border-box; margin: 0; padding: 0; }
2
87
  html, body { height: 100%; }
3
88
  body {
@@ -323,56 +408,119 @@
323
408
  box-shadow: 0 0 0 2px #5aa8ff, 0 8px 24px rgba(90,168,255,0.4);
324
409
  }
325
410
  .node-body { flex: 1; min-height: 0; }
326
- .node.text-node .node-body { padding: 0; display: flex; flex-direction: column; }
411
+ .node.text-node .node-body { padding: 0; display: flex; }
327
412
  .node.text-node .node-body textarea {
328
- width: 100%; resize: none; min-height: 80px;
413
+ width: 100%; height: 100%; resize: none; min-height: 80px;
329
414
  border: none; border-radius: 0; padding: 12px; flex: 1;
330
- background: transparent; color: #eaeaea;
331
- font-size: 14px; font-style: normal;
332
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
333
- line-height: 1.4;
334
- }
335
- /* Декоративные шрифты для текстовых нод. */
336
- .node.text-node .node-body textarea[data-font="handwritten"] {
337
- /* «Карандашом» — рукописные шрифты, мягкие, наклонные. */
338
- font-family: 'Caveat', 'Marker Felt', 'Bradley Hand', 'Snell Roundhand', cursive;
339
- line-height: 1.2;
340
- }
341
- .node.text-node .node-body textarea[data-font="marker"] {
342
- /* «Краской/маркером» — крупные мазки, plakat-стиль. */
343
- font-family: 'Permanent Marker', 'Chalkduster', 'Marker Felt', 'Comic Sans MS', fantasy;
344
- letter-spacing: 0.02em;
345
415
  }
346
- .node.text-node .node-body textarea[data-font="serif"] {
347
- font-family: Georgia, 'Times New Roman', serif;
348
- }
349
- .node.text-node .node-body textarea[data-font="mono"] {
350
- font-family: ui-monospace, 'SF Mono', Menlo, 'Courier New', monospace;
416
+
417
+ /* === Label-node плавающая текст-аннотация поверх холста ===
418
+ Без фона / рамки / тени — выглядит как «написанный» прямо на сцене текст.
419
+ Header / footer / resize / anchor скрыты по умолчанию, появляются на
420
+ hover чтобы можно было перетаскивать, удалять, ресайзить. */
421
+ .node.label-node {
422
+ background: transparent;
423
+ border: none; box-shadow: none;
424
+ padding: 0;
425
+ }
426
+ .node.label-node.selected {
427
+ outline: 1px dashed rgba(90,168,255,0.8);
428
+ outline-offset: 2px;
429
+ }
430
+ .node.label-node .node-header,
431
+ .node.label-node .node-footer,
432
+ .node.label-node .resize-handle,
433
+ .node.label-node .anchor,
434
+ .node.label-node .label-toolbar {
435
+ opacity: 0; transition: opacity 0.15s;
436
+ }
437
+ .node.label-node:hover .node-header,
438
+ .node.label-node:hover .node-footer,
439
+ .node.label-node:hover .resize-handle,
440
+ .node.label-node:hover .anchor,
441
+ .node.label-node:hover .label-toolbar,
442
+ .node.label-node:focus-within .label-toolbar { opacity: 1; }
443
+ .node.label-node .node-header {
444
+ background: rgba(20,20,20,0.85);
445
+ border-radius: 6px 6px 0 0;
446
+ }
447
+ .node.label-node .node-body {
448
+ padding: 0; background: transparent;
449
+ display: flex; flex-direction: column; min-height: 0;
351
450
  }
352
451
 
353
- /* Тулбар форматирования — компактный, появляется на hover, чтобы не
354
- отвлекать когда юзер просто читает. */
355
- .node.text-node .text-toolbar {
452
+ /* Тулбар форматирования label поверх текста, на hover. */
453
+ .node.label-node .label-toolbar {
356
454
  display: flex; align-items: center; gap: 4px;
357
- padding: 4px 6px; background: #181818; border-bottom: 1px solid #2a2a2a;
358
- font-size: 11px; opacity: 0.35; transition: opacity 0.15s;
359
- flex-shrink: 0;
360
- }
361
- .node.text-node:hover .text-toolbar,
362
- .node.text-node .text-toolbar:focus-within { opacity: 1; }
363
- .node.text-node .text-toolbar select,
364
- .node.text-node .text-toolbar button {
455
+ padding: 3px 6px; background: rgba(20,20,20,0.92);
456
+ border-radius: 4px; margin: 4px;
457
+ font-size: 11px; flex-shrink: 0;
458
+ align-self: flex-start;
459
+ pointer-events: auto;
460
+ }
461
+ .node.label-node .label-toolbar select,
462
+ .node.label-node .label-toolbar button {
365
463
  background: #1e1e1e; border: 1px solid #383838; color: #ccc;
366
464
  border-radius: 3px; padding: 2px 6px; font-size: 11px;
367
465
  cursor: pointer; line-height: 1.2;
368
466
  }
369
- .node.text-node .text-toolbar select:hover,
370
- .node.text-node .text-toolbar button:hover { background: #2c2c2c; color: #fff; }
371
- .node.text-node .text-toolbar button.active {
467
+ .node.label-node .label-toolbar select:hover,
468
+ .node.label-node .label-toolbar button:hover { background: #2c2c2c; color: #fff; }
469
+ .node.label-node .label-toolbar button.active {
372
470
  background: #3a5a8a; color: #fff; border-color: #4a6a9a;
373
471
  }
374
- .node.text-node .text-toolbar .tt-spacer { flex: 1; }
375
- .node.text-node .text-toolbar .tt-italic { font-style: italic; min-width: 22px; }
472
+ .node.label-node .label-toolbar .tt-italic { font-style: italic; min-width: 22px; }
473
+
474
+ /* Сам текст. flex:1 — занимает оставшееся тело, поэтому drag-area
475
+ совпадает с визуальной областью. */
476
+ .node.label-node .label-text {
477
+ flex: 1; padding: 8px 12px;
478
+ color: #eaeaea; line-height: 1.2;
479
+ word-break: break-word; white-space: pre-wrap;
480
+ outline: none; cursor: text;
481
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
482
+ text-shadow: 0 1px 2px rgba(0,0,0,0.6);
483
+ }
484
+ .node.label-node .label-text:empty::before {
485
+ content: attr(data-placeholder);
486
+ color: rgba(200,200,200,0.35);
487
+ }
488
+ /* === Label-шрифты ===
489
+ Все варианты — bundled woff2 (см. assets/fonts/), кириллица поддержана. */
490
+ .node.label-node .label-text[data-font="handwritten"] {
491
+ /* Карандашом — мягкий рукописный (Caveat). */
492
+ font-family: 'KK Caveat', 'Caveat', 'Marker Felt', cursive;
493
+ line-height: 1.1;
494
+ }
495
+ .node.label-node .label-text[data-font="brush"] {
496
+ /* Скорописью — каллиграфия в русском стиле (Bad Script). */
497
+ font-family: 'KK BadScript', 'Bad Script', 'Snell Roundhand', cursive;
498
+ }
499
+ .node.label-node .label-text[data-font="marker"] {
500
+ /* Кистью — закруглённый плакатный шрифт (Pacifico). */
501
+ font-family: 'KK Pacifico', 'Pacifico', 'Marker Felt', cursive;
502
+ letter-spacing: 0.01em;
503
+ }
504
+ .node.label-node .label-text[data-font="display"] {
505
+ /* Декор — крупная декоративная подпись (Lobster). */
506
+ font-family: 'KK Lobster', 'Lobster', Georgia, serif;
507
+ }
508
+ .node.label-node .label-text[data-font="elegant"] {
509
+ /* Элегант — выразительный заголовочный serif (Yeseva One). */
510
+ font-family: 'KK YesevaOne', 'Yeseva One', Georgia, serif;
511
+ }
512
+ .node.label-node .label-text[data-font="rounded"] {
513
+ /* Округлый — мягкий sans (Comfortaa). */
514
+ font-family: 'KK Comfortaa', 'Comfortaa', -apple-system, sans-serif;
515
+ }
516
+ .node.label-node .label-text[data-font="serif"] {
517
+ /* С засечками — академический serif (PT Serif). */
518
+ font-family: 'KK PTSerif', 'PT Serif', Georgia, serif;
519
+ }
520
+ .node.label-node .label-text[data-font="mono"] {
521
+ /* Моноширинный (PT Mono). */
522
+ font-family: 'KK PTMono', 'PT Mono', ui-monospace, Menlo, monospace;
523
+ }
376
524
  .node.video-node .node-body { padding: 0; overflow: hidden; border-radius: 0 0 8px 8px; }
377
525
  .node.video-node .node-body video { width: 100%; height: auto; max-height: none; background: transparent; border-radius: 0; display: block; }
378
526
  .audio-speed { display: flex; gap: 2px; margin-top: 6px; }