kingkont 0.7.85 → 0.7.87
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 +1 -1
- package/renderer/board.js +70 -5
- package/renderer/settings.js +75 -60
- package/renderer/styles.css +16 -31
package/package.json
CHANGED
package/renderer/board.js
CHANGED
|
@@ -1111,25 +1111,35 @@ function askName(title, placeholder = '', initialValue = '', opts = {}) {
|
|
|
1111
1111
|
// Возвращает выбранную строку или null если юзер закрыл.
|
|
1112
1112
|
// window.prompt() в Electron renderer молча возвращает null — поэтому
|
|
1113
1113
|
// нельзя использовать его для select-подобных диалогов.
|
|
1114
|
-
|
|
1114
|
+
// askChoice(title, options, currentValue, opts?)
|
|
1115
|
+
// opts:
|
|
1116
|
+
// vertical: bool — стэкаем кнопки вертикально (по умолчанию — flex-wrap)
|
|
1117
|
+
// optionStyles: { [opt]: 'css-string' } — кастомный CSS на кнопку (для preview-эффектов,
|
|
1118
|
+
// напр. рендер опции «Карандашом» в самом шрифте Karandashom)
|
|
1119
|
+
function askChoice(title, options, currentValue, opts = {}) {
|
|
1115
1120
|
return new Promise(resolve => {
|
|
1116
1121
|
const overlay = document.createElement('div');
|
|
1117
1122
|
overlay.className = 'modal';
|
|
1118
1123
|
overlay.style.cssText = 'position:fixed; inset:0; background:rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center; z-index:9999;';
|
|
1119
1124
|
const box = document.createElement('div');
|
|
1120
|
-
box.style.cssText = 'background:#222; border:1px solid #444; border-radius:8px; padding:18px 20px; min-width:360px; box-shadow:0 8px 32px rgba(0,0,0,0.6);';
|
|
1125
|
+
box.style.cssText = 'background:#222; border:1px solid #444; border-radius:8px; padding:18px 20px; min-width:360px; max-height:80vh; overflow-y:auto; box-shadow:0 8px 32px rgba(0,0,0,0.6);';
|
|
1121
1126
|
const h = document.createElement('h3');
|
|
1122
1127
|
h.textContent = title;
|
|
1123
1128
|
h.style.cssText = 'margin:0 0 12px; font-size:14px; color:#e0e0e0;';
|
|
1124
1129
|
box.append(h);
|
|
1125
1130
|
const grid = document.createElement('div');
|
|
1126
|
-
grid.style.cssText =
|
|
1131
|
+
grid.style.cssText = opts.vertical
|
|
1132
|
+
? 'display:flex; flex-direction:column; gap:6px; margin-bottom:14px;'
|
|
1133
|
+
: 'display:flex; flex-wrap:wrap; gap:6px; margin-bottom:14px;';
|
|
1127
1134
|
const close = (val) => { overlay.remove(); resolve(val); };
|
|
1128
1135
|
for (const opt of options) {
|
|
1129
1136
|
const b = document.createElement('button');
|
|
1130
1137
|
b.textContent = opt;
|
|
1131
|
-
|
|
1132
|
-
|
|
1138
|
+
let css = 'padding:6px 12px; background:#1a1a1a; color:#e0e0e0; border:1px solid #444; border-radius:4px; font-size:13px; cursor:pointer;';
|
|
1139
|
+
if (opts.vertical) css += 'text-align:left;';
|
|
1140
|
+
if (opt === currentValue) css += 'border-color:#7c3aed; background:#2a1a3a;';
|
|
1141
|
+
if (opts.optionStyles?.[opt]) css += opts.optionStyles[opt];
|
|
1142
|
+
b.style.cssText = css;
|
|
1133
1143
|
b.addEventListener('click', () => close(opt));
|
|
1134
1144
|
grid.append(b);
|
|
1135
1145
|
}
|
|
@@ -1773,6 +1783,61 @@ function showNodeContextMenu(node, clientX, clientY) {
|
|
|
1773
1783
|
menu.appendChild(b);
|
|
1774
1784
|
};
|
|
1775
1785
|
add(node.name ? `✏ Переименовать (${node.name})` : '✏ Переименовать', () => renameNode(node));
|
|
1786
|
+
// Label: пункты для смены стиля (шрифт / размер / курсив).
|
|
1787
|
+
// Сделано через ПКМ а не постоянный hover-тулбар, чтобы UI label-ноды
|
|
1788
|
+
// оставался «чистым текстом» без фурнитуры.
|
|
1789
|
+
if (node.type === 'label') {
|
|
1790
|
+
if (!node.textStyle) node.textStyle = { fontSize: 32, italic: false, fontFamily: 'pencil' };
|
|
1791
|
+
const ts = node.textStyle;
|
|
1792
|
+
const curFont = LABEL_FONTS.find(f => f.id === ts.fontFamily) || LABEL_FONTS[0];
|
|
1793
|
+
add(`🔤 Шрифт: ${curFont.label}`, async () => {
|
|
1794
|
+
// Каждая опция в picker'е рендерится в собственно её шрифте — юзер
|
|
1795
|
+
// сразу видит как «Карандашом» / «Прописью» / «Кистью» выглядят.
|
|
1796
|
+
const fontStyles = {};
|
|
1797
|
+
for (const f of LABEL_FONTS) {
|
|
1798
|
+
fontStyles[f.label] = `font-family: ${getLabelFontFamily(f.id)}; font-size: 22px; padding: 8px 14px; line-height: 1.3;`;
|
|
1799
|
+
}
|
|
1800
|
+
const choice = await askChoice(
|
|
1801
|
+
'Шрифт label-ноды',
|
|
1802
|
+
LABEL_FONTS.map(f => f.label),
|
|
1803
|
+
curFont.label,
|
|
1804
|
+
{ vertical: true, optionStyles: fontStyles },
|
|
1805
|
+
);
|
|
1806
|
+
if (!choice) return;
|
|
1807
|
+
const picked = LABEL_FONTS.find(f => f.label === choice);
|
|
1808
|
+
if (!picked) return;
|
|
1809
|
+
ts.fontFamily = picked.id;
|
|
1810
|
+
scheduleSave();
|
|
1811
|
+
await refreshNodeDOM(node.id);
|
|
1812
|
+
});
|
|
1813
|
+
add(`📏 Размер: ${ts.fontSize || 32}px`, async () => {
|
|
1814
|
+
// Каждый размер в picker'е рендерится в реальном размере (с лимитом),
|
|
1815
|
+
// чтобы юзер чувствовал «насколько крупно». Также вертикально.
|
|
1816
|
+
const sizeStyles = {};
|
|
1817
|
+
for (const s of LABEL_FONT_SIZES) {
|
|
1818
|
+
const previewSize = Math.min(s, 36); // не растим окно — лимит превью
|
|
1819
|
+
sizeStyles[s + 'px'] = `font-size: ${previewSize}px; line-height: 1.2; padding: 6px 14px;`;
|
|
1820
|
+
}
|
|
1821
|
+
const choice = await askChoice(
|
|
1822
|
+
'Размер шрифта',
|
|
1823
|
+
LABEL_FONT_SIZES.map(s => s + 'px'),
|
|
1824
|
+
(ts.fontSize || 32) + 'px',
|
|
1825
|
+
{ vertical: true, optionStyles: sizeStyles },
|
|
1826
|
+
);
|
|
1827
|
+
if (!choice) return;
|
|
1828
|
+
const n = parseInt(choice, 10);
|
|
1829
|
+
if (Number.isFinite(n)) {
|
|
1830
|
+
ts.fontSize = n;
|
|
1831
|
+
scheduleSave();
|
|
1832
|
+
await refreshNodeDOM(node.id);
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
add(ts.italic ? '𝐼 Курсив: вкл' : '𝐼 Курсив: выкл', async () => {
|
|
1836
|
+
ts.italic = !ts.italic;
|
|
1837
|
+
scheduleSave();
|
|
1838
|
+
await refreshNodeDOM(node.id);
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1776
1841
|
add('📋 Логи', () => showNodeLogs(node));
|
|
1777
1842
|
if (node.generated) add('⚙ Параметры', () => showNodeSettings(node));
|
|
1778
1843
|
if (node.status === 'generating') {
|
package/renderer/settings.js
CHANGED
|
@@ -85,76 +85,80 @@ function applyLabelStyle(ed, style) {
|
|
|
85
85
|
ed.dataset.font = style.fontFamily || 'default';
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Соответствие label-font-id → CSS font-family stack. Используется для
|
|
89
|
+
// preview-кнопок в picker'e шрифта (askChoice показывает каждую опцию
|
|
90
|
+
// в собственно её шрифте, чтобы юзер видел как выглядит). Семейства
|
|
91
|
+
// должны совпадать с теми, что заданы в .label-text[data-font="..."]
|
|
92
|
+
// в renderer/styles.css.
|
|
93
|
+
function getLabelFontFamily(id) {
|
|
94
|
+
const map = {
|
|
95
|
+
default: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
96
|
+
pencil: "'KK Neucha', 'Marker Felt', 'Comic Sans MS', cursive",
|
|
97
|
+
handwritten: "'KK Caveat', 'Caveat', 'Marker Felt', cursive",
|
|
98
|
+
brush: "'KK BadScript', 'Bad Script', 'Snell Roundhand', cursive",
|
|
99
|
+
marker: "'KK Pacifico', 'Pacifico', 'Marker Felt', cursive",
|
|
100
|
+
display: "'KK Lobster', 'Lobster', Georgia, serif",
|
|
101
|
+
elegant: "'KK YesevaOne', 'Yeseva One', Georgia, serif",
|
|
102
|
+
rounded: "'KK Comfortaa', 'Comfortaa', -apple-system, sans-serif",
|
|
103
|
+
serif: "'KK PTSerif', 'PT Serif', Georgia, serif",
|
|
104
|
+
mono: "'KK PTMono', 'PT Mono', ui-monospace, Menlo, monospace",
|
|
105
|
+
};
|
|
106
|
+
return map[id] || map.default;
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
function renderLabelNodeBody(node, body) {
|
|
89
110
|
if (!node.textStyle) node.textStyle = { ...LABEL_DEFAULT_STYLE };
|
|
90
111
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (const f of LABEL_FONTS) {
|
|
99
|
-
const o = document.createElement('option');
|
|
100
|
-
o.value = f.id; o.textContent = f.label;
|
|
101
|
-
fontSel.appendChild(o);
|
|
102
|
-
}
|
|
103
|
-
fontSel.value = node.textStyle.fontFamily || 'default';
|
|
104
|
-
fontSel.addEventListener('change', () => {
|
|
105
|
-
node.textStyle.fontFamily = fontSel.value;
|
|
106
|
-
applyLabelStyle(ed, node.textStyle);
|
|
107
|
-
scheduleSave();
|
|
108
|
-
});
|
|
109
|
-
tb.appendChild(fontSel);
|
|
110
|
-
|
|
111
|
-
const sizeSel = document.createElement('select');
|
|
112
|
-
sizeSel.title = 'Размер шрифта';
|
|
113
|
-
for (const s of LABEL_FONT_SIZES) {
|
|
114
|
-
const o = document.createElement('option');
|
|
115
|
-
o.value = String(s); o.textContent = s + 'px';
|
|
116
|
-
sizeSel.appendChild(o);
|
|
117
|
-
}
|
|
118
|
-
sizeSel.value = String(node.textStyle.fontSize || 32);
|
|
119
|
-
sizeSel.addEventListener('change', () => {
|
|
120
|
-
node.textStyle.fontSize = parseInt(sizeSel.value, 10) || 32;
|
|
121
|
-
applyLabelStyle(ed, node.textStyle);
|
|
122
|
-
scheduleSave();
|
|
123
|
-
});
|
|
124
|
-
tb.appendChild(sizeSel);
|
|
125
|
-
|
|
126
|
-
const italicBtn = document.createElement('button');
|
|
127
|
-
italicBtn.className = 'tt-italic';
|
|
128
|
-
italicBtn.textContent = 'I';
|
|
129
|
-
italicBtn.title = 'Курсив';
|
|
130
|
-
if (node.textStyle.italic) italicBtn.classList.add('active');
|
|
131
|
-
italicBtn.addEventListener('click', e => {
|
|
132
|
-
e.stopPropagation();
|
|
133
|
-
node.textStyle.italic = !node.textStyle.italic;
|
|
134
|
-
italicBtn.classList.toggle('active', node.textStyle.italic);
|
|
135
|
-
applyLabelStyle(ed, node.textStyle);
|
|
136
|
-
scheduleSave();
|
|
137
|
-
});
|
|
138
|
-
tb.appendChild(italicBtn);
|
|
139
|
-
|
|
140
|
-
body.appendChild(tb);
|
|
141
|
-
|
|
142
|
-
// Сам текст — contenteditable (без рамок, без скроллбаров, ровно как
|
|
143
|
-
// плавающая надпись на холсте). plaintext-only — режем форматирование
|
|
144
|
-
// из буфера обмена (текст копируется только как текст).
|
|
112
|
+
// Сам текст. По умолчанию НЕ contenteditable — обычный <div>. Это значит:
|
|
113
|
+
// • single-click = выделение ноды (drag handler из attachDrag)
|
|
114
|
+
// • ПКМ = node-context-menu (ниже добавлены пункты «Шрифт/Размер/Курсив»)
|
|
115
|
+
// • dblclick = вход в режим редактирования
|
|
116
|
+
// • blur = выход из редактирования
|
|
117
|
+
// Шрифт/размер/курсив меняются через ПКМ (а не через постоянный hover-тулбар),
|
|
118
|
+
// чтобы UI label-ноды был «чистый текст без фурнитуры».
|
|
145
119
|
const ed = document.createElement('div');
|
|
146
120
|
ed.className = 'label-text';
|
|
147
|
-
ed.contentEditable = 'plaintext-only';
|
|
148
121
|
ed.spellcheck = false;
|
|
149
|
-
ed.dataset.placeholder = '
|
|
122
|
+
ed.dataset.placeholder = 'Двойной клик — редактировать';
|
|
150
123
|
ed.textContent = node.text || '';
|
|
151
124
|
applyLabelStyle(ed, node.textStyle);
|
|
125
|
+
|
|
126
|
+
const enterEditMode = () => {
|
|
127
|
+
if (ed.isContentEditable) return;
|
|
128
|
+
ed.contentEditable = 'plaintext-only';
|
|
129
|
+
ed.focus();
|
|
130
|
+
// Курсор в конец (если контент пустой — это и есть начало).
|
|
131
|
+
const sel = window.getSelection();
|
|
132
|
+
const r = document.createRange();
|
|
133
|
+
r.selectNodeContents(ed);
|
|
134
|
+
r.collapse(false);
|
|
135
|
+
sel?.removeAllRanges();
|
|
136
|
+
sel?.addRange(r);
|
|
137
|
+
};
|
|
138
|
+
const exitEditMode = () => {
|
|
139
|
+
if (!ed.isContentEditable) return;
|
|
140
|
+
// removeAttribute полностью убирает contenteditable, чтобы
|
|
141
|
+
// closest('[contenteditable]') в обработчиках вне (dblclick/contextmenu)
|
|
142
|
+
// не матчился — и ПКМ снова открывал наш node-menu.
|
|
143
|
+
ed.removeAttribute('contenteditable');
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
ed.addEventListener('dblclick', e => {
|
|
147
|
+
e.stopPropagation();
|
|
148
|
+
enterEditMode();
|
|
149
|
+
});
|
|
150
|
+
ed.addEventListener('blur', exitEditMode);
|
|
152
151
|
ed.addEventListener('input', () => { node.text = ed.textContent; scheduleSave(); });
|
|
153
|
-
ed.addEventListener('mousedown', e => e.stopPropagation());
|
|
154
|
-
// Enter — перенос строки. Esc — снять фокус.
|
|
155
152
|
ed.addEventListener('keydown', e => {
|
|
156
153
|
if (e.key === 'Escape') { e.preventDefault(); ed.blur(); }
|
|
157
154
|
});
|
|
155
|
+
ed.addEventListener('mousedown', e => {
|
|
156
|
+
// В edit-режиме: не пускаем drag-обработчик (нужно поставить курсор/выделить текст).
|
|
157
|
+
if (ed.isContentEditable) e.stopPropagation();
|
|
158
|
+
// В read-режиме: пузырится — attachDrag-handler (повешен и на label-text)
|
|
159
|
+
// обработает single-click как «выбрать ноду» и подхватит drag.
|
|
160
|
+
});
|
|
161
|
+
|
|
158
162
|
body.appendChild(ed);
|
|
159
163
|
}
|
|
160
164
|
|
|
@@ -711,7 +715,17 @@ function attachResize(el, node, handle) {
|
|
|
711
715
|
|
|
712
716
|
function attachDrag(el, node) {
|
|
713
717
|
const header = el.querySelector('.node-header');
|
|
714
|
-
header
|
|
718
|
+
// Для label-нод вешаем drag и на сам текст: header скрыт до hover, а
|
|
719
|
+
// юзер хочет цеплять ноду с любого места видимого текста. В handler'е
|
|
720
|
+
// пропускаем срабатывание, если label в edit-режиме (см. условие ниже).
|
|
721
|
+
const sources = [header];
|
|
722
|
+
if (node.type === 'label') {
|
|
723
|
+
const lt = el.querySelector('.label-text');
|
|
724
|
+
if (lt) sources.push(lt);
|
|
725
|
+
}
|
|
726
|
+
const handler = e => {
|
|
727
|
+
// Edit-режим label — не таскаем (контекст редактирования текста).
|
|
728
|
+
if (e.currentTarget?.classList?.contains('label-text') && e.currentTarget.isContentEditable) return;
|
|
715
729
|
if (e.target.closest('.delete')) return;
|
|
716
730
|
// Multi-select c модификаторами — без drag
|
|
717
731
|
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
@@ -848,7 +862,8 @@ function attachDrag(el, node) {
|
|
|
848
862
|
};
|
|
849
863
|
document.addEventListener('mousemove', onMove);
|
|
850
864
|
document.addEventListener('mouseup', onUp);
|
|
851
|
-
}
|
|
865
|
+
};
|
|
866
|
+
for (const src of sources) if (src) src.addEventListener('mousedown', handler);
|
|
852
867
|
}
|
|
853
868
|
|
|
854
869
|
// Добавить ноду как клип в указанную дорожку с ripple-вставкой по времени
|
package/renderer/styles.css
CHANGED
|
@@ -485,39 +485,12 @@
|
|
|
485
485
|
position: relative;
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
/* Тулбар
|
|
489
|
-
|
|
490
|
-
position: absolute;
|
|
491
|
-
top: 100%; /* под label */
|
|
492
|
-
left: 0;
|
|
493
|
-
margin-top: 4px;
|
|
494
|
-
display: flex; align-items: center; gap: 4px;
|
|
495
|
-
padding: 3px 6px; background: rgba(20,20,20,0.92);
|
|
496
|
-
border-radius: 4px;
|
|
497
|
-
font-size: 11px; flex-shrink: 0;
|
|
498
|
-
opacity: 0; transition: opacity 0.15s;
|
|
499
|
-
pointer-events: none;
|
|
500
|
-
white-space: nowrap;
|
|
501
|
-
z-index: 10;
|
|
502
|
-
}
|
|
488
|
+
/* Тулбар (.label-toolbar) больше не используется — стиль/шрифт меняются
|
|
489
|
+
через ПКМ. Видимый header показываем только на hover. */
|
|
503
490
|
.node.label-node:hover .node-header,
|
|
504
|
-
.node.label-node:
|
|
505
|
-
.node.label-node:focus-within .node-header,
|
|
506
|
-
.node.label-node:focus-within .label-toolbar {
|
|
491
|
+
.node.label-node:focus-within .node-header {
|
|
507
492
|
opacity: 1; pointer-events: auto;
|
|
508
493
|
}
|
|
509
|
-
.node.label-node .label-toolbar select,
|
|
510
|
-
.node.label-node .label-toolbar button {
|
|
511
|
-
background: #1e1e1e; border: 1px solid #383838; color: #ccc;
|
|
512
|
-
border-radius: 3px; padding: 2px 6px; font-size: 11px;
|
|
513
|
-
cursor: pointer; line-height: 1.2;
|
|
514
|
-
}
|
|
515
|
-
.node.label-node .label-toolbar select:hover,
|
|
516
|
-
.node.label-node .label-toolbar button:hover { background: #2c2c2c; color: #fff; }
|
|
517
|
-
.node.label-node .label-toolbar button.active {
|
|
518
|
-
background: #3a5a8a; color: #fff; border-color: #4a6a9a;
|
|
519
|
-
}
|
|
520
|
-
.node.label-node .label-toolbar .tt-italic { font-style: italic; min-width: 22px; }
|
|
521
494
|
|
|
522
495
|
/* Сам текст — auto-ширина по контенту, до max-width.
|
|
523
496
|
line-height: 1.5 — чтобы descender'ы рукописных шрифтов (Caveat g/p/y,
|
|
@@ -539,8 +512,20 @@
|
|
|
539
512
|
}
|
|
540
513
|
.node.label-node .label-text:empty::before {
|
|
541
514
|
content: attr(data-placeholder);
|
|
542
|
-
color: rgba(200,200,200,0.
|
|
515
|
+
color: rgba(200,200,200,0.55);
|
|
516
|
+
}
|
|
517
|
+
/* Когда label пустая — рисуем подсказывающий dashed-контур, чтобы юзер
|
|
518
|
+
видел что нода существует (текст-плейсхолдер мог быть не замечен).
|
|
519
|
+
При наличии текста — никакой рамки, чисто подпись на холсте. */
|
|
520
|
+
.node.label-node:has(.label-text:empty) {
|
|
521
|
+
outline: 1px dashed rgba(200,200,200,0.25);
|
|
522
|
+
outline-offset: 1px;
|
|
523
|
+
border-radius: 3px;
|
|
543
524
|
}
|
|
525
|
+
/* Курсор «I-beam» над текстом — на hover показываем «текстуальную
|
|
526
|
+
природу» ноды, в read-mode сигнал что dblclick → редактировать. */
|
|
527
|
+
.node.label-node .label-text { cursor: text; }
|
|
528
|
+
.node.label-node:hover .label-text:not([contenteditable]) { cursor: pointer; }
|
|
544
529
|
/* === Label-шрифты ===
|
|
545
530
|
Все варианты — bundled woff2 (см. assets/fonts/), кириллица поддержана. */
|
|
546
531
|
.node.label-node .label-text[data-font="pencil"] {
|