kingkont 0.20.8 → 0.20.9

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,259 @@
1
+ // renderer/drawings.js — рисование стрелок на холсте.
2
+ //
3
+ // Юзерская команда: «стрелка это нода». Поэтому drag по холсту создаёт
4
+ // обычную ноду `type:'drawing'` со своим bbox + points. Дальше всё что
5
+ // можно делать с нодой работает «бесплатно»: select, drag, Backspace/×,
6
+ // Cmd-Z (через pushHistory ДО мутации), сохранение в scene.json.
7
+ //
8
+ // 3 инструмента (toolbar [data-draw-tool="…"]):
9
+ // • pencil — тонкая серая линия
10
+ // • lead — средне-чёрная
11
+ // • paint — толстая жёлтая
12
+ //
13
+ // Хранение в node:
14
+ // { id, type:'drawing', tool, x, y, width, height, points: [[relX, relY],…] }
15
+ // points в системе bbox (0,0 = node.x, node.y). При drag меняется только x/y.
16
+ //
17
+ // API (window.boardDrawings):
18
+ // setTool(name) — включить/выключить режим
19
+ // renderInto(node, el) — добавить <svg> внутрь node-элемента (зовёт board.js
20
+ // из createNodeEl)
21
+ // smoothPath / arrowHead — экспортированы для возможной кастомизации.
22
+
23
+ (function () {
24
+ const SVG_NS = 'http://www.w3.org/2000/svg';
25
+ const _draw = { tool: null, current: null };
26
+
27
+ function _state() { try { return state; } catch { return null; } }
28
+
29
+ function _canvasCoord(ev) {
30
+ const c = document.getElementById('canvas');
31
+ if (!c) return { x: ev.clientX, y: ev.clientY };
32
+ const r = c.getBoundingClientRect();
33
+ const sx = c.offsetWidth ? r.width / c.offsetWidth : 1;
34
+ const sy = c.offsetHeight ? r.height / c.offsetHeight : 1;
35
+ return { x: (ev.clientX - r.left) / (sx || 1), y: (ev.clientY - r.top) / (sy || 1) };
36
+ }
37
+
38
+ // Catmull-Rom → cubic bezier для мягких линий.
39
+ function smoothPath(pts) {
40
+ if (!pts || pts.length === 0) return '';
41
+ if (pts.length === 1) return `M ${pts[0][0]} ${pts[0][1]}`;
42
+ if (pts.length === 2) return `M ${pts[0][0]} ${pts[0][1]} L ${pts[1][0]} ${pts[1][1]}`;
43
+ let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
44
+ for (let i = 0; i < pts.length - 1; i++) {
45
+ const p0 = pts[i - 1] || pts[i];
46
+ const p1 = pts[i];
47
+ const p2 = pts[i + 1];
48
+ const p3 = pts[i + 2] || p2;
49
+ const c1x = p1[0] + (p2[0] - p0[0]) / 6;
50
+ const c1y = p1[1] + (p2[1] - p0[1]) / 6;
51
+ const c2x = p2[0] - (p3[0] - p1[0]) / 6;
52
+ const c2y = p2[1] - (p3[1] - p1[1]) / 6;
53
+ d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
54
+ }
55
+ return d;
56
+ }
57
+
58
+ function arrowHead(pts, size) {
59
+ if (!pts || pts.length < 2) return null;
60
+ const last = pts[pts.length - 1];
61
+ let prev = null;
62
+ for (let i = pts.length - 2; i >= 0; i--) {
63
+ if (Math.hypot(last[0] - pts[i][0], last[1] - pts[i][1]) > 12) { prev = pts[i]; break; }
64
+ }
65
+ if (!prev) prev = pts[0];
66
+ const ang = Math.atan2(last[1] - prev[1], last[0] - prev[0]);
67
+ const len = size || 14, w = (size || 14) * 0.55;
68
+ const ax = last[0] - len * Math.cos(ang);
69
+ const ay = last[1] - len * Math.sin(ang);
70
+ const lx = ax + w * Math.cos(ang + Math.PI / 2);
71
+ const ly = ay + w * Math.sin(ang + Math.PI / 2);
72
+ const rx = ax - w * Math.cos(ang + Math.PI / 2);
73
+ const ry = ay - w * Math.sin(ang + Math.PI / 2);
74
+ return `${last[0].toFixed(1)},${last[1].toFixed(1)} ${lx.toFixed(1)},${ly.toFixed(1)} ${rx.toFixed(1)},${ry.toFixed(1)}`;
75
+ }
76
+
77
+ function toolStyle(tool) {
78
+ return ({
79
+ pencil: { color: '#bbb', width: 1.5, opacity: 0.8, headSize: 10 },
80
+ lead: { color: '#2b2b2b', width: 2.6, opacity: 0.92, headSize: 13 },
81
+ paint: { color: '#e8b730', width: 5, opacity: 0.95, headSize: 18 },
82
+ })[tool] || { color: '#888', width: 2, opacity: 1, headSize: 12 };
83
+ }
84
+
85
+ // Рендер SVG внутрь node-элемента. Зовётся из board.js createNodeEl
86
+ // (когда node.type === 'drawing'). Идемпотентно — старый <svg.drawing-svg>
87
+ // удаляется перед вставкой нового.
88
+ function renderInto(node, el) {
89
+ const old = el.querySelector('svg.drawing-svg');
90
+ if (old) old.remove();
91
+ const w = node.width || 200, h = node.height || 100;
92
+ const svg = document.createElementNS(SVG_NS, 'svg');
93
+ svg.setAttribute('class', 'drawing-svg');
94
+ svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
95
+ svg.setAttribute('preserveAspectRatio', 'none');
96
+ svg.style.cssText = 'display:block;width:100%;height:100%;overflow:visible;pointer-events:none;';
97
+ const st = toolStyle(node.tool);
98
+ const color = node.color || st.color;
99
+ const line = document.createElementNS(SVG_NS, 'path');
100
+ line.setAttribute('class', 'line');
101
+ line.setAttribute('d', smoothPath(node.points || []));
102
+ line.setAttribute('fill', 'none');
103
+ line.setAttribute('stroke', color);
104
+ line.setAttribute('stroke-width', st.width);
105
+ line.setAttribute('stroke-opacity', st.opacity);
106
+ line.setAttribute('stroke-linecap', 'round');
107
+ line.setAttribute('stroke-linejoin', 'round');
108
+ svg.appendChild(line);
109
+ if (node.arrow !== false) {
110
+ const head = arrowHead(node.points || [], st.headSize);
111
+ if (head) {
112
+ const tri = document.createElementNS(SVG_NS, 'polygon');
113
+ tri.setAttribute('class', 'head');
114
+ tri.setAttribute('points', head);
115
+ tri.setAttribute('fill', color);
116
+ tri.setAttribute('opacity', st.opacity);
117
+ svg.appendChild(tri);
118
+ }
119
+ }
120
+ el.appendChild(svg);
121
+ return svg;
122
+ }
123
+
124
+ // === Temp-preview во время draw'а. SVG-overlay вне node-системы. ========
125
+ let _previewEl = null;
126
+ function _ensurePreview() {
127
+ let svg = document.getElementById('drawing-preview-svg');
128
+ if (svg) return svg;
129
+ const c = document.getElementById('canvas');
130
+ if (!c) return null;
131
+ svg = document.createElementNS(SVG_NS, 'svg');
132
+ svg.id = 'drawing-preview-svg';
133
+ svg.setAttribute('width', '6000');
134
+ svg.setAttribute('height', '4000');
135
+ svg.setAttribute('class', 'drawing-preview');
136
+ svg.style.cssText = 'position:absolute;left:0;top:0;pointer-events:none;z-index:1000;';
137
+ c.appendChild(svg);
138
+ return svg;
139
+ }
140
+ function _updatePreview(points, tool) {
141
+ const svg = _ensurePreview();
142
+ if (!svg) return;
143
+ if (_previewEl) _previewEl.remove();
144
+ const st = toolStyle(tool);
145
+ const g = document.createElementNS(SVG_NS, 'g');
146
+ const line = document.createElementNS(SVG_NS, 'path');
147
+ line.setAttribute('d', smoothPath(points));
148
+ line.setAttribute('fill', 'none');
149
+ line.setAttribute('stroke', st.color);
150
+ line.setAttribute('stroke-width', st.width);
151
+ line.setAttribute('stroke-opacity', st.opacity);
152
+ line.setAttribute('stroke-linecap', 'round');
153
+ line.setAttribute('stroke-linejoin', 'round');
154
+ g.appendChild(line);
155
+ const head = arrowHead(points, st.headSize);
156
+ if (head) {
157
+ const tri = document.createElementNS(SVG_NS, 'polygon');
158
+ tri.setAttribute('points', head);
159
+ tri.setAttribute('fill', st.color);
160
+ tri.setAttribute('opacity', st.opacity);
161
+ g.appendChild(tri);
162
+ }
163
+ svg.appendChild(g);
164
+ _previewEl = g;
165
+ }
166
+ function _clearPreview() {
167
+ if (_previewEl) { _previewEl.remove(); _previewEl = null; }
168
+ }
169
+
170
+ function setTool(tool) {
171
+ if (_draw.tool === tool) tool = null;
172
+ _draw.tool = tool;
173
+ const c = document.getElementById('canvas');
174
+ if (c) c.style.cursor = tool ? 'crosshair' : '';
175
+ document.querySelectorAll('[data-draw-tool]').forEach(b => {
176
+ b.classList.toggle('active', !!tool && b.dataset.drawTool === tool);
177
+ });
178
+ }
179
+
180
+ function _onMouseDown(e) {
181
+ if (!_draw.tool) return;
182
+ if (e.button !== 0) return;
183
+ if (e.target.closest('.node')) return;
184
+ e.preventDefault();
185
+ e.stopPropagation();
186
+ const { x, y } = _canvasCoord(e);
187
+ _draw.current = { tool: _draw.tool, points: [[x, y]] };
188
+ _updatePreview(_draw.current.points, _draw.current.tool);
189
+ document.addEventListener('mousemove', _onMouseMove);
190
+ document.addEventListener('mouseup', _onMouseUp, { once: true });
191
+ }
192
+
193
+ function _onMouseMove(e) {
194
+ if (!_draw.current) return;
195
+ const { x, y } = _canvasCoord(e);
196
+ const last = _draw.current.points[_draw.current.points.length - 1];
197
+ if (Math.hypot(x - last[0], y - last[1]) < 1.5) return;
198
+ _draw.current.points.push([x, y]);
199
+ _updatePreview(_draw.current.points, _draw.current.tool);
200
+ }
201
+
202
+ async function _onMouseUp() {
203
+ document.removeEventListener('mousemove', _onMouseMove);
204
+ _clearPreview();
205
+ if (!_draw.current) return;
206
+ const pts = _draw.current.points;
207
+ const tool = _draw.current.tool;
208
+ _draw.current = null;
209
+ if (pts.length < 3) return; // случайный клик → игнор
210
+ // BBox + relative-points + padding под линию и arrowhead.
211
+ const PAD = 22;
212
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
213
+ for (const [x, y] of pts) {
214
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
215
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
216
+ }
217
+ const x = Math.round(minX - PAD);
218
+ const y = Math.round(minY - PAD);
219
+ const width = Math.round(maxX - minX + PAD * 2);
220
+ const height = Math.round(maxY - minY + PAD * 2);
221
+ const rel = pts.map(([px, py]) => [+(px - x).toFixed(1), +(py - y).toFixed(1)]);
222
+ const node = {
223
+ id: crypto.randomUUID(),
224
+ type: 'drawing',
225
+ tool, x, y, width, height, points: rel,
226
+ };
227
+ const board = _state()?.currentBoard;
228
+ if (!board) return;
229
+ // Snapshot ДО мутации → Cmd-Z вернёт состояние без этой ноды.
230
+ if (typeof window.pushHistory === 'function') {
231
+ window.pushHistory('Рисование стрелки');
232
+ }
233
+ board.metadata.nodes.push(node);
234
+ // Рендерим ноду без полного renderCanvas (быстрее).
235
+ if (typeof window.createNodeEl === 'function') {
236
+ const el = await window.createNodeEl(node);
237
+ const canvas = document.getElementById('canvas');
238
+ if (canvas) canvas.appendChild(el);
239
+ }
240
+ if (typeof window.scheduleSave === 'function') window.scheduleSave();
241
+ }
242
+
243
+ function _onKeyDown(e) {
244
+ if (e.key === 'Escape' && _draw.tool) setTool(null);
245
+ }
246
+
247
+ window.boardDrawings = { setTool, renderInto, smoothPath, arrowHead, toolStyle };
248
+
249
+ document.addEventListener('DOMContentLoaded', () => {
250
+ const canvas = document.getElementById('canvas');
251
+ if (canvas) canvas.addEventListener('mousedown', _onMouseDown, true);
252
+ document.addEventListener('keydown', _onKeyDown);
253
+ document.querySelectorAll('[data-draw-tool]').forEach(b => {
254
+ b.addEventListener('click', () => setTool(b.dataset.drawTool));
255
+ });
256
+ // 🗑 «Очистить» из старой версии больше нет — каждая стрелка теперь
257
+ // нода, удаляется через Backspace/× или Cmd-Z прямо после draw'а.
258
+ });
259
+ })();
@@ -7,14 +7,19 @@
7
7
  // важен: см. <script> теги внизу index.html.
8
8
 
9
9
  // =================== Add-menu (dblclick / ПКМ по пустому холсту) ===================
10
+ // В view-only режиме (расшаренный/публичный шаблон) меню «создать ноду»
11
+ // бессмысленно — юзер всё равно не может ничего сохранить. Не показываем.
12
+ function _isViewOnly() { return document.body.classList.contains('view-only-mode'); }
10
13
  canvasWrap.addEventListener('dblclick', e => {
11
14
  if (!state.currentBoard) return;
15
+ if (_isViewOnly()) return;
12
16
  if (e.target.closest('.node, .conn, .resize-handle, .anchor')) return;
13
17
  if (e.target.closest('#addMenu, .modal')) return;
14
18
  showAddMenu(e.clientX, e.clientY);
15
19
  });
16
20
  canvasWrap.addEventListener('contextmenu', e => {
17
21
  if (!state.currentBoard) return;
22
+ if (_isViewOnly()) return;
18
23
  if (e.target.closest('.node, .conn, .resize-handle, .anchor, button, input, textarea, select')) return;
19
24
  if (e.target.closest('#addMenu, .modal')) return;
20
25
  e.preventDefault();
@@ -617,6 +617,9 @@
617
617
  ws.onclose = () => setTimeout(_connect, 5000);
618
618
  ws.onerror = () => {};
619
619
  }
620
- _connect();
620
+ // /ws живёт только в Electron-server'е (lib/wsHub.js → server.js). В web
621
+ // job'ы запускаются client-side (web-shim → polling chatium API), push-канал
622
+ // не нужен. Без guard'а — постоянные failed-WSS попытки на kingkont.ru/ws.
623
+ if (!window.__KINGKONT_WEB__) _connect();
621
624
  });
622
625
  })();
package/renderer/state.js CHANGED
@@ -159,23 +159,31 @@ window.bgJobEnd = bgJobEnd;
159
159
  window.bgJobsAll = bgJobsAll;
160
160
 
161
161
  // === SYSTEM NOTIFICATION (HTML5 Notification API) ===
162
- // Просим разрешение eagerly при старте приложения чтобы первое уведомление
163
- // уже работало (без race с первой генерацией).
162
+ // Запрос разрешения ТОЛЬКО из user-gesture'а (Chrome иначе кидает
163
+ // console.error: «Notification prompting can only be done from a user
164
+ // gesture»). Привязываемся к первому click/keydown — он считается жестом.
164
165
  let _notifPermAsked = false;
165
166
  if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
166
- // Откладываем чуть-чуть — иначе диалог появляется до того как юзер увидел UI.
167
- setTimeout(() => {
167
+ function _askNotifOnGesture() {
168
+ if (_notifPermAsked) return;
168
169
  _notifPermAsked = true;
169
- Notification.requestPermission().catch(() => {});
170
- }, 2000);
170
+ try { Notification.requestPermission().catch(() => {}); } catch {}
171
+ document.removeEventListener('click', _askNotifOnGesture);
172
+ document.removeEventListener('keydown', _askNotifOnGesture);
173
+ }
174
+ document.addEventListener('click', _askNotifOnGesture, { once: false });
175
+ document.addEventListener('keydown', _askNotifOnGesture, { once: false });
171
176
  }
172
177
  async function systemNotify(title, body, opts = {}) {
173
178
  if (typeof Notification === 'undefined') return null;
174
179
  let perm = Notification.permission;
175
- if (perm === 'default' && !_notifPermAsked) {
176
- _notifPermAsked = true;
177
- try { perm = await Notification.requestPermission(); } catch {}
178
- }
180
+ // Chrome требует чтобы requestPermission вызывался ТОЛЬКО из user-gesture'а
181
+ // (click/keydown handler). systemNotify часто зовут из фоновых событий
182
+ // (generation done, WS push) запрос упадёт с console.error «Notification
183
+ // prompting can only be done from a user gesture». Без granted перм-а
184
+ // OS-нотификации не работают, но в notifyPanel событие УЖЕ добавлено.
185
+ // Если default — silently skip; запросим permission на первом «реальном»
186
+ // user-клике (см. _wireNotifPermPrompt ниже).
179
187
  if (perm !== 'granted') {
180
188
  // Без OS-нотификации — событие УЖЕ добавлено в notifyPanel через
181
189
  // showToast-wrapper (single-source-of-truth). Раньше тут был fallback
@@ -171,7 +171,10 @@
171
171
  .sidebar-header .brand .sub.has-project {
172
172
  color: #fff; text-transform: none; letter-spacing: 0;
173
173
  font-size: 18px; font-weight: 600; line-height: 1.2;
174
- word-break: break-word;
174
+ /* Всегда одна строка — иначе после загрузки имя проекта переносится
175
+ и шапка прыгает вверх-вниз. ellipsis даёт `…` если не помещается. */
176
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
177
+ min-width: 0;
175
178
  }
176
179
  /* Имя выбранной сцены спрятано в шапке (юзер: «название выбранной сцены
177
180
  скрывай»). Сама подсветка сцены остаётся в sidebar-list. */
@@ -492,6 +495,11 @@
492
495
  .welcome-recent-grid {
493
496
  display: grid;
494
497
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
498
+ /* grid-auto-rows: max-content — иначе cards (у которых дети все
499
+ position:absolute → 0 intrinsic height) получают row-height ~18px,
500
+ и aspect-ratio:4/3 рисует контент за пределы трека. Соседние cards
501
+ визуально наезжают друг на друга — именно это юзер видел в Electron. */
502
+ grid-auto-rows: max-content;
495
503
  gap: 16px;
496
504
  overflow-y: auto; overflow-x: hidden;
497
505
  padding: 8px 52px 24px;
@@ -1054,6 +1062,82 @@
1054
1062
  .connections-layer .line { stroke: #6a8aaa; stroke-width: 2; fill: none; pointer-events: none; }
1055
1063
  .connections-layer .conn:hover .line { stroke: #f88; stroke-width: 3; }
1056
1064
  .connections-layer .temp-line { stroke: #6a8aaa; stroke-width: 2; stroke-dasharray: 6 4; fill: none; pointer-events: none; }
1065
+ /* === Drawing-нода (стрелка). ============================================
1066
+ `.drawing-node` — обычная .node, но без header/body/anchor (см.
1067
+ createNodeEl branch). Прозрачный фон, без border'а в нормальном
1068
+ состоянии — видна только SVG-линия. На select подсвечиваем bbox
1069
+ стандартным outline'ом для консистентности с другими нодами. */
1070
+ .node.drawing-node {
1071
+ background: transparent; border: none; box-shadow: none; padding: 0;
1072
+ overflow: visible; cursor: move;
1073
+ }
1074
+ .node.drawing-node .drawing-svg { display: block; width: 100%; height: 100%; }
1075
+ .node.drawing-node.selected,
1076
+ .node.drawing-node:hover {
1077
+ outline: 1px dashed rgba(106,138,170,0.6); outline-offset: 2px;
1078
+ }
1079
+ .node.drawing-node.selected {
1080
+ outline: 1.5px solid #4af; outline-offset: 2px;
1081
+ }
1082
+ /* Toolbar drawing-buttons: active — подсветка как «нажата». */
1083
+ .toolbar [data-draw-tool] { font-size: 16px; padding: 4px 8px; }
1084
+ .toolbar [data-draw-tool].active {
1085
+ background: #3a5273; color: #fff; border-color: #4a6a9a;
1086
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.15);
1087
+ }
1088
+ .toolbar-sep { display: inline-block; width: 1px; height: 20px; background: #333; margin: 0 6px; vertical-align: middle; }
1089
+
1090
+ /* === Edit-affordances visibility ========================================
1091
+ Юзер: «вид по умолчанию должен быть без элементов управления, они
1092
+ должны появляться потом». Поэтому:
1093
+ - .kk-edit-only по умолчанию СКРЫТО.
1094
+ - body.edit-mode реstoreит дефолтный display.
1095
+ edit-mode выставляется в cloudProjects.open() когда canModify=true.
1096
+ Для view-only mode edit-mode НЕ выставляется → все edit-кнопки остаются
1097
+ спрятанными, в toolbar показывается inline-баннер (.kk-view-only-banner).
1098
+ */
1099
+ .kk-edit-only { display: none !important; }
1100
+ body.edit-mode .kk-edit-only { display: revert !important; }
1101
+
1102
+ /* === View-only mode (template-share / canModify=false) =================
1103
+ Sidebar остаётся (юзер навигирует по сценам), но edit-секции уже скрыты
1104
+ через .kk-edit-only выше. В toolbar показываем inline-banner вместо
1105
+ отсутствующих edit-кнопок. */
1106
+ .kk-view-only-banner { display: none; }
1107
+ body.view-only-mode .kk-view-only-banner {
1108
+ display: flex; align-items: center; gap: 14px;
1109
+ flex: 1; min-width: 0;
1110
+ padding: 4px 14px; margin: -4px 0;
1111
+ background: linear-gradient(90deg,#3a5f8c,#4a8ad6);
1112
+ color: #fff; border-radius: 6px;
1113
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1114
+ font-size: 14px;
1115
+ }
1116
+ .kk-view-only-banner .kk-vo-label { opacity: 0.95; }
1117
+ .kk-view-only-banner #viewOnlyCloneBtn {
1118
+ padding: 6px 14px; background: #fff; color: #3a5f8c;
1119
+ border: none; border-radius: 5px; cursor: pointer;
1120
+ font-weight: 600; font-size: 13px;
1121
+ box-shadow: 0 1px 4px rgba(0,0,0,0.15);
1122
+ }
1123
+ .kk-view-only-banner #viewOnlyCloneBtn:hover { background: #f0f4fa; }
1124
+ /* В view-only ноды некликабельны — без drag/edit/resize/anchor.
1125
+ Картинки и видео по-прежнему кликабельны для fullscreen-просмотра. */
1126
+ body.view-only-mode .node {
1127
+ cursor: default !important; pointer-events: none;
1128
+ }
1129
+ body.view-only-mode .node img,
1130
+ body.view-only-mode .node video { pointer-events: auto; cursor: zoom-in; }
1131
+ body.view-only-mode .connections-layer .hit { pointer-events: none !important; }
1132
+ /* Дополнительно прячем in-node edit-аффордансы (× delete, resize-handle,
1133
+ anchor, draft-«Запустить» кнопки) — они генерятся JS'ом и не получают
1134
+ .kk-edit-only автоматом. */
1135
+ body.view-only-mode .node .node-header .delete,
1136
+ body.view-only-mode .node .resize-handle,
1137
+ body.view-only-mode .node .label-width-handle,
1138
+ body.view-only-mode .node .anchor,
1139
+ body.view-only-mode .node .gen-pending,
1140
+ body.view-only-mode .node-footer { display: none !important; }
1057
1141
 
1058
1142
  /* Меню «что добавить» */
1059
1143
  .add-menu {
package/server.js CHANGED
@@ -462,6 +462,33 @@ async function handleProjectDelete(res, id) {
462
462
  } catch (e) { sendError(res, e, 502); }
463
463
  }
464
464
 
465
+ // Прокси /api/proj/<action> → https://kingkont.ru/proj_<action>. Использует
466
+ // сохранённый Bearer-токен юзера (тот же что для всех chatium-вызовов).
467
+ // Сохраняем method, query-string и тело, передаём response как есть.
468
+ async function handleChatiumProjProxy(req, res, action, url) {
469
+ const s = getSettings();
470
+ if (!s.useChatium || !s.chatium?.token || !s.chatium?.base) {
471
+ return sendError(res, new Error('Войдите в KingKont — нет токена'), 401);
472
+ }
473
+ try {
474
+ const target = `${s.chatium.base.replace(/\/$/, '')}/app/spaces/server/api/proj_${action}${url.search || ''}`;
475
+ const init = {
476
+ method: req.method,
477
+ headers: {
478
+ 'Authorization': `Bearer ${s.chatium.token}`,
479
+ 'Content-Type': 'application/json',
480
+ },
481
+ };
482
+ if (req.method === 'POST' || req.method === 'PUT') {
483
+ init.body = JSON.stringify(await readJson(req));
484
+ }
485
+ const r = await fetch(target, init);
486
+ const text = await r.text();
487
+ res.writeHead(r.status, { 'Content-Type': r.headers.get('content-type') || 'application/json' });
488
+ res.end(text);
489
+ } catch (e) { sendError(res, e, 502); }
490
+ }
491
+
465
492
  // ---- Project texts (.md контент text-нод). --------------------------------
466
493
  // Зачем отдельный API: см. lib/providers.js → CHATIUM_PATHS.projectTexts.
467
494
 
@@ -715,6 +742,14 @@ async function _requestHandler(req, res) {
715
742
  if (req.method === 'DELETE') return handleProjectDelete(res, decodeURIComponent(m[1]));
716
743
  }
717
744
  }
745
+ // Share-management endpoints. Прозрачный прокси на chatium /proj_*
746
+ // (новые TS-файлы — обход модульного кеша исходного projects.ts).
747
+ // Действия: setPublic / share / unshare / shares / clone / sharedWithMe.
748
+ {
749
+ const m = url.pathname.match(/^\/api\/proj\/(setPublic|share|unshare|shares|clone|sharedWithMe)$/);
750
+ if (m) return handleChatiumProjProxy(req, res, m[1], url);
751
+ }
752
+
718
753
  // Project texts (.md контент). Отдельная таблица на сервере; клиент
719
754
  // ссылается на записи по id из manifest'а board.texts[relPath] = textId.
720
755
  if (req.method === 'POST' && url.pathname === '/api/project-texts') return handleProjectTextCreate(req, res);