@sequent-org/moodboard 1.4.59 → 1.4.61
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/src/assets/icons/ban.svg +1 -0
- package/src/core/PixiEngine.js +19 -7
- package/src/objects/FrameObject.js +16 -3
- package/src/services/text/TextBoxMetrics.js +86 -0
- package/src/tools/manager/ToolManagerLifecycle.js +48 -2
- package/src/tools/object-tools/connector/ConnectorDragController.js +9 -0
- package/src/tools/object-tools/selection/NoteInlineEditorController.js +58 -47
- package/src/tools/object-tools/selection/TextEditorCaretService.js +55 -3
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +40 -1
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +7 -0
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +4 -3
- package/src/tools/object-tools/selection/TextEditorSyncService.js +391 -29
- package/src/tools/object-tools/selection/TextInlineEditorController.js +62 -8
- package/src/ui/HtmlTextLayer.js +31 -13
- package/src/ui/animation/HoverLiftController.js +33 -1
- package/src/ui/handles/HandlesDomRenderer.js +30 -3
- package/src/ui/handles/HandlesInteractionController.js +26 -28
- package/src/ui/styles/chat.css +43 -0
- package/src/ui/styles/topbar.css +20 -0
- package/src/ui/styles/workspace.css +57 -2
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +19 -19
package/src/ui/HtmlTextLayer.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveLineHeightRatio,
|
|
10
10
|
computeTextRightPadPx,
|
|
11
11
|
resolveStaticTextPadding,
|
|
12
|
+
computeSingleLineCenterDelta,
|
|
12
13
|
TEXT_BOX_BOTTOM_PAD_PX,
|
|
13
14
|
} from '../services/text/TextBoxMetrics.js';
|
|
14
15
|
|
|
@@ -761,16 +762,26 @@ export class HtmlTextLayer {
|
|
|
761
762
|
}
|
|
762
763
|
} catch (_) {}
|
|
763
764
|
|
|
764
|
-
//
|
|
765
|
+
// Высота текстового бокса всегда равна высоте контента: рамка облегает строки,
|
|
766
|
+
// под текстом нет пустого зазора. Высоту объекта (logHeight = round(h*s)) здесь
|
|
767
|
+
// НЕ учитываем — иначе единожды завышенный obj.height (дефолт при создании или
|
|
768
|
+
// остаток от прежнего многострочного текста) залипал, и появлялся зазор после зума.
|
|
765
769
|
try {
|
|
766
770
|
el.style.height = 'auto';
|
|
767
|
-
//
|
|
768
|
-
|
|
771
|
+
// Для обычного однострочного текста центрируем видимые буквы по вертикали:
|
|
772
|
+
// line-box резервирует сверху место под прописные/выносные, которого строчный
|
|
773
|
+
// текст не занимает, поэтому буквы кажутся прижатыми к низу рамки. centerDelta
|
|
774
|
+
// уравнивает верхний и нижний зазоры вокруг глифов. Для markdown/списков и в
|
|
775
|
+
// окружении без layout (jsdom) дельта = null → сохраняем прежний нижний отступ
|
|
776
|
+
// TEXT_BOX_BOTTOM_PAD_PX под хвосты букв (например, «з», «у»).
|
|
777
|
+
const centerDelta = (!isMarkdown && !useList)
|
|
778
|
+
? computeSingleLineCenterDelta(el)
|
|
779
|
+
: null;
|
|
780
|
+
const extra = (centerDelta === null) ? TEXT_BOX_BOTTOM_PAD_PX : centerDelta;
|
|
781
|
+
const hCss = Math.max(1, Math.round(el.scrollHeight + extra));
|
|
769
782
|
el.style.height = `${hCss}px`;
|
|
770
|
-
// Обновим высоту для
|
|
771
|
-
|
|
772
|
-
logHeight = hCss;
|
|
773
|
-
}
|
|
783
|
+
// Обновим высоту для лога
|
|
784
|
+
logHeight = hCss;
|
|
774
785
|
} catch (_) {}
|
|
775
786
|
|
|
776
787
|
console.log(`🔍 HtmlTextLayer: позиция обновлена для ${objectId}:`, {
|
|
@@ -910,19 +921,26 @@ export class HtmlTextLayer {
|
|
|
910
921
|
// Измеряем фактическую высоту HTML-текста
|
|
911
922
|
el.style.height = 'auto';
|
|
912
923
|
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
913
|
-
|
|
914
|
-
// Пересчитываем мировую высоту и отправляем обновление размера через события ResizeUpdate
|
|
924
|
+
|
|
915
925
|
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
916
926
|
const s = world?.scale?.x || 1;
|
|
917
927
|
const res = (this.core?.pixi?.app?.renderer?.resolution) || 1;
|
|
918
|
-
const
|
|
919
|
-
|
|
928
|
+
const worldH_auto = (measured * res) / s;
|
|
929
|
+
|
|
930
|
+
const currentWorldH = obj?.height || 0;
|
|
931
|
+
// Высота объекта всегда подгоняется под контент (и вверх, и вниз): без сжатия
|
|
932
|
+
// единожды завышенная высота залипала и давала пустой зазор под текстом.
|
|
933
|
+
const finalWorldH = worldH_auto;
|
|
934
|
+
const finalCssH = Math.round((finalWorldH * s) / res);
|
|
935
|
+
|
|
936
|
+
el.style.height = `${finalCssH}px`;
|
|
937
|
+
|
|
920
938
|
const worldW = obj?.width || 0;
|
|
921
939
|
const position = obj?.position || null;
|
|
922
|
-
if (worldW > 0 && position) {
|
|
940
|
+
if (worldW > 0 && position && finalWorldH !== currentWorldH) {
|
|
923
941
|
this.core.eventBus.emit(Events.Tool.ResizeUpdate, {
|
|
924
942
|
object: objectId,
|
|
925
|
-
size: { width: worldW, height:
|
|
943
|
+
size: { width: worldW, height: finalWorldH },
|
|
926
944
|
position
|
|
927
945
|
});
|
|
928
946
|
}
|
|
@@ -83,6 +83,7 @@ export class HoverLiftController {
|
|
|
83
83
|
this._isDragging = false;
|
|
84
84
|
this._isResizing = false;
|
|
85
85
|
this._isRotating = false;
|
|
86
|
+
this._isConnecting = false;
|
|
86
87
|
/** Set<id> — выделенные объекты (по _mb.objectId) */
|
|
87
88
|
this._selectedIds = new Set();
|
|
88
89
|
|
|
@@ -172,6 +173,26 @@ export class HoverLiftController {
|
|
|
172
173
|
this._entries.delete(pixiObject);
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Текущая визуальная hover-трансформация объекта относительно его базы.
|
|
178
|
+
* Нужна, чтобы внешние оверлеи (подсветка коннектора) совпадали с
|
|
179
|
+
* масштабированным/приподнятым на hover объектом.
|
|
180
|
+
* @param {import('pixi.js').DisplayObject} pixiObject
|
|
181
|
+
* @returns {{ scaleMulX: number, scaleMulY: number, centerX: number, centerY: number } | null}
|
|
182
|
+
*/
|
|
183
|
+
getVisualTransform(pixiObject) {
|
|
184
|
+
const entry = this._entries.get(pixiObject);
|
|
185
|
+
if (!entry) return null;
|
|
186
|
+
const baseScaleX = entry.baseScaleX || 1;
|
|
187
|
+
const baseScaleY = entry.baseScaleY || 1;
|
|
188
|
+
return {
|
|
189
|
+
scaleMulX: (pixiObject.scale?.x ?? baseScaleX) / baseScaleX,
|
|
190
|
+
scaleMulY: (pixiObject.scale?.y ?? baseScaleY) / baseScaleY,
|
|
191
|
+
centerX: pixiObject.x,
|
|
192
|
+
centerY: pixiObject.y,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
175
196
|
/** Обновить базовые значения после внешнего resize/move */
|
|
176
197
|
syncBase(pixiObject) {
|
|
177
198
|
const entry = this._entries.get(pixiObject);
|
|
@@ -197,9 +218,20 @@ export class HoverLiftController {
|
|
|
197
218
|
return world?.scale?.x ?? 1;
|
|
198
219
|
}
|
|
199
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Внешняя блокировка hover на время протягивания коннектора.
|
|
223
|
+
* Цель коннектора не должна играть scale/lift: иначе её визуальные
|
|
224
|
+
* границы выходят за рамку подсветки, которая строится по логическим bounds.
|
|
225
|
+
* @param {boolean} active
|
|
226
|
+
*/
|
|
227
|
+
setConnecting(active) {
|
|
228
|
+
this._isConnecting = !!active;
|
|
229
|
+
if (this._isConnecting) this._snapBackAll();
|
|
230
|
+
}
|
|
231
|
+
|
|
200
232
|
/** Можно ли сейчас показывать hover */
|
|
201
233
|
_isBlocked(pixiObject) {
|
|
202
|
-
if (this._isDragging || this._isResizing || this._isRotating) return true;
|
|
234
|
+
if (this._isDragging || this._isResizing || this._isRotating || this._isConnecting) return true;
|
|
203
235
|
const id = pixiObject._mb?.objectId;
|
|
204
236
|
if (id && this._selectedIds.has(id)) return true;
|
|
205
237
|
return false;
|
|
@@ -9,6 +9,15 @@ import { MINDMAP_LAYOUT } from '../mindmap/MindmapLayoutConfig.js';
|
|
|
9
9
|
import { MindmapStatePatchCommand } from '../../core/commands/MindmapStatePatchCommand.js';
|
|
10
10
|
|
|
11
11
|
const HANDLES_ACCENT_COLOR = '#80D8FF';
|
|
12
|
+
const VERTICAL_RESIZE_CURSOR_COLOR = '#6B7280';
|
|
13
|
+
const VERTICAL_RESIZE_CURSOR = 'url("/icons/move-vertical.svg") 12 12, ns-resize';
|
|
14
|
+
const VERTICAL_RESIZE_CURSOR_TYPES = new Set(['frame', 'text', 'simple-text', 'image']);
|
|
15
|
+
const VERTICAL_RESIZE_CORNER_CURSOR_ANGLES = {
|
|
16
|
+
nw: -45,
|
|
17
|
+
se: -45,
|
|
18
|
+
ne: 45,
|
|
19
|
+
sw: 45,
|
|
20
|
+
};
|
|
12
21
|
const REVIT_SHOW_IN_MODEL_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path d="M384 64C366.3 64 352 78.3 352 96C352 113.7 366.3 128 384 128L466.7 128L265.3 329.4C252.8 341.9 252.8 362.2 265.3 374.7C277.8 387.2 298.1 387.2 310.6 374.7L512 173.3L512 256C512 273.7 526.3 288 544 288C561.7 288 576 273.7 576 256L576 96C576 78.3 561.7 64 544 64L384 64zM144 160C99.8 160 64 195.8 64 240L64 496C64 540.2 99.8 576 144 576L400 576C444.2 576 480 540.2 480 496L480 416C480 398.3 465.7 384 448 384C430.3 384 416 398.3 416 416L416 496C416 504.8 408.8 512 400 512L144 512C135.2 512 128 504.8 128 496L128 240C128 231.2 135.2 224 144 224L224 224C241.7 224 256 209.7 256 192C256 174.3 241.7 160 224 160L144 160z"/></svg>';
|
|
13
22
|
const MODEL3D_SHOW_IN_VIEWER_ICON_SVG = REVIT_SHOW_IN_MODEL_ICON_SVG;
|
|
14
23
|
const MINDMAP_CHILD_WIDTH_FACTOR = 0.9;
|
|
@@ -39,6 +48,19 @@ function resolveBottomSiblingParentId(sourceObjectId, sourceMeta) {
|
|
|
39
48
|
return sourceMeta?.parentId || null;
|
|
40
49
|
}
|
|
41
50
|
|
|
51
|
+
function shouldUseVerticalResizeCursor(mbType, handleName) {
|
|
52
|
+
return VERTICAL_RESIZE_CURSOR_TYPES.has(mbType)
|
|
53
|
+
&& (handleName === 'handle' || handleName === 'top' || handleName === 'bottom');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createVerticalResizeCornerCursor(dir, rotation) {
|
|
57
|
+
const baseAngle = VERTICAL_RESIZE_CORNER_CURSOR_ANGLES[dir] || 0;
|
|
58
|
+
const totalAngle = baseAngle + (rotation || 0);
|
|
59
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><g transform="rotate(${totalAngle} 12 12)"><path d="M12 2v20" stroke="${VERTICAL_RESIZE_CURSOR_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="m8 18 4 4 4-4" stroke="${VERTICAL_RESIZE_CURSOR_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="m8 6 4-4 4 4" stroke="${VERTICAL_RESIZE_CURSOR_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></svg>`;
|
|
60
|
+
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
61
|
+
return `url("${dataUrl}") 12 12, nwse-resize`;
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
function relayoutMindmapBranchLevel({ core, eventBus, parentId, side }) {
|
|
43
65
|
if (!core || !eventBus || !parentId || (side !== 'left' && side !== 'right')) return;
|
|
44
66
|
const objects = core?.state?.state?.objects || [];
|
|
@@ -1166,10 +1188,11 @@ export class HandlesDomRenderer {
|
|
|
1166
1188
|
let sourceMindmapProperties = null;
|
|
1167
1189
|
const occupiedOutgoingSides = new Set();
|
|
1168
1190
|
const hiddenIncomingSide = { value: null };
|
|
1191
|
+
let mbType = null;
|
|
1169
1192
|
if (id !== '__group__') {
|
|
1170
1193
|
const req = { objectId: id, pixiObject: null };
|
|
1171
1194
|
this.host.eventBus.emit('tool:get:object:pixi', req);
|
|
1172
|
-
|
|
1195
|
+
mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
1173
1196
|
isFileTarget = mbType === 'file';
|
|
1174
1197
|
isFrameTarget = mbType === 'frame';
|
|
1175
1198
|
isMindmapTarget = mbType === 'mindmap';
|
|
@@ -1244,7 +1267,9 @@ export class HandlesDomRenderer {
|
|
|
1244
1267
|
}
|
|
1245
1268
|
|
|
1246
1269
|
const mkCorner = (dir, x, y) => {
|
|
1247
|
-
const cursor =
|
|
1270
|
+
const cursor = shouldUseVerticalResizeCursor(mbType, 'handle')
|
|
1271
|
+
? createVerticalResizeCornerCursor(dir, rotation)
|
|
1272
|
+
: createRotatedResizeCursor(dir, rotation);
|
|
1248
1273
|
const h = document.createElement('div');
|
|
1249
1274
|
h.dataset.dir = dir;
|
|
1250
1275
|
h.dataset.id = id;
|
|
@@ -1288,7 +1313,9 @@ export class HandlesDomRenderer {
|
|
|
1288
1313
|
|
|
1289
1314
|
const edgeSize = 10;
|
|
1290
1315
|
const makeEdge = (name, style, cursorHandleType) => {
|
|
1291
|
-
const cursor =
|
|
1316
|
+
const cursor = shouldUseVerticalResizeCursor(mbType, name)
|
|
1317
|
+
? VERTICAL_RESIZE_CURSOR
|
|
1318
|
+
: createRotatedResizeCursor(cursorHandleType, rotation);
|
|
1292
1319
|
const e = document.createElement('div');
|
|
1293
1320
|
e.dataset.edge = name;
|
|
1294
1321
|
e.dataset.id = id;
|
|
@@ -216,12 +216,14 @@ export class HandlesInteractionController {
|
|
|
216
216
|
let aspectLockDominantAxis = null;
|
|
217
217
|
let isTextTarget = false;
|
|
218
218
|
let isNoteTarget = false;
|
|
219
|
+
let isVerticalResizeTarget = false;
|
|
219
220
|
{
|
|
220
221
|
const req = { objectId: id, pixiObject: null };
|
|
221
222
|
this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
|
|
222
223
|
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
223
224
|
isTextTarget = (mbType === 'text' || mbType === 'simple-text');
|
|
224
225
|
isNoteTarget = (mbType === 'note');
|
|
226
|
+
isVerticalResizeTarget = ['frame', 'text', 'simple-text', 'image'].includes(mbType);
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
const onMove = (ev) => {
|
|
@@ -263,9 +265,9 @@ export class HandlesInteractionController {
|
|
|
263
265
|
newW = rotatedBox.width;
|
|
264
266
|
newH = rotatedBox.height;
|
|
265
267
|
} else {
|
|
266
|
-
if (dir.includes('e')) newW = Math.max(1, startCSS.width + dx);
|
|
268
|
+
if (dir.includes('e') && !isVerticalResizeTarget) newW = Math.max(1, startCSS.width + dx);
|
|
267
269
|
if (dir.includes('s')) newH = Math.max(1, startCSS.height + dy);
|
|
268
|
-
if (dir.includes('w')) {
|
|
270
|
+
if (dir.includes('w') && !isVerticalResizeTarget) {
|
|
269
271
|
newW = Math.max(1, startCSS.width - dx);
|
|
270
272
|
newLeft = startCSS.left + dx;
|
|
271
273
|
}
|
|
@@ -344,7 +346,7 @@ export class HandlesInteractionController {
|
|
|
344
346
|
el.style.width = `${Math.max(1, Math.round(newW))}px`;
|
|
345
347
|
el.style.height = 'auto';
|
|
346
348
|
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
347
|
-
newH = measured;
|
|
349
|
+
newH = Math.max(newH, measured);
|
|
348
350
|
}
|
|
349
351
|
} catch (_) {}
|
|
350
352
|
}
|
|
@@ -418,35 +420,30 @@ export class HandlesInteractionController {
|
|
|
418
420
|
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
419
421
|
isFrameTarget = mbType === 'frame';
|
|
420
422
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
oldPosition: { x: startWorld.x, y: startWorld.y },
|
|
426
|
-
newPosition: isFrameTarget ? null : (isEdgeLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }),
|
|
427
|
-
};
|
|
428
|
-
this.host.eventBus.emit(Events.Tool.ResizeEnd, resizeEndData);
|
|
429
|
-
try {
|
|
430
|
-
const req2 = { objectId: id, pixiObject: null };
|
|
431
|
-
this.host.eventBus.emit(Events.Tool.GetObjectPixi, req2);
|
|
432
|
-
const mbType2 = req2.pixiObject && req2.pixiObject._mb && req2.pixiObject._mb.type;
|
|
433
|
-
if (mbType2 === 'text' || mbType2 === 'simple-text') {
|
|
423
|
+
|
|
424
|
+
let finalWorldH = worldH;
|
|
425
|
+
if (isTextTarget) {
|
|
426
|
+
try {
|
|
434
427
|
const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
|
|
435
428
|
const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
|
|
436
429
|
if (el) {
|
|
437
430
|
el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
|
|
438
431
|
el.style.height = 'auto';
|
|
439
432
|
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
object: id,
|
|
443
|
-
size: { width: worldW, height: worldH2 },
|
|
444
|
-
position: isFrameTarget ? null : (isEdgeLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }),
|
|
445
|
-
};
|
|
446
|
-
this.host.eventBus.emit(Events.Tool.ResizeUpdate, fixData);
|
|
433
|
+
const finalCssH = Math.max(measured, endCSS.height);
|
|
434
|
+
finalWorldH = finalCssH / s;
|
|
447
435
|
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
436
|
+
} catch (_) {}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const resizeEndData = {
|
|
440
|
+
object: id,
|
|
441
|
+
oldSize: { width: startWorld.width, height: startWorld.height },
|
|
442
|
+
newSize: { width: worldW, height: finalWorldH },
|
|
443
|
+
oldPosition: { x: startWorld.x, y: startWorld.y },
|
|
444
|
+
newPosition: edgeFinalPositionChanged ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y },
|
|
445
|
+
};
|
|
446
|
+
this.host.eventBus.emit(Events.Tool.ResizeEnd, resizeEndData);
|
|
450
447
|
}
|
|
451
448
|
};
|
|
452
449
|
|
|
@@ -626,7 +623,7 @@ export class HandlesInteractionController {
|
|
|
626
623
|
el.style.width = `${Math.max(1, Math.round(newW))}px`;
|
|
627
624
|
el.style.height = 'auto';
|
|
628
625
|
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
629
|
-
newH = measured;
|
|
626
|
+
newH = Math.max(newH, measured);
|
|
630
627
|
}
|
|
631
628
|
} catch (_) {}
|
|
632
629
|
}
|
|
@@ -717,7 +714,7 @@ export class HandlesInteractionController {
|
|
|
717
714
|
} else {
|
|
718
715
|
const edgeFinalPositionChanged = (endCSS.left !== startCSS.left) || (endCSS.top !== startCSS.top);
|
|
719
716
|
let finalWorldH = worldH;
|
|
720
|
-
if (isTextTarget
|
|
717
|
+
if (isTextTarget) {
|
|
721
718
|
try {
|
|
722
719
|
const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
|
|
723
720
|
const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
|
|
@@ -725,7 +722,8 @@ export class HandlesInteractionController {
|
|
|
725
722
|
el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
|
|
726
723
|
el.style.height = 'auto';
|
|
727
724
|
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
728
|
-
|
|
725
|
+
const finalCssH = Math.max(measured, endCSS.height);
|
|
726
|
+
finalWorldH = finalCssH / s;
|
|
729
727
|
}
|
|
730
728
|
} catch (_) {}
|
|
731
729
|
}
|
package/src/ui/styles/chat.css
CHANGED
|
@@ -32,6 +32,49 @@
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/* Mobile (≤768px): панель генерации — компактный bottom-sheet во всю ширину,
|
|
36
|
+
приподнят над зум/карта-баром, не наезжает на тулбар и холст. */
|
|
37
|
+
@media (max-width: 768px) {
|
|
38
|
+
.moodboard-chat {
|
|
39
|
+
left: 8px;
|
|
40
|
+
right: 8px;
|
|
41
|
+
bottom: 84px; /* выше зум/карта-бара (16px + 60px touch-кнопки + зазор) */
|
|
42
|
+
transform: none;
|
|
43
|
+
width: auto;
|
|
44
|
+
max-width: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.moodboard-chat__composer {
|
|
48
|
+
min-height: 0;
|
|
49
|
+
padding: 0 6px 6px 8px;
|
|
50
|
+
gap: 6px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* iOS не зумит инпут при font-size ≥ 16px */
|
|
54
|
+
.moodboard-chat__textarea {
|
|
55
|
+
font-size: 16px;
|
|
56
|
+
min-height: 40px;
|
|
57
|
+
height: 40px;
|
|
58
|
+
padding: 12px 0 4px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Пилюли и кнопка отправки переносятся, чтобы не выпадать за край */
|
|
62
|
+
.moodboard-chat__actions-row {
|
|
63
|
+
flex-wrap: wrap;
|
|
64
|
+
gap: 6px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.moodboard-chat__pills {
|
|
68
|
+
gap: 8px 10px;
|
|
69
|
+
flex-wrap: wrap;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* История диалога не должна занимать весь экран на телефоне */
|
|
73
|
+
.moodboard-chat__history {
|
|
74
|
+
max-height: 40vh;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
35
78
|
.moodboard-chat__history {
|
|
36
79
|
max-height: 360px;
|
|
37
80
|
overflow-y: auto;
|
package/src/ui/styles/topbar.css
CHANGED
|
@@ -111,3 +111,23 @@
|
|
|
111
111
|
.moodboard-topbar__button--zoom { width: 32px; height: 32px; font-size: 16px; }
|
|
112
112
|
.moodboard-topbar__zoom-label { min-width: 48px; text-align: center; font-size: 14px; }
|
|
113
113
|
|
|
114
|
+
/* Mobile (≤768px): топбар не должен выходить за край экрана. Сжимаем зазоры,
|
|
115
|
+
ограничиваем ширину вьюпортом и даём прокрутку внутри пилюли, если кнопок много. */
|
|
116
|
+
@media (max-width: 768px) {
|
|
117
|
+
.moodboard-topbar {
|
|
118
|
+
top: 8px;
|
|
119
|
+
left: 8px;
|
|
120
|
+
gap: 2px;
|
|
121
|
+
padding: 6px;
|
|
122
|
+
max-width: calc(100vw - 16px);
|
|
123
|
+
overflow-x: auto;
|
|
124
|
+
scrollbar-width: none; /* Firefox */
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.moodboard-topbar::-webkit-scrollbar { display: none; }
|
|
128
|
+
|
|
129
|
+
.moodboard-topbar__button { flex: 0 0 auto; }
|
|
130
|
+
|
|
131
|
+
.moodboard-topbar__divider { margin: 0 3px; }
|
|
132
|
+
}
|
|
133
|
+
|
|
@@ -676,6 +676,28 @@
|
|
|
676
676
|
border-radius: 1px;
|
|
677
677
|
}
|
|
678
678
|
|
|
679
|
+
/* Иконка запрета ввода при достижении лимита строк записки */
|
|
680
|
+
.moodboard-note-limit-ban {
|
|
681
|
+
position: absolute;
|
|
682
|
+
left: 50%;
|
|
683
|
+
top: 50%;
|
|
684
|
+
transform: translate(-50%, -50%);
|
|
685
|
+
width: 48px;
|
|
686
|
+
height: 48px;
|
|
687
|
+
align-items: center;
|
|
688
|
+
justify-content: center;
|
|
689
|
+
color: #b0b0b0;
|
|
690
|
+
opacity: 0.9;
|
|
691
|
+
pointer-events: none;
|
|
692
|
+
z-index: 10002;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.moodboard-note-limit-ban svg {
|
|
696
|
+
width: 100%;
|
|
697
|
+
height: 100%;
|
|
698
|
+
display: block;
|
|
699
|
+
}
|
|
700
|
+
|
|
679
701
|
@keyframes mb-caret-blink {
|
|
680
702
|
0%, 100% { opacity: 1; }
|
|
681
703
|
50% { opacity: 0; }
|
|
@@ -1575,9 +1597,42 @@
|
|
|
1575
1597
|
|
|
1576
1598
|
/* Responsive */
|
|
1577
1599
|
@media (max-width: 768px) {
|
|
1600
|
+
/* Тулбар прижимаем ближе к краю, чтобы оставить максимум холста.
|
|
1601
|
+
Высоту регулирует ToolbarResponsiveController (схлопывание в «Ещё»). */
|
|
1602
|
+
.moodboard-workspace__toolbar {
|
|
1603
|
+
left: 8px;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1578
1606
|
.moodboard-toolbar {
|
|
1579
|
-
|
|
1580
|
-
|
|
1607
|
+
padding: 6px;
|
|
1608
|
+
gap: 6px;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/* Зум- и карта-бары компактнее и ближе к краю на телефоне */
|
|
1612
|
+
.moodboard-zoombar {
|
|
1613
|
+
right: 72px;
|
|
1614
|
+
bottom: 16px;
|
|
1615
|
+
padding: 6px;
|
|
1616
|
+
gap: 6px;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.moodboard-mapbar {
|
|
1620
|
+
right: 8px;
|
|
1621
|
+
bottom: 16px;
|
|
1622
|
+
padding: 6px;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
.moodboard-zoombar__menu,
|
|
1626
|
+
.moodboard-mapbar__popup {
|
|
1627
|
+
right: 0;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/* Мини-карта не должна перекрывать половину экрана */
|
|
1631
|
+
.moodboard-mapbar__popup {
|
|
1632
|
+
width: 60vw;
|
|
1633
|
+
height: 28vh;
|
|
1634
|
+
min-width: 180px;
|
|
1635
|
+
min-height: 120px;
|
|
1581
1636
|
}
|
|
1582
1637
|
}
|
|
1583
1638
|
|
|
@@ -7,6 +7,16 @@ import {
|
|
|
7
7
|
import { createTextFormatControls } from './TextFormatControls.js';
|
|
8
8
|
import { createTextLockMoreControls } from './TextLockMoreControls.js';
|
|
9
9
|
|
|
10
|
+
const NO_COLOR_ICON_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 23.5C18.3513 23.5 23.5 18.3513 23.5 12C23.5 5.64873 18.3513 0.5 12 0.5C5.64873 0.5 0.5 5.64873 0.5 12C0.5 18.3513 5.64873 23.5 12 23.5Z" stroke="#B2B2B2" stroke-width="1.2"></path><path d="M4.27344 19.7266L19.7148 4.28516" stroke="#B2B2B2" stroke-width="1.2"></path></svg>`;
|
|
11
|
+
|
|
12
|
+
function createNoColorIcon(extraStyle = '') {
|
|
13
|
+
const wrapper = document.createElement('span');
|
|
14
|
+
wrapper.className = 'no-color-icon';
|
|
15
|
+
wrapper.style.cssText = `display:inline-flex;pointer-events:none;width:20px;height:20px;${extraStyle}`;
|
|
16
|
+
wrapper.innerHTML = NO_COLOR_ICON_SVG;
|
|
17
|
+
return wrapper;
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
export function createTextPropertiesPanelRenderer(panelInstance) {
|
|
11
21
|
const panel = document.createElement('div');
|
|
12
22
|
panel.className = 'text-properties-panel';
|
|
@@ -106,20 +116,18 @@ export function updateCurrentBgColorButton(panelInstance, color) {
|
|
|
106
116
|
line.remove();
|
|
107
117
|
}
|
|
108
118
|
|
|
109
|
-
if (!panelInstance.currentBgColorButton.querySelector('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
img.style.cssText = 'width: 20px; height: 20px; pointer-events: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);';
|
|
114
|
-
panelInstance.currentBgColorButton.appendChild(img);
|
|
119
|
+
if (!panelInstance.currentBgColorButton.querySelector('.no-color-icon')) {
|
|
120
|
+
panelInstance.currentBgColorButton.appendChild(
|
|
121
|
+
createNoColorIcon('position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);')
|
|
122
|
+
);
|
|
115
123
|
}
|
|
116
124
|
} else {
|
|
117
125
|
panelInstance.currentBgColorButton.style.backgroundColor = color;
|
|
118
126
|
panelInstance.currentBgColorButton.title = `Цвет выделения: ${color}`;
|
|
119
127
|
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
|
|
128
|
+
const icon = panelInstance.currentBgColorButton.querySelector('.no-color-icon');
|
|
129
|
+
if (icon) {
|
|
130
|
+
icon.remove();
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
const line = panelInstance.currentBgColorButton.querySelector('div');
|
|
@@ -514,11 +522,7 @@ function createHighlightColorGrid(panelInstance, container) {
|
|
|
514
522
|
position: relative;
|
|
515
523
|
`;
|
|
516
524
|
|
|
517
|
-
|
|
518
|
-
img.src = '/icons/no-color.svg';
|
|
519
|
-
img.className = 'no-color-icon';
|
|
520
|
-
img.style.cssText = 'width: 20px; height: 20px; pointer-events: none;';
|
|
521
|
-
colorButton.appendChild(img);
|
|
525
|
+
colorButton.appendChild(createNoColorIcon());
|
|
522
526
|
} else {
|
|
523
527
|
colorButton.style.cssText = `
|
|
524
528
|
width: 28px;
|
|
@@ -668,11 +672,7 @@ function createBackgroundColorGrid(panelInstance, container) {
|
|
|
668
672
|
position: relative;
|
|
669
673
|
`;
|
|
670
674
|
|
|
671
|
-
|
|
672
|
-
img.src = '/icons/no-color.svg';
|
|
673
|
-
img.className = 'no-color-icon';
|
|
674
|
-
img.style.cssText = 'width: 20px; height: 20px; pointer-events: none;';
|
|
675
|
-
colorButton.appendChild(img);
|
|
675
|
+
colorButton.appendChild(createNoColorIcon());
|
|
676
676
|
} else {
|
|
677
677
|
colorButton.style.cssText = `
|
|
678
678
|
width: 28px;
|