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.
- package/assets/fonts/NOTICE.md +19 -0
- package/assets/fonts/bad-script-cyrillic-400.woff2 +0 -0
- package/assets/fonts/bad-script-latin-400.woff2 +0 -0
- package/assets/fonts/caveat-cyrillic-400.woff2 +0 -0
- package/assets/fonts/caveat-latin-400.woff2 +0 -0
- package/assets/fonts/comfortaa-cyrillic-400.woff2 +0 -0
- package/assets/fonts/comfortaa-latin-400.woff2 +0 -0
- package/assets/fonts/lobster-cyrillic-400.woff2 +0 -0
- package/assets/fonts/lobster-latin-400.woff2 +0 -0
- package/assets/fonts/pacifico-cyrillic-400.woff2 +0 -0
- package/assets/fonts/pacifico-latin-400.woff2 +0 -0
- package/assets/fonts/pt-mono-cyrillic-400.woff2 +0 -0
- package/assets/fonts/pt-mono-latin-400.woff2 +0 -0
- package/assets/fonts/pt-serif-cyrillic-400.woff2 +0 -0
- package/assets/fonts/pt-serif-latin-400.woff2 +0 -0
- package/assets/fonts/yeseva-one-cyrillic-400.woff2 +0 -0
- package/assets/fonts/yeseva-one-latin-400.woff2 +0 -0
- package/index.html +1 -0
- package/package.json +1 -1
- package/renderer/board.js +3 -2
- package/renderer/generate.js +41 -0
- package/renderer/settings.js +66 -49
- package/renderer/state.js +3 -1
- package/renderer/styles.css +186 -38
|
@@ -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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
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);
|
package/renderer/generate.js
CHANGED
|
@@ -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) {
|
package/renderer/settings.js
CHANGED
|
@@ -52,76 +52,76 @@ $('settingsRegen').addEventListener('click', () => {
|
|
|
52
52
|
regenerateNode(node); // там редактируются ВСЕ поля (промпт, модель, голос)
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
// ===================
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
67
|
-
const
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
80
|
-
|
|
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
|
-
// Тулбар
|
|
90
|
+
// Тулбар форматирования (на hover, чтобы не отвлекать от чистого текста).
|
|
84
91
|
const tb = document.createElement('div');
|
|
85
|
-
tb.className = '
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
117
|
+
sizeSel.value = String(node.textStyle.fontSize || 32);
|
|
117
118
|
sizeSel.addEventListener('change', () => {
|
|
118
|
-
node.textStyle.fontSize = parseInt(sizeSel.value, 10) ||
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
package/renderer/styles.css
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
/* Тулбар форматирования —
|
|
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:
|
|
358
|
-
|
|
359
|
-
flex-shrink: 0;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
.node.
|
|
364
|
-
.node.
|
|
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.
|
|
370
|
-
.node.
|
|
371
|
-
.node.
|
|
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.
|
|
375
|
-
|
|
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; }
|