@sequent-org/moodboard 1.4.49 → 1.4.51
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/core/commands/ApplyCropCommand.js +90 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +9 -2
- package/src/core/flows/ObjectLifecycleFlow.js +25 -6
- package/src/objects/FrameObject.js +90 -8
- package/src/objects/ImageObject.js +23 -7
- package/src/services/CropController.js +242 -0
- package/src/services/FrameService.js +32 -0
- package/src/tools/manager/ToolManagerLifecycle.js +4 -0
- package/src/tools/object-tools/DrawingTool.js +1 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +50 -0
- package/src/ui/CropOverlay.js +654 -0
- package/src/ui/FramePropertiesPanel.js +1061 -407
- package/src/ui/HtmlHandlesLayer.js +2 -0
- package/src/ui/ImagePropertiesPanel.js +77 -41
- package/src/ui/connectors/ConnectionAnchorsLayer.js +4 -0
- package/src/ui/handles/HandlesDomRenderer.js +9 -2
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/styles/panels.css +362 -27
- package/src/ui/styles/workspace.css +21 -1
- package/src/utils/applyCrop.js +92 -0
|
@@ -27,6 +27,7 @@ export class HtmlHandlesLayer {
|
|
|
27
27
|
this.handles = {};
|
|
28
28
|
this._drag = null;
|
|
29
29
|
this._handlesSuppressed = false; // скрывать ручки во время перетаскивания/трансформаций
|
|
30
|
+
this._cropMode = false; // полное подавление во время crop (ни рамка, ни ручки)
|
|
30
31
|
this._groupRotationPreview = null;
|
|
31
32
|
|
|
32
33
|
// Ссылки на обработчики, чтобы корректно отписаться при destroy()
|
|
@@ -96,6 +97,7 @@ export class HtmlHandlesLayer {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
update() {
|
|
100
|
+
if (this._cropMode) return;
|
|
99
101
|
// Дополнительная защита: если слой или core уже уничтожены,
|
|
100
102
|
// выходим, чтобы не получить ошибок при resize/смене DPR
|
|
101
103
|
if (!this.core || !this.core.pixi || !this.core.pixi.app || !this.layer) return;
|
|
@@ -2,6 +2,7 @@ import * as PIXI from 'pixi.js';
|
|
|
2
2
|
import { Events } from '../core/events/Events.js';
|
|
3
3
|
import { EditFileNameCommand } from '../core/commands/EditFileNameCommand.js';
|
|
4
4
|
import { applyRoundedMask } from '../utils/applyRoundedMask.js';
|
|
5
|
+
import { CropController } from '../services/CropController.js';
|
|
5
6
|
|
|
6
7
|
const ICONS = {
|
|
7
8
|
download: `<svg width="24" height="24" fill="none" viewBox="0 0 24 24" type="download"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.75 14.75V16.25C4.75 17.9069 6.09315 19.25 7.75 19.25H16.25C17.9069 19.25 19.25 17.9069 19.25 16.25V14.75"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 14.25L12 4.75"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.75 10.75L12 14.25L15.25 10.75"></path></svg>`,
|
|
@@ -38,6 +39,9 @@ export class ImagePropertiesPanel {
|
|
|
38
39
|
this.currentId = null;
|
|
39
40
|
this._handlers = null;
|
|
40
41
|
|
|
42
|
+
this._cropController = new CropController({ core, eventBus, container });
|
|
43
|
+
this._cropController._onActivate = (active) => this._onCropActivate(active);
|
|
44
|
+
|
|
41
45
|
this._attachEvents();
|
|
42
46
|
this._createPanel();
|
|
43
47
|
}
|
|
@@ -125,6 +129,9 @@ export class ImagePropertiesPanel {
|
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
showFor(objectId) {
|
|
132
|
+
if (this.currentId !== objectId) {
|
|
133
|
+
this._cropController?.cancel();
|
|
134
|
+
}
|
|
128
135
|
this.currentId = objectId;
|
|
129
136
|
if (this.panel) {
|
|
130
137
|
this.panel.style.display = 'flex';
|
|
@@ -148,13 +155,12 @@ export class ImagePropertiesPanel {
|
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
_toggleLocked() {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
+
if (!this.currentId) return;
|
|
159
|
+
const newLocked = !this._isLocked();
|
|
160
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
161
|
+
objectId: this.currentId,
|
|
162
|
+
updates: { properties: { locked: newLocked } },
|
|
163
|
+
});
|
|
158
164
|
this._updateLockUI();
|
|
159
165
|
this.reposition();
|
|
160
166
|
}
|
|
@@ -163,7 +169,7 @@ export class ImagePropertiesPanel {
|
|
|
163
169
|
if (!this._btn_lock) return;
|
|
164
170
|
const locked = this._isLocked();
|
|
165
171
|
|
|
166
|
-
this._btn_lock.innerHTML = locked ? ICONS.
|
|
172
|
+
this._btn_lock.innerHTML = locked ? ICONS.lock : ICONS.unlock;
|
|
167
173
|
this._btn_lock.title = locked ? 'Разблокировать' : 'Заблокировать';
|
|
168
174
|
|
|
169
175
|
if (Array.isArray(this._lockableEls)) {
|
|
@@ -178,6 +184,7 @@ export class ImagePropertiesPanel {
|
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
hide() {
|
|
187
|
+
this._cropController?.cancel();
|
|
181
188
|
this.currentId = null;
|
|
182
189
|
if (this.panel) {
|
|
183
190
|
this.panel.style.display = 'none';
|
|
@@ -185,6 +192,36 @@ export class ImagePropertiesPanel {
|
|
|
185
192
|
this._closeBorderRadiusPopover();
|
|
186
193
|
}
|
|
187
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Вызывается CropController при входе/выходе из режима crop.
|
|
197
|
+
* Прячет тулбар и ручки выделения на время crop; восстанавливает по завершении.
|
|
198
|
+
*/
|
|
199
|
+
_onCropActivate(active) {
|
|
200
|
+
if (!this.panel) return;
|
|
201
|
+
|
|
202
|
+
const handlesLayer = typeof window !== 'undefined' ? window.moodboardHtmlHandlesLayer : null;
|
|
203
|
+
const anchorsLayer = typeof window !== 'undefined' ? window.moodboardConnectionAnchorsLayer : null;
|
|
204
|
+
|
|
205
|
+
if (active) {
|
|
206
|
+
this.panel.style.display = 'none';
|
|
207
|
+
if (handlesLayer) {
|
|
208
|
+
handlesLayer._cropMode = true;
|
|
209
|
+
handlesLayer.hide();
|
|
210
|
+
}
|
|
211
|
+
anchorsLayer?.update();
|
|
212
|
+
} else {
|
|
213
|
+
// Восстанавливаем тулбар только если объект ещё выделен
|
|
214
|
+
if (this.currentId) {
|
|
215
|
+
this.panel.style.display = 'flex';
|
|
216
|
+
}
|
|
217
|
+
if (handlesLayer) {
|
|
218
|
+
handlesLayer._cropMode = false;
|
|
219
|
+
handlesLayer.update();
|
|
220
|
+
}
|
|
221
|
+
anchorsLayer?.update();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
188
225
|
_closeBorderRadiusPopover() {
|
|
189
226
|
if (this._borderRadiusPopover) {
|
|
190
227
|
this._borderRadiusPopover.classList.remove('is-open');
|
|
@@ -316,12 +353,6 @@ export class ImagePropertiesPanel {
|
|
|
316
353
|
mainBtn.className = 'ipp-btn-split-main';
|
|
317
354
|
mainBtn.title = 'Обрезать';
|
|
318
355
|
mainBtn.innerHTML = ICONS.crop;
|
|
319
|
-
mainBtn.addEventListener('click', (e) => {
|
|
320
|
-
e.preventDefault();
|
|
321
|
-
e.stopPropagation();
|
|
322
|
-
// Логика применения текущего кропа
|
|
323
|
-
});
|
|
324
|
-
|
|
325
356
|
const expandBtn = document.createElement('button');
|
|
326
357
|
expandBtn.className = 'ipp-btn-split-expand';
|
|
327
358
|
expandBtn.title = 'Выбрать пропорции';
|
|
@@ -330,15 +361,36 @@ export class ImagePropertiesPanel {
|
|
|
330
361
|
const dropdown = document.createElement('div');
|
|
331
362
|
dropdown.className = 'ipp-crop-dropdown';
|
|
332
363
|
|
|
364
|
+
const toggleCropDropdown = (e) => {
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
e.stopPropagation();
|
|
367
|
+
const isOpen = dropdown.classList.contains('is-open');
|
|
368
|
+
|
|
369
|
+
document.querySelectorAll('.ipp-crop-dropdown.is-open, .ipp-more-dropdown.is-open, .ipp-border-radius-popover.is-open, .ipp-border-style-popover.is-open').forEach(el => {
|
|
370
|
+
el.classList.remove('is-open');
|
|
371
|
+
});
|
|
372
|
+
document.querySelectorAll('.ipp-btn-split-expand.is-expanded, .ipp-btn.is-active').forEach(el => {
|
|
373
|
+
el.classList.remove('is-expanded', 'is-active');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (!isOpen) {
|
|
377
|
+
dropdown.classList.add('is-open');
|
|
378
|
+
expandBtn.classList.add('is-expanded');
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
mainBtn.addEventListener('click', toggleCropDropdown);
|
|
383
|
+
expandBtn.addEventListener('click', toggleCropDropdown);
|
|
384
|
+
|
|
333
385
|
const items = [
|
|
334
|
-
{ id: 'custom', label: '
|
|
335
|
-
{ id: 'original', label: '
|
|
386
|
+
{ id: 'custom', label: 'Произвольный', icon: ICONS.ddCustom },
|
|
387
|
+
{ id: 'original', label: 'Оригинал', icon: ICONS.ddOriginal },
|
|
336
388
|
{ divider: true },
|
|
337
|
-
{ id: 'circle', label: '
|
|
338
|
-
{ id: 'square', label: '
|
|
339
|
-
{ id: 'portrait', label: '
|
|
340
|
-
{ id: 'landscape', label: '
|
|
341
|
-
{ id: 'wide', label: '
|
|
389
|
+
{ id: 'circle', label: 'Круг', icon: ICONS.ddCircle },
|
|
390
|
+
{ id: 'square', label: 'Квадрат', icon: ICONS.ddSquare },
|
|
391
|
+
{ id: 'portrait', label: 'Портрет', icon: ICONS.ddPortrait, ratio: '3:4' },
|
|
392
|
+
{ id: 'landscape', label: 'Пейзаж', icon: ICONS.ddLandscape, ratio: '4:3' },
|
|
393
|
+
{ id: 'wide', label: 'Широкий', icon: ICONS.ddWide, ratio: '16:9' },
|
|
342
394
|
];
|
|
343
395
|
|
|
344
396
|
items.forEach(item => {
|
|
@@ -372,33 +424,16 @@ export class ImagePropertiesPanel {
|
|
|
372
424
|
btn.addEventListener('click', (e) => {
|
|
373
425
|
e.preventDefault();
|
|
374
426
|
e.stopPropagation();
|
|
375
|
-
// Логика выбора пропорции
|
|
376
427
|
dropdown.classList.remove('is-open');
|
|
377
428
|
expandBtn.classList.remove('is-expanded');
|
|
429
|
+
if (this.currentId) {
|
|
430
|
+
this._cropController.start(this.currentId, item.id);
|
|
431
|
+
}
|
|
378
432
|
});
|
|
379
433
|
|
|
380
434
|
dropdown.appendChild(btn);
|
|
381
435
|
});
|
|
382
436
|
|
|
383
|
-
expandBtn.addEventListener('click', (e) => {
|
|
384
|
-
e.preventDefault();
|
|
385
|
-
e.stopPropagation();
|
|
386
|
-
const isOpen = dropdown.classList.contains('is-open');
|
|
387
|
-
|
|
388
|
-
// Закрываем все другие дропдауны, если есть (на будущее)
|
|
389
|
-
document.querySelectorAll('.ipp-crop-dropdown.is-open, .ipp-more-dropdown.is-open').forEach(el => {
|
|
390
|
-
el.classList.remove('is-open');
|
|
391
|
-
});
|
|
392
|
-
document.querySelectorAll('.ipp-btn-split-expand.is-expanded, .ipp-btn.is-active').forEach(el => {
|
|
393
|
-
el.classList.remove('is-expanded', 'is-active');
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
if (!isOpen) {
|
|
397
|
-
dropdown.classList.add('is-open');
|
|
398
|
-
expandBtn.classList.add('is-expanded');
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
437
|
wrapper.appendChild(mainBtn);
|
|
403
438
|
wrapper.appendChild(expandBtn);
|
|
404
439
|
wrapper.appendChild(dropdown);
|
|
@@ -1442,6 +1477,7 @@ export class ImagePropertiesPanel {
|
|
|
1442
1477
|
}
|
|
1443
1478
|
|
|
1444
1479
|
destroy() {
|
|
1480
|
+
this._cropController?.destroy();
|
|
1445
1481
|
if (!this.eventBus || !this._handlers) return;
|
|
1446
1482
|
|
|
1447
1483
|
this.eventBus.off(Events.Tool.SelectionAdd, this._handlers.onSelectionAdd);
|
|
@@ -144,6 +144,10 @@ export class ConnectionAnchorsLayer {
|
|
|
144
144
|
update() {
|
|
145
145
|
if (!this.layer) return;
|
|
146
146
|
if (this._commentPopoverOpen) return;
|
|
147
|
+
if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer?._cropMode) {
|
|
148
|
+
this.layer.innerHTML = '';
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
147
151
|
this.layer.innerHTML = '';
|
|
148
152
|
|
|
149
153
|
const selection = Array.from(this.core?.selectTool?.selectedObjects || []);
|
|
@@ -1157,6 +1157,7 @@ export class HandlesDomRenderer {
|
|
|
1157
1157
|
let isFrameTarget = false;
|
|
1158
1158
|
let isMindmapTarget = false;
|
|
1159
1159
|
let isMindmapOnlyGroupTarget = false;
|
|
1160
|
+
let isLockedTarget = false;
|
|
1160
1161
|
let isRevitScreenshotTarget = false;
|
|
1161
1162
|
let revitViewPayload = null;
|
|
1162
1163
|
let isModel3dScreenshotTarget = false;
|
|
@@ -1167,11 +1168,12 @@ export class HandlesDomRenderer {
|
|
|
1167
1168
|
const hiddenIncomingSide = { value: null };
|
|
1168
1169
|
if (id !== '__group__') {
|
|
1169
1170
|
const req = { objectId: id, pixiObject: null };
|
|
1170
|
-
this.host.eventBus.emit(
|
|
1171
|
+
this.host.eventBus.emit('tool:get:object:pixi', req);
|
|
1171
1172
|
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
1172
1173
|
isFileTarget = mbType === 'file';
|
|
1173
1174
|
isFrameTarget = mbType === 'frame';
|
|
1174
1175
|
isMindmapTarget = mbType === 'mindmap';
|
|
1176
|
+
isLockedTarget = !!req.pixiObject?._mb?.properties?.locked;
|
|
1175
1177
|
isRevitScreenshotTarget = mbType === 'revit-screenshot-img';
|
|
1176
1178
|
revitViewPayload = req.pixiObject?._mb?.properties?.view || null;
|
|
1177
1179
|
isModel3dScreenshotTarget = mbType === 'model3d-screenshot-img';
|
|
@@ -1199,9 +1201,14 @@ export class HandlesDomRenderer {
|
|
|
1199
1201
|
if (selectionIds.length > 0) {
|
|
1200
1202
|
const byId = new Map((this.host.core?.state?.state?.objects || []).map((obj) => [obj?.id, obj]));
|
|
1201
1203
|
isMindmapOnlyGroupTarget = selectionIds.every((selectedId) => byId.get(selectedId)?.type === 'mindmap');
|
|
1204
|
+
isLockedTarget = selectionIds.some(selId => {
|
|
1205
|
+
const req = { objectId: selId, pixiObject: null };
|
|
1206
|
+
this.host.eventBus.emit('tool:get:object:pixi', req);
|
|
1207
|
+
return !!req.pixiObject?._mb?.properties?.locked;
|
|
1208
|
+
});
|
|
1202
1209
|
}
|
|
1203
1210
|
}
|
|
1204
|
-
const isNonResizableTarget = isFileTarget || isMindmapTarget || isMindmapOnlyGroupTarget;
|
|
1211
|
+
const isNonResizableTarget = isFileTarget || isMindmapTarget || isMindmapOnlyGroupTarget || isLockedTarget;
|
|
1205
1212
|
|
|
1206
1213
|
const left = Math.round(cssRect.left);
|
|
1207
1214
|
const top = Math.round(cssRect.top);
|
|
@@ -114,6 +114,7 @@ export class HandlesEventBridge {
|
|
|
114
114
|
[Events.UI.ZoomPercent, () => this.host.update()],
|
|
115
115
|
[Events.Tool.PanUpdate, () => this.host.update()],
|
|
116
116
|
[Events.Viewport.Changed, () => this.host.update()],
|
|
117
|
+
[Events.Object.TransformUpdated, () => this.host.update()],
|
|
117
118
|
[Events.History.Changed, (data) => {
|
|
118
119
|
if (data?.lastUndone || data?.lastRedone) {
|
|
119
120
|
this.host._endGroupRotationPreview();
|
package/src/ui/styles/panels.css
CHANGED
|
@@ -206,49 +206,119 @@
|
|
|
206
206
|
height: 36px;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
/* Frame properties
|
|
209
|
+
/* Frame properties toolbar — mirror image toolbar look */
|
|
210
210
|
.frame-properties-panel {
|
|
211
211
|
position: absolute;
|
|
212
212
|
pointer-events: auto;
|
|
213
|
-
display:
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-direction: row;
|
|
214
215
|
align-items: center;
|
|
215
|
-
gap:
|
|
216
|
-
padding:
|
|
217
|
-
background
|
|
218
|
-
border: 1px solid #
|
|
219
|
-
border-radius:
|
|
220
|
-
box-shadow: 0
|
|
216
|
+
gap: 2px;
|
|
217
|
+
padding: 4px 8px;
|
|
218
|
+
background: #ffffff;
|
|
219
|
+
border: 1px solid #E5E7EB;
|
|
220
|
+
border-radius: 8px;
|
|
221
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
222
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
221
223
|
font-size: 13px;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
z-index: 3000;
|
|
225
|
+
user-select: none;
|
|
226
|
+
white-space: nowrap;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Ratio split-dropdown (двухстрочная кнопка «Ratio / значение») */
|
|
230
|
+
.fpp-ratio {
|
|
231
|
+
position: relative;
|
|
232
|
+
display: inline-flex;
|
|
233
|
+
flex-shrink: 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.fpp-ratio-btn {
|
|
237
|
+
display: inline-flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 6px;
|
|
224
240
|
height: 36px;
|
|
241
|
+
padding: 0 8px;
|
|
242
|
+
border: none;
|
|
243
|
+
background: none;
|
|
244
|
+
border-radius: 6px;
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
color: #374151;
|
|
247
|
+
font-family: inherit;
|
|
248
|
+
transition: background 0.15s;
|
|
225
249
|
}
|
|
226
250
|
|
|
227
|
-
.
|
|
228
|
-
|
|
229
|
-
font-size: 12px;
|
|
230
|
-
color: #666;
|
|
231
|
-
font-weight: 500;
|
|
251
|
+
.fpp-ratio-btn:hover {
|
|
252
|
+
background: #F3F4F6;
|
|
232
253
|
}
|
|
233
254
|
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
255
|
+
.fpp-ratio-text {
|
|
256
|
+
display: flex;
|
|
257
|
+
flex-direction: column;
|
|
258
|
+
align-items: flex-start;
|
|
259
|
+
line-height: 1.15;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.fpp-ratio-caption {
|
|
263
|
+
font-size: 10px;
|
|
264
|
+
color: #9CA3AF;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.fpp-ratio-value {
|
|
239
268
|
font-size: 13px;
|
|
240
|
-
|
|
269
|
+
color: #374151;
|
|
270
|
+
font-weight: 500;
|
|
241
271
|
}
|
|
242
272
|
|
|
243
|
-
.
|
|
244
|
-
|
|
245
|
-
|
|
273
|
+
.fpp-ratio-btn svg {
|
|
274
|
+
color: #6B7280;
|
|
275
|
+
transition: transform 0.2s ease;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.fpp-ratio-btn.is-active svg {
|
|
279
|
+
transform: rotate(180deg);
|
|
246
280
|
}
|
|
247
281
|
|
|
248
|
-
.
|
|
282
|
+
.fpp-ratio-dropdown {
|
|
283
|
+
position: absolute;
|
|
284
|
+
top: calc(100% + 6px);
|
|
285
|
+
left: 0;
|
|
286
|
+
background: #ffffff;
|
|
287
|
+
border: 1px solid #E5E7EB;
|
|
288
|
+
border-radius: 8px;
|
|
289
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 10px rgba(0, 0, 0, 0.05);
|
|
290
|
+
padding: 6px;
|
|
291
|
+
display: none;
|
|
292
|
+
flex-direction: column;
|
|
293
|
+
min-width: 180px;
|
|
294
|
+
z-index: 1001;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.fpp-ratio-dropdown.is-open {
|
|
298
|
+
display: flex;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* Кружок цвета фона */
|
|
302
|
+
.fpp-color-wrap {
|
|
303
|
+
display: inline-flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: center;
|
|
249
306
|
width: 28px;
|
|
250
307
|
height: 28px;
|
|
251
|
-
border:
|
|
308
|
+
border-radius: 6px;
|
|
309
|
+
cursor: pointer;
|
|
310
|
+
flex-shrink: 0;
|
|
311
|
+
transition: background 0.15s;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.fpp-color-wrap:hover {
|
|
315
|
+
background: #F3F4F6;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.fpp-color-button {
|
|
319
|
+
width: 20px;
|
|
320
|
+
height: 20px;
|
|
321
|
+
border: 1px solid #D1D5DB;
|
|
252
322
|
border-radius: 50%;
|
|
253
323
|
background-color: #ffffff;
|
|
254
324
|
cursor: pointer;
|
|
@@ -256,9 +326,274 @@
|
|
|
256
326
|
padding: 0;
|
|
257
327
|
display: block;
|
|
258
328
|
box-sizing: border-box;
|
|
329
|
+
position: relative;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* transparent indicator — diagonal slash via pseudo */
|
|
333
|
+
.fpp-color-button--none {
|
|
334
|
+
background-color: #ffffff !important;
|
|
335
|
+
border-color: #D1D5DB;
|
|
336
|
+
}
|
|
337
|
+
.fpp-color-button--none::after {
|
|
338
|
+
content: '';
|
|
339
|
+
position: absolute;
|
|
340
|
+
inset: 0;
|
|
341
|
+
background: linear-gradient(to top-right,
|
|
342
|
+
transparent calc(50% - 0.75px),
|
|
343
|
+
#EF4444 calc(50% - 0.75px),
|
|
344
|
+
#EF4444 calc(50% + 0.75px),
|
|
345
|
+
transparent calc(50% + 0.75px)
|
|
346
|
+
);
|
|
347
|
+
border-radius: 50%;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
351
|
+
Colour popup dropdown
|
|
352
|
+
═══════════════════════════════════════════════════════════════ */
|
|
353
|
+
|
|
354
|
+
.fpp-color-popup {
|
|
355
|
+
position: absolute;
|
|
356
|
+
background: #ffffff;
|
|
357
|
+
border: 1px solid #E5E7EB;
|
|
358
|
+
border-radius: 12px;
|
|
359
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.10), 0 4px 10px rgba(0,0,0,0.05);
|
|
360
|
+
padding: 12px;
|
|
361
|
+
display: none;
|
|
362
|
+
flex-direction: column;
|
|
363
|
+
gap: 14px;
|
|
364
|
+
min-width: 196px;
|
|
365
|
+
z-index: 1001;
|
|
366
|
+
box-sizing: border-box;
|
|
367
|
+
}
|
|
368
|
+
.fpp-color-popup.is-open { display: flex; }
|
|
369
|
+
|
|
370
|
+
.fpp-section { display: flex; flex-direction: column; gap: 8px; }
|
|
371
|
+
|
|
372
|
+
.fpp-section-label {
|
|
373
|
+
font-family: 'Roboto', Arial, sans-serif;
|
|
374
|
+
font-size: 11px;
|
|
375
|
+
font-weight: 500;
|
|
376
|
+
color: #9CA3AF;
|
|
377
|
+
letter-spacing: 0.02em;
|
|
378
|
+
user-select: none;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* Background type row */
|
|
382
|
+
.fpp-bg-type-row { display: flex; gap: 4px; }
|
|
383
|
+
|
|
384
|
+
.fpp-bg-type-btn {
|
|
385
|
+
display: inline-flex;
|
|
386
|
+
align-items: center;
|
|
387
|
+
justify-content: center;
|
|
388
|
+
width: 36px;
|
|
389
|
+
height: 32px;
|
|
390
|
+
border: 1.5px solid transparent;
|
|
391
|
+
border-radius: 6px;
|
|
392
|
+
background: #F9FAFB;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
color: #374151;
|
|
395
|
+
padding: 0;
|
|
396
|
+
transition: background 0.12s, border-color 0.12s;
|
|
397
|
+
}
|
|
398
|
+
.fpp-bg-type-btn:hover { background: #F3F4F6; border-color: #E5E7EB; }
|
|
399
|
+
.fpp-bg-type-btn.is-active { border-color: #374151; background: #F3F4F6; }
|
|
400
|
+
|
|
401
|
+
/* Color swatch grid */
|
|
402
|
+
.fpp-color-grid {
|
|
403
|
+
display: grid;
|
|
404
|
+
grid-template-columns: repeat(6, 28px);
|
|
405
|
+
gap: 6px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.fpp-color-swatch {
|
|
409
|
+
width: 28px;
|
|
410
|
+
height: 28px;
|
|
411
|
+
border-radius: 50%;
|
|
412
|
+
border: 1.5px solid rgba(0,0,0,0.12);
|
|
413
|
+
background-color: var(--swatch-color, #ffffff);
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
padding: 0;
|
|
416
|
+
margin: 0;
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: center;
|
|
420
|
+
transition: transform 0.10s, box-shadow 0.10s;
|
|
421
|
+
flex-shrink: 0;
|
|
422
|
+
}
|
|
423
|
+
.fpp-color-swatch:hover { transform: scale(1.12); box-shadow: 0 2px 8px rgba(0,0,0,0.18); }
|
|
424
|
+
.fpp-color-swatch.is-selected { box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #374151; }
|
|
425
|
+
|
|
426
|
+
.fpp-color-swatch--none {
|
|
427
|
+
background-color: #ffffff;
|
|
428
|
+
border-color: #D1D5DB;
|
|
429
|
+
}
|
|
430
|
+
.fpp-color-swatch--none svg { pointer-events: none; }
|
|
431
|
+
|
|
432
|
+
/* Disabled colour section when transparent is selected */
|
|
433
|
+
.fpp-section--color.is-disabled .fpp-color-swatch:not(.fpp-color-swatch--none) {
|
|
434
|
+
opacity: 0.32;
|
|
435
|
+
pointer-events: none;
|
|
436
|
+
cursor: default;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Custom colours row */
|
|
440
|
+
.fpp-custom-row {
|
|
441
|
+
display: flex;
|
|
442
|
+
flex-wrap: wrap;
|
|
443
|
+
gap: 6px;
|
|
444
|
+
align-items: center;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.fpp-add-color-btn {
|
|
448
|
+
width: 28px;
|
|
449
|
+
height: 28px;
|
|
450
|
+
border-radius: 50%;
|
|
451
|
+
border: 1.5px dashed #D1D5DB;
|
|
452
|
+
background: transparent;
|
|
453
|
+
cursor: pointer;
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
justify-content: center;
|
|
457
|
+
color: #9CA3AF;
|
|
458
|
+
padding: 0;
|
|
459
|
+
flex-shrink: 0;
|
|
460
|
+
transition: border-color 0.12s, color 0.12s;
|
|
461
|
+
}
|
|
462
|
+
.fpp-add-color-btn:hover { border-color: #9CA3AF; color: #374151; }
|
|
463
|
+
|
|
464
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
465
|
+
HSV colour picker
|
|
466
|
+
═══════════════════════════════════════════════════════════════ */
|
|
467
|
+
|
|
468
|
+
.fpp-color-picker {
|
|
469
|
+
position: absolute;
|
|
470
|
+
background: #ffffff;
|
|
471
|
+
border: 1px solid #E5E7EB;
|
|
472
|
+
border-radius: 12px;
|
|
473
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.12);
|
|
474
|
+
padding: 12px;
|
|
475
|
+
display: none;
|
|
476
|
+
flex-direction: column;
|
|
477
|
+
gap: 10px;
|
|
478
|
+
z-index: 1002;
|
|
479
|
+
width: 236px;
|
|
480
|
+
box-sizing: border-box;
|
|
259
481
|
}
|
|
482
|
+
.fpp-color-picker.is-open { display: flex; }
|
|
260
483
|
|
|
261
|
-
.
|
|
484
|
+
.fpp-color-picker__canvas-wrap {
|
|
485
|
+
position: relative;
|
|
486
|
+
width: 212px;
|
|
487
|
+
height: 148px;
|
|
488
|
+
border-radius: 6px;
|
|
489
|
+
overflow: hidden;
|
|
490
|
+
cursor: crosshair;
|
|
491
|
+
flex-shrink: 0;
|
|
492
|
+
}
|
|
493
|
+
.fpp-color-picker__canvas {
|
|
494
|
+
display: block;
|
|
495
|
+
width: 212px;
|
|
496
|
+
height: 148px;
|
|
497
|
+
}
|
|
498
|
+
.fpp-color-picker__cursor {
|
|
499
|
+
position: absolute;
|
|
500
|
+
width: 14px;
|
|
501
|
+
height: 14px;
|
|
502
|
+
border-radius: 50%;
|
|
503
|
+
border: 2px solid #ffffff;
|
|
504
|
+
box-shadow: 0 0 0 1px rgba(0,0,0,0.30);
|
|
505
|
+
transform: translate(-50%, -50%);
|
|
506
|
+
pointer-events: none;
|
|
507
|
+
top: 0;
|
|
508
|
+
left: 0;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.fpp-color-picker__hue-wrap { width: 100%; }
|
|
512
|
+
|
|
513
|
+
.fpp-color-picker__hue-slider {
|
|
514
|
+
-webkit-appearance: none;
|
|
515
|
+
appearance: none;
|
|
516
|
+
width: 100%;
|
|
517
|
+
height: 10px;
|
|
518
|
+
border-radius: 5px;
|
|
519
|
+
background: linear-gradient(to right,
|
|
520
|
+
hsl(0,100%,50%), hsl(30,100%,50%), hsl(60,100%,50%), hsl(90,100%,50%),
|
|
521
|
+
hsl(120,100%,50%), hsl(150,100%,50%), hsl(180,100%,50%), hsl(210,100%,50%),
|
|
522
|
+
hsl(240,100%,50%), hsl(270,100%,50%), hsl(300,100%,50%), hsl(330,100%,50%),
|
|
523
|
+
hsl(360,100%,50%));
|
|
524
|
+
outline: none;
|
|
525
|
+
cursor: pointer;
|
|
526
|
+
border: none;
|
|
527
|
+
}
|
|
528
|
+
.fpp-color-picker__hue-slider::-webkit-slider-thumb {
|
|
529
|
+
-webkit-appearance: none;
|
|
530
|
+
width: 16px;
|
|
531
|
+
height: 16px;
|
|
532
|
+
border-radius: 50%;
|
|
533
|
+
background: #ffffff;
|
|
534
|
+
border: 2px solid rgba(0,0,0,0.28);
|
|
535
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.20);
|
|
536
|
+
cursor: pointer;
|
|
537
|
+
}
|
|
538
|
+
.fpp-color-picker__hue-slider::-moz-range-thumb {
|
|
539
|
+
width: 16px;
|
|
540
|
+
height: 16px;
|
|
541
|
+
border-radius: 50%;
|
|
542
|
+
background: #ffffff;
|
|
543
|
+
border: 2px solid rgba(0,0,0,0.28);
|
|
544
|
+
cursor: pointer;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.fpp-color-picker__bottom {
|
|
548
|
+
display: flex;
|
|
549
|
+
align-items: center;
|
|
550
|
+
gap: 8px;
|
|
551
|
+
}
|
|
552
|
+
.fpp-color-picker__eyedropper {
|
|
553
|
+
width: 30px;
|
|
554
|
+
height: 30px;
|
|
555
|
+
border: 1px solid #E5E7EB;
|
|
556
|
+
border-radius: 6px;
|
|
557
|
+
background: transparent;
|
|
558
|
+
display: flex;
|
|
559
|
+
align-items: center;
|
|
560
|
+
justify-content: center;
|
|
561
|
+
cursor: pointer;
|
|
562
|
+
color: #6B7280;
|
|
563
|
+
padding: 0;
|
|
564
|
+
flex-shrink: 0;
|
|
565
|
+
transition: background 0.12s, color 0.12s;
|
|
566
|
+
}
|
|
567
|
+
.fpp-color-picker__eyedropper:hover { background: #F3F4F6; color: #374151; }
|
|
568
|
+
|
|
569
|
+
.fpp-color-picker__hex-input {
|
|
570
|
+
flex: 1;
|
|
571
|
+
height: 30px;
|
|
572
|
+
border: 1px solid #E5E7EB;
|
|
573
|
+
border-radius: 6px;
|
|
574
|
+
padding: 0 8px;
|
|
575
|
+
font-size: 13px;
|
|
576
|
+
font-family: 'Roboto Mono', 'Courier New', monospace;
|
|
577
|
+
color: #374151;
|
|
578
|
+
outline: none;
|
|
579
|
+
background: #ffffff;
|
|
580
|
+
}
|
|
581
|
+
.fpp-color-picker__hex-input:focus { border-color: #9CA3AF; }
|
|
582
|
+
|
|
583
|
+
.fpp-color-picker__apply {
|
|
584
|
+
width: 100%;
|
|
585
|
+
height: 30px;
|
|
586
|
+
border: none;
|
|
587
|
+
border-radius: 6px;
|
|
588
|
+
background: #374151;
|
|
589
|
+
color: #ffffff;
|
|
590
|
+
font-size: 13px;
|
|
591
|
+
font-family: 'Roboto', Arial, sans-serif;
|
|
592
|
+
font-weight: 500;
|
|
593
|
+
cursor: pointer;
|
|
594
|
+
transition: background 0.12s;
|
|
595
|
+
}
|
|
596
|
+
.fpp-color-picker__apply:hover { background: #1F2937; }
|
|
262
597
|
|
|
263
598
|
.text-properties-panel .tpp-label {
|
|
264
599
|
font-family: 'Roboto', Arial, sans-serif;
|