@node-projects/web-component-designer 0.2.17 → 0.2.18
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/dist/elements/helper/getBoxQuads.js +91 -6
- package/dist/elements/helper/getBoxQuads.js.map +1 -1
- package/dist/elements/services/DefaultServiceBootstrap.js +3 -0
- package/dist/elements/services/DefaultServiceBootstrap.js.map +1 -1
- package/dist/elements/widgets/designerView/extensions/contextMenu/ToolWindowsContextMenu.d.ts +8 -0
- package/dist/elements/widgets/designerView/extensions/contextMenu/ToolWindowsContextMenu.js +54 -0
- package/dist/elements/widgets/designerView/extensions/contextMenu/ToolWindowsContextMenu.js.map +1 -0
- package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.d.ts +1 -0
- package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.js +19 -1
- package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.js.map +1 -1
- package/dist/elements/widgets/designerView/tools/toolBar/DesignerToolbar.js +7 -1
- package/dist/elements/widgets/designerView/tools/toolBar/DesignerToolbar.js.map +1 -1
- package/dist/elements/widgets/designerView/tools/toolBar/popups/BorderRadiusEditorWindow.d.ts +82 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/BorderRadiusEditorWindow.js +1260 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/BorderRadiusEditorWindow.js.map +1 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/BoxShadowEditorWindow.d.ts +38 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/BoxShadowEditorWindow.js +518 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/BoxShadowEditorWindow.js.map +1 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/DraggableToolWindow.d.ts +39 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/DraggableToolWindow.js +181 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/DraggableToolWindow.js.map +1 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/GradientEditorWindow.d.ts +53 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/GradientEditorWindow.js +753 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/GradientEditorWindow.js.map +1 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/TextShadowEditorWindow.d.ts +36 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/TextShadowEditorWindow.js +449 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/TextShadowEditorWindow.js.map +1 -0
- package/dist/elements/widgets/designerView/tools/toolBar/popups/TransformToolPopup.d.ts +8 -6
- package/dist/elements/widgets/designerView/tools/toolBar/popups/TransformToolPopup.js +56 -72
- package/dist/elements/widgets/designerView/tools/toolBar/popups/TransformToolPopup.js.map +1 -1
- package/dist/index-min.js +544 -7
- package/dist/index-min.js.map +4 -4
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import { css } from '@node-projects/base-custom-webcomponent';
|
|
2
|
+
import { DraggableToolWindow } from './DraggableToolWindow.js';
|
|
3
|
+
function defaultConfig() {
|
|
4
|
+
return {
|
|
5
|
+
type: 'linear',
|
|
6
|
+
angle: 135,
|
|
7
|
+
radialShape: 'ellipse',
|
|
8
|
+
radialSize: 'farthest-corner',
|
|
9
|
+
posX: 50,
|
|
10
|
+
posY: 50,
|
|
11
|
+
stops: [
|
|
12
|
+
{ color: '#667eea', opacity: 100, position: 0 },
|
|
13
|
+
{ color: '#764ba2', opacity: 100, position: 100 },
|
|
14
|
+
],
|
|
15
|
+
repeating: false,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function hexToRgb(hex) {
|
|
19
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
20
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
21
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
22
|
+
return `${r},${g},${b}`;
|
|
23
|
+
}
|
|
24
|
+
function rgbToHex(r, g, b) {
|
|
25
|
+
return '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('');
|
|
26
|
+
}
|
|
27
|
+
function stopToCss(s) {
|
|
28
|
+
return `rgba(${hexToRgb(s.color)},${(s.opacity / 100).toFixed(2)}) ${s.position}%`;
|
|
29
|
+
}
|
|
30
|
+
function configToCss(cfg) {
|
|
31
|
+
const sorted = [...cfg.stops].sort((a, b) => a.position - b.position);
|
|
32
|
+
const stops = sorted.map(stopToCss).join(', ');
|
|
33
|
+
const prefix = cfg.repeating ? 'repeating-' : '';
|
|
34
|
+
if (cfg.type === 'linear') {
|
|
35
|
+
return `${prefix}linear-gradient(${cfg.angle}deg, ${stops})`;
|
|
36
|
+
}
|
|
37
|
+
else if (cfg.type === 'radial') {
|
|
38
|
+
return `${prefix}radial-gradient(${cfg.radialShape} ${cfg.radialSize} at ${cfg.posX}% ${cfg.posY}%, ${stops})`;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
return `${prefix}conic-gradient(from ${cfg.angle}deg at ${cfg.posX}% ${cfg.posY}%, ${stops})`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extracts the first gradient function from a CSS value string,
|
|
46
|
+
* using bracket counting to correctly handle nested functions like rgba().
|
|
47
|
+
*/
|
|
48
|
+
function extractFirstGradient(value) {
|
|
49
|
+
const keywords = ['repeating-linear-gradient', 'repeating-radial-gradient', 'repeating-conic-gradient',
|
|
50
|
+
'linear-gradient', 'radial-gradient', 'conic-gradient'];
|
|
51
|
+
for (const kw of keywords) {
|
|
52
|
+
const idx = value.indexOf(kw + '(');
|
|
53
|
+
if (idx === -1)
|
|
54
|
+
continue;
|
|
55
|
+
const start = idx + kw.length; // points to '('
|
|
56
|
+
let depth = 0, end = start;
|
|
57
|
+
for (let i = start; i < value.length; i++) {
|
|
58
|
+
if (value[i] === '(')
|
|
59
|
+
depth++;
|
|
60
|
+
else if (value[i] === ')') {
|
|
61
|
+
depth--;
|
|
62
|
+
if (depth === 0) {
|
|
63
|
+
end = i;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return value.slice(idx, end + 1);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// ─── Parser ────────────────────────────────────────────────────────────────
|
|
73
|
+
function splitTopLevel(s) {
|
|
74
|
+
const result = [];
|
|
75
|
+
let depth = 0, current = '';
|
|
76
|
+
for (const ch of s) {
|
|
77
|
+
if (ch === '(')
|
|
78
|
+
depth++;
|
|
79
|
+
else if (ch === ')')
|
|
80
|
+
depth--;
|
|
81
|
+
else if (ch === ',' && depth === 0) {
|
|
82
|
+
result.push(current);
|
|
83
|
+
current = '';
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
current += ch;
|
|
87
|
+
}
|
|
88
|
+
if (current)
|
|
89
|
+
result.push(current);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
function parseGradientStop(token) {
|
|
93
|
+
let color = '#000000', opacity = 100, position = 0;
|
|
94
|
+
let remaining = token.trim();
|
|
95
|
+
const colorFnMatch = remaining.match(/^(rgba?\([^)]+\)|hsla?\([^)]+\))/);
|
|
96
|
+
if (colorFnMatch) {
|
|
97
|
+
const fn = colorFnMatch[1];
|
|
98
|
+
remaining = remaining.slice(fn.length).trim();
|
|
99
|
+
const m = fn.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
100
|
+
if (m) {
|
|
101
|
+
color = rgbToHex(parseInt(m[1]), parseInt(m[2]), parseInt(m[3]));
|
|
102
|
+
opacity = m[4] != null ? Math.round(parseFloat(m[4]) * 100) : 100;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const hexMatch = remaining.match(/^(#[0-9a-fA-F]{3,8})/);
|
|
107
|
+
if (hexMatch) {
|
|
108
|
+
color = hexMatch[1].length === 4
|
|
109
|
+
? '#' + hexMatch[1].slice(1).split('').map(c => c + c).join('')
|
|
110
|
+
: hexMatch[1].slice(0, 7);
|
|
111
|
+
remaining = remaining.slice(hexMatch[1].length).trim();
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
return null; // named color – skip
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const posMatch = remaining.match(/([\d.]+)%/);
|
|
118
|
+
if (posMatch)
|
|
119
|
+
position = parseFloat(posMatch[1]);
|
|
120
|
+
return { color, opacity, position };
|
|
121
|
+
}
|
|
122
|
+
function looksLikeStop(token) {
|
|
123
|
+
const t = token.trim();
|
|
124
|
+
return t.startsWith('#') || /^rgba?\(/.test(t) || /^hsla?\(/.test(t);
|
|
125
|
+
}
|
|
126
|
+
function parseGradientCss(value) {
|
|
127
|
+
const cfg = defaultConfig();
|
|
128
|
+
if (!value)
|
|
129
|
+
return cfg;
|
|
130
|
+
const trimmed = value.trim();
|
|
131
|
+
cfg.repeating = trimmed.startsWith('repeating-');
|
|
132
|
+
let inner = cfg.repeating ? trimmed.slice('repeating-'.length) : trimmed;
|
|
133
|
+
if (inner.startsWith('linear-gradient'))
|
|
134
|
+
cfg.type = 'linear';
|
|
135
|
+
else if (inner.startsWith('radial-gradient'))
|
|
136
|
+
cfg.type = 'radial';
|
|
137
|
+
else if (inner.startsWith('conic-gradient'))
|
|
138
|
+
cfg.type = 'conic';
|
|
139
|
+
else
|
|
140
|
+
return cfg;
|
|
141
|
+
const parenStart = inner.indexOf('(');
|
|
142
|
+
const parenEnd = inner.lastIndexOf(')');
|
|
143
|
+
if (parenStart < 0 || parenEnd < 0)
|
|
144
|
+
return cfg;
|
|
145
|
+
const content = inner.slice(parenStart + 1, parenEnd);
|
|
146
|
+
const tokens = splitTopLevel(content);
|
|
147
|
+
// Separate gradient-specific args from color stops
|
|
148
|
+
const argTokens = [];
|
|
149
|
+
let stopStart = 0;
|
|
150
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
151
|
+
if (looksLikeStop(tokens[i])) {
|
|
152
|
+
stopStart = i;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
argTokens.push(tokens[i]);
|
|
156
|
+
if (i === tokens.length - 1)
|
|
157
|
+
stopStart = tokens.length;
|
|
158
|
+
}
|
|
159
|
+
// Parse type args
|
|
160
|
+
if (cfg.type === 'linear') {
|
|
161
|
+
const firstArg = argTokens[0]?.trim() ?? '';
|
|
162
|
+
const degMatch = firstArg.match(/^(-?[\d.]+)deg$/);
|
|
163
|
+
if (degMatch)
|
|
164
|
+
cfg.angle = parseFloat(degMatch[1]);
|
|
165
|
+
else if (firstArg.startsWith('to ')) {
|
|
166
|
+
const toMap = {
|
|
167
|
+
'to right': 90, 'to left': 270, 'to bottom': 180, 'to top': 0,
|
|
168
|
+
'to bottom right': 135, 'to bottom left': 225, 'to top right': 45, 'to top left': 315,
|
|
169
|
+
};
|
|
170
|
+
cfg.angle = toMap[firstArg] ?? 135;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (cfg.type === 'radial') {
|
|
174
|
+
const firstArg = argTokens[0]?.trim() ?? '';
|
|
175
|
+
const atMatch = firstArg.match(/^(.*?)\s+at\s+([\d.]+)%\s+([\d.]+)%$/);
|
|
176
|
+
if (atMatch) {
|
|
177
|
+
const shapePart = atMatch[1].trim();
|
|
178
|
+
cfg.posX = parseFloat(atMatch[2]);
|
|
179
|
+
cfg.posY = parseFloat(atMatch[3]);
|
|
180
|
+
cfg.radialShape = shapePart.includes('circle') ? 'circle' : 'ellipse';
|
|
181
|
+
for (const sz of ['closest-side', 'closest-corner', 'farthest-side', 'farthest-corner']) {
|
|
182
|
+
if (shapePart.includes(sz)) {
|
|
183
|
+
cfg.radialSize = sz;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (cfg.type === 'conic') {
|
|
190
|
+
const firstArg = argTokens[0]?.trim() ?? '';
|
|
191
|
+
const fromMatch = firstArg.match(/from\s+(-?[\d.]+)deg/);
|
|
192
|
+
if (fromMatch)
|
|
193
|
+
cfg.angle = parseFloat(fromMatch[1]);
|
|
194
|
+
const atMatch = firstArg.match(/at\s+([\d.]+)%\s+([\d.]+)%/);
|
|
195
|
+
if (atMatch) {
|
|
196
|
+
cfg.posX = parseFloat(atMatch[1]);
|
|
197
|
+
cfg.posY = parseFloat(atMatch[2]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Parse color stops
|
|
201
|
+
const stops = [];
|
|
202
|
+
for (let i = stopStart; i < tokens.length; i++) {
|
|
203
|
+
const s = parseGradientStop(tokens[i]);
|
|
204
|
+
if (s)
|
|
205
|
+
stops.push(s);
|
|
206
|
+
}
|
|
207
|
+
if (stops.length >= 2)
|
|
208
|
+
cfg.stops = stops;
|
|
209
|
+
return cfg;
|
|
210
|
+
}
|
|
211
|
+
// ─── Window class ──────────────────────────────────────────────────────────
|
|
212
|
+
export class GradientEditorWindow extends DraggableToolWindow {
|
|
213
|
+
_designerCanvas;
|
|
214
|
+
_config = defaultConfig();
|
|
215
|
+
_selectedStopIndex = 0;
|
|
216
|
+
_previewStrip;
|
|
217
|
+
_stopTrack;
|
|
218
|
+
_typeSelect;
|
|
219
|
+
_repeatingCheck;
|
|
220
|
+
_linearGroup;
|
|
221
|
+
_radialGroup;
|
|
222
|
+
_conicGroup;
|
|
223
|
+
_angleInput;
|
|
224
|
+
_angleVal;
|
|
225
|
+
_conicAngleInput;
|
|
226
|
+
_conicAngleVal;
|
|
227
|
+
_shapeSelect;
|
|
228
|
+
_sizeSelect;
|
|
229
|
+
_posXInput;
|
|
230
|
+
_posXVal;
|
|
231
|
+
_posYInput;
|
|
232
|
+
_posYVal;
|
|
233
|
+
_conicPosXInput;
|
|
234
|
+
_conicPosXVal;
|
|
235
|
+
_conicPosYInput;
|
|
236
|
+
_conicPosYVal;
|
|
237
|
+
_stopColorInput;
|
|
238
|
+
_stopOpacityInput;
|
|
239
|
+
_stopOpacityVal;
|
|
240
|
+
_stopPositionInput;
|
|
241
|
+
_stopPositionVal;
|
|
242
|
+
_removeStopBtn;
|
|
243
|
+
_propertySelect;
|
|
244
|
+
_cssOutput;
|
|
245
|
+
_loadBtn;
|
|
246
|
+
_copyBtn;
|
|
247
|
+
_applyBtn;
|
|
248
|
+
get windowTitle() { return 'Gradient Editor'; }
|
|
249
|
+
get windowContentStyle() {
|
|
250
|
+
return css `
|
|
251
|
+
* { box-sizing: border-box; }
|
|
252
|
+
.ge-root {
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
width: 380px;
|
|
256
|
+
color: #ddd;
|
|
257
|
+
font-family: sans-serif;
|
|
258
|
+
font-size: 12px;
|
|
259
|
+
gap: 6px;
|
|
260
|
+
padding: 8px;
|
|
261
|
+
background: #2a2a2a;
|
|
262
|
+
}
|
|
263
|
+
.preview-strip {
|
|
264
|
+
height: 50px;
|
|
265
|
+
border-radius: 4px 4px 0 0;
|
|
266
|
+
border: 1px solid #444;
|
|
267
|
+
border-bottom: none;
|
|
268
|
+
}
|
|
269
|
+
.stop-track {
|
|
270
|
+
height: 20px;
|
|
271
|
+
background: repeating-conic-gradient(#333 0% 25%, #444 0% 50%) 0 0 / 10px 10px;
|
|
272
|
+
border: 1px solid #444;
|
|
273
|
+
border-radius: 0 0 4px 4px;
|
|
274
|
+
position: relative;
|
|
275
|
+
cursor: crosshair;
|
|
276
|
+
margin-bottom: 2px;
|
|
277
|
+
}
|
|
278
|
+
.stop-marker {
|
|
279
|
+
position: absolute;
|
|
280
|
+
top: 2px;
|
|
281
|
+
width: 16px;
|
|
282
|
+
height: 16px;
|
|
283
|
+
border-radius: 50%;
|
|
284
|
+
border: 2px solid #aaa;
|
|
285
|
+
cursor: pointer;
|
|
286
|
+
transform: translateX(-50%);
|
|
287
|
+
transition: border-color 0.1s;
|
|
288
|
+
}
|
|
289
|
+
.stop-marker.selected {
|
|
290
|
+
border-color: #fff;
|
|
291
|
+
box-shadow: 0 0 0 2px #3a7ad5;
|
|
292
|
+
z-index: 1;
|
|
293
|
+
}
|
|
294
|
+
.row {
|
|
295
|
+
display: grid;
|
|
296
|
+
grid-template-columns: 70px 1fr 40px;
|
|
297
|
+
gap: 3px 6px;
|
|
298
|
+
align-items: center;
|
|
299
|
+
}
|
|
300
|
+
.row.two-col {
|
|
301
|
+
grid-template-columns: 70px 1fr;
|
|
302
|
+
}
|
|
303
|
+
.row.multi {
|
|
304
|
+
grid-template-columns: 70px 1fr 70px 1fr;
|
|
305
|
+
}
|
|
306
|
+
.row label { color: #aaa; }
|
|
307
|
+
.row input[type=range] { width: 100%; }
|
|
308
|
+
.row input[type=color] {
|
|
309
|
+
width: 100%;
|
|
310
|
+
height: 22px;
|
|
311
|
+
padding: 1px;
|
|
312
|
+
border: 1px solid #555;
|
|
313
|
+
border-radius: 3px;
|
|
314
|
+
background: #1e1e1e;
|
|
315
|
+
cursor: pointer;
|
|
316
|
+
}
|
|
317
|
+
select {
|
|
318
|
+
background: #2e2e2e;
|
|
319
|
+
color: #ddd;
|
|
320
|
+
border: 1px solid #555;
|
|
321
|
+
border-radius: 3px;
|
|
322
|
+
padding: 2px 4px;
|
|
323
|
+
font-size: 11px;
|
|
324
|
+
width: 100%;
|
|
325
|
+
}
|
|
326
|
+
.separator {
|
|
327
|
+
height: 1px;
|
|
328
|
+
background: #444;
|
|
329
|
+
margin: 2px 0;
|
|
330
|
+
}
|
|
331
|
+
.stop-header {
|
|
332
|
+
display: flex;
|
|
333
|
+
justify-content: space-between;
|
|
334
|
+
align-items: center;
|
|
335
|
+
color: #888;
|
|
336
|
+
font-size: 11px;
|
|
337
|
+
}
|
|
338
|
+
.stop-header .stop-actions {
|
|
339
|
+
display: flex;
|
|
340
|
+
gap: 3px;
|
|
341
|
+
}
|
|
342
|
+
.stop-actions button {
|
|
343
|
+
padding: 1px 7px;
|
|
344
|
+
background: #3a3a3a;
|
|
345
|
+
color: #ddd;
|
|
346
|
+
border: 1px solid #555;
|
|
347
|
+
border-radius: 3px;
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
font-size: 12px;
|
|
350
|
+
}
|
|
351
|
+
.stop-actions button:hover { background: #555; }
|
|
352
|
+
.val-label { text-align: right; color: #888; }
|
|
353
|
+
.css-out {
|
|
354
|
+
background: #1e1e1e;
|
|
355
|
+
border: 1px solid #333;
|
|
356
|
+
border-radius: 3px;
|
|
357
|
+
color: #7ec8e3;
|
|
358
|
+
font-family: monospace;
|
|
359
|
+
font-size: 11px;
|
|
360
|
+
padding: 6px;
|
|
361
|
+
resize: none;
|
|
362
|
+
width: 100%;
|
|
363
|
+
height: 48px;
|
|
364
|
+
}
|
|
365
|
+
.actions {
|
|
366
|
+
display: flex;
|
|
367
|
+
gap: 6px;
|
|
368
|
+
justify-content: flex-end;
|
|
369
|
+
}
|
|
370
|
+
.actions .load-btn { margin-right: auto; }
|
|
371
|
+
.actions button, .apply-btn {
|
|
372
|
+
padding: 4px 12px;
|
|
373
|
+
background: #3a3a3a;
|
|
374
|
+
color: #ddd;
|
|
375
|
+
border: 1px solid #555;
|
|
376
|
+
border-radius: 3px;
|
|
377
|
+
cursor: pointer;
|
|
378
|
+
font-size: 12px;
|
|
379
|
+
}
|
|
380
|
+
.actions button:hover, .apply-btn:hover { background: #555; }
|
|
381
|
+
.apply-btn {
|
|
382
|
+
background: #3a6a9a;
|
|
383
|
+
border-color: #2a5a8a;
|
|
384
|
+
}
|
|
385
|
+
.apply-btn:hover { background: #4a7aaa; }
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
get windowTemplate() {
|
|
389
|
+
return `
|
|
390
|
+
<div class="ge-root">
|
|
391
|
+
<div class="preview-strip" id="ge-preview-strip"></div>
|
|
392
|
+
<div class="stop-track" id="ge-stop-track"></div>
|
|
393
|
+
|
|
394
|
+
<div class="row">
|
|
395
|
+
<label>Type</label>
|
|
396
|
+
<select id="ge-type">
|
|
397
|
+
<option value="linear">Linear</option>
|
|
398
|
+
<option value="radial">Radial</option>
|
|
399
|
+
<option value="conic">Conic</option>
|
|
400
|
+
</select>
|
|
401
|
+
<span></span>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="row two-col" style="margin-top:-3px">
|
|
404
|
+
<label></label>
|
|
405
|
+
<label style="color:#ddd"><input type="checkbox" id="ge-repeating"> Repeating</label>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<!-- Linear controls -->
|
|
409
|
+
<div id="ge-linear-group">
|
|
410
|
+
<div class="row">
|
|
411
|
+
<label>Angle</label>
|
|
412
|
+
<input type="range" id="ge-angle" min="0" max="360" step="1">
|
|
413
|
+
<span class="val-label" id="ge-angle-val">135°</span>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<!-- Radial controls -->
|
|
418
|
+
<div id="ge-radial-group" style="display:none">
|
|
419
|
+
<div class="row multi">
|
|
420
|
+
<label>Shape</label>
|
|
421
|
+
<select id="ge-shape">
|
|
422
|
+
<option value="ellipse">Ellipse</option>
|
|
423
|
+
<option value="circle">Circle</option>
|
|
424
|
+
</select>
|
|
425
|
+
<label>Size</label>
|
|
426
|
+
<select id="ge-size">
|
|
427
|
+
<option value="farthest-corner">Farthest Corner</option>
|
|
428
|
+
<option value="farthest-side">Farthest Side</option>
|
|
429
|
+
<option value="closest-corner">Closest Corner</option>
|
|
430
|
+
<option value="closest-side">Closest Side</option>
|
|
431
|
+
</select>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="row">
|
|
434
|
+
<label>Position X</label>
|
|
435
|
+
<input type="range" id="ge-pos-x" min="0" max="100" step="1">
|
|
436
|
+
<span class="val-label" id="ge-pos-x-val">50%</span>
|
|
437
|
+
</div>
|
|
438
|
+
<div class="row">
|
|
439
|
+
<label>Position Y</label>
|
|
440
|
+
<input type="range" id="ge-pos-y" min="0" max="100" step="1">
|
|
441
|
+
<span class="val-label" id="ge-pos-y-val">50%</span>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<!-- Conic controls -->
|
|
446
|
+
<div id="ge-conic-group" style="display:none">
|
|
447
|
+
<div class="row">
|
|
448
|
+
<label>Angle</label>
|
|
449
|
+
<input type="range" id="ge-conic-angle" min="0" max="360" step="1">
|
|
450
|
+
<span class="val-label" id="ge-conic-angle-val">0°</span>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="row">
|
|
453
|
+
<label>Position X</label>
|
|
454
|
+
<input type="range" id="ge-conic-pos-x" min="0" max="100" step="1">
|
|
455
|
+
<span class="val-label" id="ge-conic-pos-x-val">50%</span>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="row">
|
|
458
|
+
<label>Position Y</label>
|
|
459
|
+
<input type="range" id="ge-conic-pos-y" min="0" max="100" step="1">
|
|
460
|
+
<span class="val-label" id="ge-conic-pos-y-val">50%</span>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<div class="separator"></div>
|
|
465
|
+
|
|
466
|
+
<div class="stop-header">
|
|
467
|
+
<span>Selected Stop</span>
|
|
468
|
+
<div class="stop-actions">
|
|
469
|
+
<button id="ge-add-stop-btn" title="Add stop at 50%">+ Add</button>
|
|
470
|
+
<button id="ge-remove-stop-btn" title="Remove selected stop">− Remove</button>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<div class="row">
|
|
475
|
+
<label>Color</label>
|
|
476
|
+
<input type="color" id="ge-stop-color" value="#667eea">
|
|
477
|
+
<span></span>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="row">
|
|
480
|
+
<label>Opacity</label>
|
|
481
|
+
<input type="range" id="ge-stop-opacity" min="0" max="100" step="1">
|
|
482
|
+
<span class="val-label" id="ge-stop-opacity-val">100%</span>
|
|
483
|
+
</div>
|
|
484
|
+
<div class="row">
|
|
485
|
+
<label>Position</label>
|
|
486
|
+
<input type="range" id="ge-stop-pos" min="0" max="100" step="1">
|
|
487
|
+
<span class="val-label" id="ge-stop-pos-val">0%</span>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<div class="separator"></div>
|
|
491
|
+
|
|
492
|
+
<div class="row two-col">
|
|
493
|
+
<label>Apply to</label>
|
|
494
|
+
<select id="ge-property">
|
|
495
|
+
<option value="background-image">background-image</option>
|
|
496
|
+
<option value="background">background</option>
|
|
497
|
+
</select>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<textarea class="css-out" id="ge-css-out"></textarea>
|
|
501
|
+
|
|
502
|
+
<div class="actions">
|
|
503
|
+
<button class="load-btn" id="ge-load-btn">Load</button>
|
|
504
|
+
<button id="ge-copy-btn">Copy CSS</button>
|
|
505
|
+
<button class="apply-btn" id="ge-apply-btn">Apply to selection</button>
|
|
506
|
+
</div>
|
|
507
|
+
</div>`;
|
|
508
|
+
}
|
|
509
|
+
constructor(designerCanvas) {
|
|
510
|
+
super();
|
|
511
|
+
this._designerCanvas = designerCanvas;
|
|
512
|
+
this._previewStrip = this._getDomElement('ge-preview-strip');
|
|
513
|
+
this._stopTrack = this._getDomElement('ge-stop-track');
|
|
514
|
+
this._typeSelect = this._getDomElement('ge-type');
|
|
515
|
+
this._repeatingCheck = this._getDomElement('ge-repeating');
|
|
516
|
+
this._linearGroup = this._getDomElement('ge-linear-group');
|
|
517
|
+
this._radialGroup = this._getDomElement('ge-radial-group');
|
|
518
|
+
this._conicGroup = this._getDomElement('ge-conic-group');
|
|
519
|
+
this._angleInput = this._getDomElement('ge-angle');
|
|
520
|
+
this._angleVal = this._getDomElement('ge-angle-val');
|
|
521
|
+
this._conicAngleInput = this._getDomElement('ge-conic-angle');
|
|
522
|
+
this._conicAngleVal = this._getDomElement('ge-conic-angle-val');
|
|
523
|
+
this._shapeSelect = this._getDomElement('ge-shape');
|
|
524
|
+
this._sizeSelect = this._getDomElement('ge-size');
|
|
525
|
+
this._posXInput = this._getDomElement('ge-pos-x');
|
|
526
|
+
this._posXVal = this._getDomElement('ge-pos-x-val');
|
|
527
|
+
this._posYInput = this._getDomElement('ge-pos-y');
|
|
528
|
+
this._posYVal = this._getDomElement('ge-pos-y-val');
|
|
529
|
+
this._conicPosXInput = this._getDomElement('ge-conic-pos-x');
|
|
530
|
+
this._conicPosXVal = this._getDomElement('ge-conic-pos-x-val');
|
|
531
|
+
this._conicPosYInput = this._getDomElement('ge-conic-pos-y');
|
|
532
|
+
this._conicPosYVal = this._getDomElement('ge-conic-pos-y-val');
|
|
533
|
+
this._stopColorInput = this._getDomElement('ge-stop-color');
|
|
534
|
+
this._stopOpacityInput = this._getDomElement('ge-stop-opacity');
|
|
535
|
+
this._stopOpacityVal = this._getDomElement('ge-stop-opacity-val');
|
|
536
|
+
this._stopPositionInput = this._getDomElement('ge-stop-pos');
|
|
537
|
+
this._stopPositionVal = this._getDomElement('ge-stop-pos-val');
|
|
538
|
+
this._removeStopBtn = this._getDomElement('ge-remove-stop-btn');
|
|
539
|
+
this._propertySelect = this._getDomElement('ge-property');
|
|
540
|
+
this._cssOutput = this._getDomElement('ge-css-out');
|
|
541
|
+
this._loadBtn = this._getDomElement('ge-load-btn');
|
|
542
|
+
this._copyBtn = this._getDomElement('ge-copy-btn');
|
|
543
|
+
this._applyBtn = this._getDomElement('ge-apply-btn');
|
|
544
|
+
// Type / repeating
|
|
545
|
+
this._typeSelect.onchange = () => {
|
|
546
|
+
this._config.type = this._typeSelect.value;
|
|
547
|
+
this._updateTypeVisibility();
|
|
548
|
+
this._refresh();
|
|
549
|
+
};
|
|
550
|
+
this._repeatingCheck.onchange = () => { this._config.repeating = this._repeatingCheck.checked; this._refresh(); };
|
|
551
|
+
// Linear angle
|
|
552
|
+
this._angleInput.oninput = () => {
|
|
553
|
+
this._config.angle = Number(this._angleInput.value);
|
|
554
|
+
this._angleVal.textContent = this._angleInput.value + '°';
|
|
555
|
+
this._refresh();
|
|
556
|
+
};
|
|
557
|
+
// Conic angle
|
|
558
|
+
this._conicAngleInput.oninput = () => {
|
|
559
|
+
this._config.angle = Number(this._conicAngleInput.value);
|
|
560
|
+
this._conicAngleVal.textContent = this._conicAngleInput.value + '°';
|
|
561
|
+
this._refresh();
|
|
562
|
+
};
|
|
563
|
+
// Radial controls
|
|
564
|
+
this._shapeSelect.onchange = () => { this._config.radialShape = this._shapeSelect.value; this._refresh(); };
|
|
565
|
+
this._sizeSelect.onchange = () => { this._config.radialSize = this._sizeSelect.value; this._refresh(); };
|
|
566
|
+
this._posXInput.oninput = () => { this._config.posX = Number(this._posXInput.value); this._posXVal.textContent = this._posXInput.value + '%'; this._refresh(); };
|
|
567
|
+
this._posYInput.oninput = () => { this._config.posY = Number(this._posYInput.value); this._posYVal.textContent = this._posYInput.value + '%'; this._refresh(); };
|
|
568
|
+
// Conic position
|
|
569
|
+
this._conicPosXInput.oninput = () => { this._config.posX = Number(this._conicPosXInput.value); this._conicPosXVal.textContent = this._conicPosXInput.value + '%'; this._refresh(); };
|
|
570
|
+
this._conicPosYInput.oninput = () => { this._config.posY = Number(this._conicPosYInput.value); this._conicPosYVal.textContent = this._conicPosYInput.value + '%'; this._refresh(); };
|
|
571
|
+
// Stop controls
|
|
572
|
+
this._stopColorInput.oninput = () => { this._currentStop().color = this._stopColorInput.value; this._refresh(); };
|
|
573
|
+
this._stopOpacityInput.oninput = () => {
|
|
574
|
+
this._currentStop().opacity = Number(this._stopOpacityInput.value);
|
|
575
|
+
this._stopOpacityVal.textContent = this._stopOpacityInput.value + '%';
|
|
576
|
+
this._refresh();
|
|
577
|
+
};
|
|
578
|
+
this._stopPositionInput.oninput = () => {
|
|
579
|
+
this._currentStop().position = Number(this._stopPositionInput.value);
|
|
580
|
+
this._stopPositionVal.textContent = this._stopPositionInput.value + '%';
|
|
581
|
+
this._refresh();
|
|
582
|
+
};
|
|
583
|
+
// Add / remove stop
|
|
584
|
+
this._getDomElement('ge-add-stop-btn').onclick = () => this._addStop(50);
|
|
585
|
+
this._removeStopBtn.onclick = () => this._removeStop();
|
|
586
|
+
// Stop track: click to add stop
|
|
587
|
+
this._stopTrack.addEventListener('click', (e) => {
|
|
588
|
+
const rect = this._stopTrack.getBoundingClientRect();
|
|
589
|
+
const pos = Math.round(((e.clientX - rect.left) / rect.width) * 100);
|
|
590
|
+
this._addStop(Math.max(0, Math.min(100, pos)));
|
|
591
|
+
});
|
|
592
|
+
// Output / apply
|
|
593
|
+
this._copyBtn.onclick = () => { navigator.clipboard?.writeText(this._cssOutput.value).catch(() => { }); };
|
|
594
|
+
this._applyBtn.onclick = () => this._applyToSelection();
|
|
595
|
+
this._loadBtn.onclick = () => {
|
|
596
|
+
this._loadFromPrimarySelection();
|
|
597
|
+
this._selectedStopIndex = Math.max(0, Math.min(this._selectedStopIndex, this._config.stops.length - 1));
|
|
598
|
+
this._updateTypeVisibility();
|
|
599
|
+
this._syncControls();
|
|
600
|
+
this._renderStopTrack();
|
|
601
|
+
this._refreshOutput();
|
|
602
|
+
};
|
|
603
|
+
this._loadFromPrimarySelection();
|
|
604
|
+
this._updateTypeVisibility();
|
|
605
|
+
this._syncControls();
|
|
606
|
+
this._renderStopTrack();
|
|
607
|
+
this._refreshOutput();
|
|
608
|
+
}
|
|
609
|
+
_loadFromPrimarySelection() {
|
|
610
|
+
const primary = this._designerCanvas?.instanceServiceContainer?.selectionService?.primarySelection;
|
|
611
|
+
if (!primary)
|
|
612
|
+
return;
|
|
613
|
+
for (const prop of ['background-image', 'background']) {
|
|
614
|
+
const val = primary.getStyle(prop);
|
|
615
|
+
if (val && val.includes('gradient')) {
|
|
616
|
+
const extracted = extractFirstGradient(val);
|
|
617
|
+
if (extracted) {
|
|
618
|
+
this._config = parseGradientCss(extracted);
|
|
619
|
+
if (prop === 'background')
|
|
620
|
+
this._propertySelect.value = 'background';
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
_currentStop() {
|
|
627
|
+
return this._config.stops[this._selectedStopIndex];
|
|
628
|
+
}
|
|
629
|
+
_addStop(position) {
|
|
630
|
+
this._config.stops.push({ color: '#ffffff', opacity: 100, position });
|
|
631
|
+
this._selectedStopIndex = this._config.stops.length - 1;
|
|
632
|
+
this._syncControls();
|
|
633
|
+
this._renderStopTrack();
|
|
634
|
+
this._refreshOutput();
|
|
635
|
+
}
|
|
636
|
+
_removeStop() {
|
|
637
|
+
if (this._config.stops.length <= 2)
|
|
638
|
+
return;
|
|
639
|
+
this._config.stops.splice(this._selectedStopIndex, 1);
|
|
640
|
+
this._selectedStopIndex = Math.min(this._selectedStopIndex, this._config.stops.length - 1);
|
|
641
|
+
this._syncControls();
|
|
642
|
+
this._renderStopTrack();
|
|
643
|
+
this._refreshOutput();
|
|
644
|
+
}
|
|
645
|
+
_updateTypeVisibility() {
|
|
646
|
+
this._linearGroup.style.display = this._config.type === 'linear' ? '' : 'none';
|
|
647
|
+
this._radialGroup.style.display = this._config.type === 'radial' ? '' : 'none';
|
|
648
|
+
this._conicGroup.style.display = this._config.type === 'conic' ? '' : 'none';
|
|
649
|
+
}
|
|
650
|
+
_syncControls() {
|
|
651
|
+
// type
|
|
652
|
+
this._typeSelect.value = this._config.type;
|
|
653
|
+
this._repeatingCheck.checked = this._config.repeating;
|
|
654
|
+
// linear/conic angle
|
|
655
|
+
this._angleInput.value = String(this._config.angle);
|
|
656
|
+
this._angleVal.textContent = this._config.angle + '°';
|
|
657
|
+
this._conicAngleInput.value = String(this._config.angle);
|
|
658
|
+
this._conicAngleVal.textContent = this._config.angle + '°';
|
|
659
|
+
// radial
|
|
660
|
+
this._shapeSelect.value = this._config.radialShape;
|
|
661
|
+
this._sizeSelect.value = this._config.radialSize;
|
|
662
|
+
this._posXInput.value = String(this._config.posX);
|
|
663
|
+
this._posXVal.textContent = this._config.posX + '%';
|
|
664
|
+
this._posYInput.value = String(this._config.posY);
|
|
665
|
+
this._posYVal.textContent = this._config.posY + '%';
|
|
666
|
+
// conic position
|
|
667
|
+
this._conicPosXInput.value = String(this._config.posX);
|
|
668
|
+
this._conicPosXVal.textContent = this._config.posX + '%';
|
|
669
|
+
this._conicPosYInput.value = String(this._config.posY);
|
|
670
|
+
this._conicPosYVal.textContent = this._config.posY + '%';
|
|
671
|
+
// selected stop
|
|
672
|
+
const s = this._currentStop();
|
|
673
|
+
this._stopColorInput.value = s.color;
|
|
674
|
+
this._stopOpacityInput.value = String(s.opacity);
|
|
675
|
+
this._stopOpacityVal.textContent = s.opacity + '%';
|
|
676
|
+
this._stopPositionInput.value = String(s.position);
|
|
677
|
+
this._stopPositionVal.textContent = s.position + '%';
|
|
678
|
+
}
|
|
679
|
+
_renderStopTrack() {
|
|
680
|
+
// Update gradient preview on strip
|
|
681
|
+
const gradCss = configToCss(this._config);
|
|
682
|
+
this._previewStrip.style.background = gradCss;
|
|
683
|
+
// Re-render stop markers
|
|
684
|
+
this._stopTrack.innerHTML = '';
|
|
685
|
+
this._config.stops.forEach((stop, i) => {
|
|
686
|
+
const marker = document.createElement('div');
|
|
687
|
+
marker.className = 'stop-marker' + (i === this._selectedStopIndex ? ' selected' : '');
|
|
688
|
+
marker.style.left = `${stop.position}%`;
|
|
689
|
+
marker.style.background = stop.color;
|
|
690
|
+
marker.style.opacity = String(stop.opacity / 100);
|
|
691
|
+
// Select on click (stop propagation so track click handler doesn't fire)
|
|
692
|
+
marker.addEventListener('click', (e) => {
|
|
693
|
+
e.stopPropagation();
|
|
694
|
+
this._selectedStopIndex = i;
|
|
695
|
+
this._syncControls();
|
|
696
|
+
this._renderStopTrack();
|
|
697
|
+
});
|
|
698
|
+
// Drag to reposition — update marker in-place to avoid destroying pointer capture
|
|
699
|
+
marker.addEventListener('pointerdown', (e) => {
|
|
700
|
+
e.preventDefault();
|
|
701
|
+
e.stopPropagation();
|
|
702
|
+
marker.setPointerCapture(e.pointerId);
|
|
703
|
+
const trackRect = this._stopTrack.getBoundingClientRect();
|
|
704
|
+
const onMove = (me) => {
|
|
705
|
+
const x = Math.max(0, Math.min(1, (me.clientX - trackRect.left) / trackRect.width));
|
|
706
|
+
const pos = Math.round(x * 100);
|
|
707
|
+
this._config.stops[i].position = pos;
|
|
708
|
+
this._selectedStopIndex = i;
|
|
709
|
+
// Update this marker's position in-place (don't re-render — that would destroy the captured element)
|
|
710
|
+
marker.style.left = pos + '%';
|
|
711
|
+
// Update the selected state visually
|
|
712
|
+
this._stopTrack.querySelectorAll('.stop-marker').forEach((m, mi) => {
|
|
713
|
+
m.classList.toggle('selected', mi === i);
|
|
714
|
+
});
|
|
715
|
+
// Refresh preview and output without re-rendering markers
|
|
716
|
+
this._previewStrip.style.background = configToCss(this._config);
|
|
717
|
+
this._refreshOutput();
|
|
718
|
+
};
|
|
719
|
+
const onUp = () => {
|
|
720
|
+
marker.removeEventListener('pointermove', onMove);
|
|
721
|
+
marker.removeEventListener('pointerup', onUp);
|
|
722
|
+
// Full sync after drag ends
|
|
723
|
+
this._syncControls();
|
|
724
|
+
this._renderStopTrack();
|
|
725
|
+
};
|
|
726
|
+
marker.addEventListener('pointermove', onMove);
|
|
727
|
+
marker.addEventListener('pointerup', onUp);
|
|
728
|
+
});
|
|
729
|
+
this._stopTrack.appendChild(marker);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
_refreshOutput() {
|
|
733
|
+
this._cssOutput.value = configToCss(this._config);
|
|
734
|
+
}
|
|
735
|
+
_refresh() {
|
|
736
|
+
this._renderStopTrack();
|
|
737
|
+
this._refreshOutput();
|
|
738
|
+
}
|
|
739
|
+
_applyToSelection() {
|
|
740
|
+
const items = this._designerCanvas?.instanceServiceContainer?.selectionService?.selectedElements;
|
|
741
|
+
if (!items?.length)
|
|
742
|
+
return;
|
|
743
|
+
const cssVal = configToCss(this._config);
|
|
744
|
+
const prop = this._propertySelect.value;
|
|
745
|
+
const group = items[0].openGroup(`set ${prop}`);
|
|
746
|
+
for (const item of items) {
|
|
747
|
+
item.setStyle(prop, cssVal);
|
|
748
|
+
}
|
|
749
|
+
group.commit();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
customElements.define('node-projects-gradient-editor-window', GradientEditorWindow);
|
|
753
|
+
//# sourceMappingURL=GradientEditorWindow.js.map
|