@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.
Files changed (36) hide show
  1. package/dist/elements/helper/getBoxQuads.js +91 -6
  2. package/dist/elements/helper/getBoxQuads.js.map +1 -1
  3. package/dist/elements/services/DefaultServiceBootstrap.js +3 -0
  4. package/dist/elements/services/DefaultServiceBootstrap.js.map +1 -1
  5. package/dist/elements/widgets/designerView/extensions/contextMenu/ToolWindowsContextMenu.d.ts +8 -0
  6. package/dist/elements/widgets/designerView/extensions/contextMenu/ToolWindowsContextMenu.js +54 -0
  7. package/dist/elements/widgets/designerView/extensions/contextMenu/ToolWindowsContextMenu.js.map +1 -0
  8. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.d.ts +1 -0
  9. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.js +19 -1
  10. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.js.map +1 -1
  11. package/dist/elements/widgets/designerView/tools/toolBar/DesignerToolbar.js +7 -1
  12. package/dist/elements/widgets/designerView/tools/toolBar/DesignerToolbar.js.map +1 -1
  13. package/dist/elements/widgets/designerView/tools/toolBar/popups/BorderRadiusEditorWindow.d.ts +82 -0
  14. package/dist/elements/widgets/designerView/tools/toolBar/popups/BorderRadiusEditorWindow.js +1260 -0
  15. package/dist/elements/widgets/designerView/tools/toolBar/popups/BorderRadiusEditorWindow.js.map +1 -0
  16. package/dist/elements/widgets/designerView/tools/toolBar/popups/BoxShadowEditorWindow.d.ts +38 -0
  17. package/dist/elements/widgets/designerView/tools/toolBar/popups/BoxShadowEditorWindow.js +518 -0
  18. package/dist/elements/widgets/designerView/tools/toolBar/popups/BoxShadowEditorWindow.js.map +1 -0
  19. package/dist/elements/widgets/designerView/tools/toolBar/popups/DraggableToolWindow.d.ts +39 -0
  20. package/dist/elements/widgets/designerView/tools/toolBar/popups/DraggableToolWindow.js +181 -0
  21. package/dist/elements/widgets/designerView/tools/toolBar/popups/DraggableToolWindow.js.map +1 -0
  22. package/dist/elements/widgets/designerView/tools/toolBar/popups/GradientEditorWindow.d.ts +53 -0
  23. package/dist/elements/widgets/designerView/tools/toolBar/popups/GradientEditorWindow.js +753 -0
  24. package/dist/elements/widgets/designerView/tools/toolBar/popups/GradientEditorWindow.js.map +1 -0
  25. package/dist/elements/widgets/designerView/tools/toolBar/popups/TextShadowEditorWindow.d.ts +36 -0
  26. package/dist/elements/widgets/designerView/tools/toolBar/popups/TextShadowEditorWindow.js +449 -0
  27. package/dist/elements/widgets/designerView/tools/toolBar/popups/TextShadowEditorWindow.js.map +1 -0
  28. package/dist/elements/widgets/designerView/tools/toolBar/popups/TransformToolPopup.d.ts +8 -6
  29. package/dist/elements/widgets/designerView/tools/toolBar/popups/TransformToolPopup.js +56 -72
  30. package/dist/elements/widgets/designerView/tools/toolBar/popups/TransformToolPopup.js.map +1 -1
  31. package/dist/index-min.js +544 -7
  32. package/dist/index-min.js.map +4 -4
  33. package/dist/index.d.ts +6 -0
  34. package/dist/index.js +6 -0
  35. package/dist/index.js.map +1 -1
  36. 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