@myrmidon/gve-snapshot-rendition 0.0.2 → 0.0.4

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/index.js CHANGED
@@ -77,7 +77,7 @@ const DEFAULT_SETTINGS = {
77
77
  // Hints - empty by default, to be filled by consumer code
78
78
  hints: {},
79
79
  hintMargin: 0,
80
- hintDesignWidth: 100,
80
+ hintDesignWidth: 300,
81
81
  hintDesignHeight: 100,
82
82
  showHintHandles: false,
83
83
  // Hilites
@@ -1545,6 +1545,9 @@ class FeatureResolver {
1545
1545
  this._logger.warn(`Invalid r_t-displaced-span value: ${value}`);
1546
1546
  }
1547
1547
  break;
1548
+ case "r_t-value":
1549
+ config.textValue = value;
1550
+ break;
1548
1551
  case "r_hints":
1549
1552
  config.hints = value.trim().split(/\s+/);
1550
1553
  break;
@@ -1854,6 +1857,24 @@ class TextRenderer {
1854
1857
  this._logger.warn("No nodes to render");
1855
1858
  return;
1856
1859
  }
1860
+ // Apply r_t-value override if specified
1861
+ // This overrides the operation's value (the text being added) for display only
1862
+ if (config.textValue) {
1863
+ this._logger.debug("TextRenderer", `Applying r_t-value override: "${config.textValue}" (original text had ${nodes.length} characters)`);
1864
+ // Replace node data with characters from r_t-value
1865
+ // If r_t-value has fewer characters than nodes, we only override the first N nodes
1866
+ // If r_t-value has more characters, we only use the first N characters
1867
+ const overrideChars = config.textValue.split('');
1868
+ const charsToOverride = Math.min(nodes.length, overrideChars.length);
1869
+ for (let i = 0; i < charsToOverride; i++) {
1870
+ nodes[i] = { ...nodes[i], data: overrideChars[i] };
1871
+ }
1872
+ // If r_t-value has fewer characters, we need to only render those nodes
1873
+ if (overrideChars.length < nodes.length) {
1874
+ nodes = nodes.slice(0, overrideChars.length);
1875
+ this._logger.debug("TextRenderer", `r_t-value has fewer characters (${overrideChars.length}) than nodes (${nodes.length}), only rendering first ${overrideChars.length} characters`);
1876
+ }
1877
+ }
1857
1878
  // 1. Calculate RBR (or use displaced span if specified)
1858
1879
  let rbrs;
1859
1880
  if (config.textDisplacedSpan) {
@@ -5109,7 +5130,6 @@ class Toolbar {
5109
5130
  <button id="prev-btn" class="toolbar-btn" title="Previous Version (←)">
5110
5131
  <i data-feather="chevron-left"></i>
5111
5132
  </button>
5112
- <div id="version-label" class="version-label">v0 (1/1)</div>
5113
5133
  <button id="next-btn" class="toolbar-btn" title="Next Version (→)">
5114
5134
  <i data-feather="chevron-right"></i>
5115
5135
  </button>
@@ -5157,8 +5177,14 @@ class Toolbar {
5157
5177
  <div id="custom-buttons-group" class="toolbar-group custom-buttons-group">
5158
5178
  </div>
5159
5179
 
5160
- <!-- Group Badge (at the end) -->
5161
- <div id="group-badge" class="version-group-badge" style="display: none; margin-left: auto;"></div>
5180
+ <!-- Log Display (flexible space) -->
5181
+ <div id="log-display" class="toolbar-log-display" style="flex: 1; display: none;"></div>
5182
+
5183
+ <!-- Version Label and Group Badge (at the right edge) -->
5184
+ <div class="toolbar-group version-info-group" style="margin-left: auto;">
5185
+ <div id="version-label" class="version-label">v0 (1/1)</div>
5186
+ <div id="group-badge" class="version-group-badge" style="display: none;"></div>
5187
+ </div>
5162
5188
  </div>
5163
5189
  `;
5164
5190
  // Get element references
@@ -5170,6 +5196,7 @@ class Toolbar {
5170
5196
  this.lastBtn = this.container.querySelector("#last-btn");
5171
5197
  this.versionLabel = this.container.querySelector("#version-label");
5172
5198
  this.groupBadge = this.container.querySelector("#group-badge");
5199
+ this.logDisplay = this.container.querySelector("#log-display");
5173
5200
  this.slideshowStartBtn = this.container.querySelector("#slideshow-start-btn");
5174
5201
  this.slideshowStopBtn = this.container.querySelector("#slideshow-stop-btn");
5175
5202
  this.slideshowReverseBtn = this.container.querySelector("#slideshow-reverse-btn");
@@ -5294,6 +5321,7 @@ class Toolbar {
5294
5321
  if (!info) {
5295
5322
  this.versionLabel.innerHTML = "v0 (1/1)";
5296
5323
  this.groupBadge.style.display = "none";
5324
+ this.logDisplay.style.display = "none";
5297
5325
  return;
5298
5326
  }
5299
5327
  let labelHtml = `<span class="version-tag">${info.tag}</span>`;
@@ -5310,6 +5338,40 @@ class Toolbar {
5310
5338
  else {
5311
5339
  this.groupBadge.style.display = "none";
5312
5340
  }
5341
+ // Update log display
5342
+ this.updateLogDisplay();
5343
+ }
5344
+ /**
5345
+ * Update the log display with the current operation's log feature.
5346
+ */
5347
+ updateLogDisplay() {
5348
+ if (!this._data || this._currentVersionIndex === 0) {
5349
+ // v0 has no operation, hide log
5350
+ this.logDisplay.style.display = "none";
5351
+ this.logDisplay.textContent = "";
5352
+ this.logDisplay.title = "";
5353
+ return;
5354
+ }
5355
+ const stepIndex = this._currentVersionIndex - 1;
5356
+ if (stepIndex < 0 || stepIndex >= (this._data.steps?.length || 0)) {
5357
+ this.logDisplay.style.display = "none";
5358
+ this.logDisplay.textContent = "";
5359
+ this.logDisplay.title = "";
5360
+ return;
5361
+ }
5362
+ const step = this._data.steps[stepIndex];
5363
+ // Look for a global feature named "log"
5364
+ const logFeature = step.operation?.features?.find((f) => f.name === "log" && f.isGlobal);
5365
+ if (logFeature && logFeature.value) {
5366
+ this.logDisplay.textContent = logFeature.value;
5367
+ this.logDisplay.title = logFeature.value; // Add tooltip with full text
5368
+ this.logDisplay.style.display = "block";
5369
+ }
5370
+ else {
5371
+ this.logDisplay.style.display = "none";
5372
+ this.logDisplay.textContent = "";
5373
+ this.logDisplay.title = "";
5374
+ }
5313
5375
  }
5314
5376
  /**
5315
5377
  * Get version information for current step.
@@ -7440,6 +7502,9 @@ class DetailsArea {
7440
7502
  this._showHiddenFeatures = false;
7441
7503
  this._data = null;
7442
7504
  this._currentVersionTag = "v0";
7505
+ this._isResizing = false;
7506
+ this._initialHeight = 0;
7507
+ this._initialMouseY = 0;
7443
7508
  this._settings = settings;
7444
7509
  this._logger = logger;
7445
7510
  this._summaryService = new OperationSummaryService(logger);
@@ -7462,9 +7527,10 @@ class DetailsArea {
7462
7527
  </div>
7463
7528
  <div class="details-content">
7464
7529
  <div id="operation-section" class="details-operation-section"></div>
7465
- <div id="version-features-section" class="details-version-section"></div>
7466
7530
  <div id="hovered-elements-section" class="details-hovered-section"></div>
7531
+ <div id="version-features-section" class="details-version-section"></div>
7467
7532
  </div>
7533
+ <div class="details-resize-handle" id="details-resize-handle"></div>
7468
7534
  `;
7469
7535
  // Get element references
7470
7536
  this.operationSection = this.container.querySelector("#operation-section");
@@ -7472,6 +7538,7 @@ class DetailsArea {
7472
7538
  this.hoveredElementsSection = this.container.querySelector("#hovered-elements-section");
7473
7539
  this.hiddenFeaturesCheckbox = this.container.querySelector("#hidden-features-checkbox");
7474
7540
  this.collapseBtn = this.container.querySelector("#collapse-details-btn");
7541
+ this.resizeHandle = this.container.querySelector("#details-resize-handle");
7475
7542
  this.attachEventListeners();
7476
7543
  // Replace feather icons - must be done after elements are in DOM
7477
7544
  this.replaceFeatherIcons();
@@ -7509,6 +7576,51 @@ class DetailsArea {
7509
7576
  this.collapseBtn.addEventListener("click", () => {
7510
7577
  this.toggleCollapse();
7511
7578
  });
7579
+ // Resize handle
7580
+ this.resizeHandle.addEventListener("mousedown", (e) => {
7581
+ this.startResize(e);
7582
+ });
7583
+ }
7584
+ /**
7585
+ * Start resizing the details area.
7586
+ */
7587
+ startResize(e) {
7588
+ e.preventDefault();
7589
+ this._isResizing = true;
7590
+ this._initialHeight = this.container.offsetHeight;
7591
+ this._initialMouseY = e.clientY;
7592
+ // Attach document-level listeners for smooth resizing
7593
+ const onMouseMove = (e) => this.handleResize(e);
7594
+ const onMouseUp = () => this.stopResize(onMouseMove, onMouseUp);
7595
+ document.addEventListener("mousemove", onMouseMove);
7596
+ document.addEventListener("mouseup", onMouseUp);
7597
+ this._logger.debug("DetailsArea", "Started resizing");
7598
+ }
7599
+ /**
7600
+ * Handle resize drag.
7601
+ */
7602
+ handleResize(e) {
7603
+ if (!this._isResizing)
7604
+ return;
7605
+ // Calculate delta: dragging down increases height, dragging up decreases height
7606
+ const deltaY = e.clientY - this._initialMouseY;
7607
+ const newHeight = this._initialHeight + deltaY;
7608
+ // Enforce minimum and maximum height
7609
+ const minHeight = 100;
7610
+ const maxHeight = 1200;
7611
+ const clampedHeight = Math.max(minHeight, Math.min(maxHeight, newHeight));
7612
+ this.container.style.height = `${clampedHeight}px`;
7613
+ this.container.style.maxHeight = `${clampedHeight}px`;
7614
+ }
7615
+ /**
7616
+ * Stop resizing the details area.
7617
+ */
7618
+ stopResize(onMouseMove, onMouseUp) {
7619
+ this._isResizing = false;
7620
+ // Remove document-level listeners
7621
+ document.removeEventListener("mousemove", onMouseMove);
7622
+ document.removeEventListener("mouseup", onMouseUp);
7623
+ this._logger.debug("DetailsArea", `Stopped resizing (height: ${this.container.offsetHeight}px)`);
7512
7624
  }
7513
7625
  /**
7514
7626
  * Toggle collapse/expand state.
@@ -10725,7 +10837,7 @@ function _assertThisInitialized(self) { if (self === void 0) { throw new Referen
10725
10837
  function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }
10726
10838
 
10727
10839
  /*!
10728
- * GSAP 3.13.0
10840
+ * GSAP 3.14.2
10729
10841
  * https://gsap.com
10730
10842
  *
10731
10843
  * @license Copyright 2008-2025, GreenSock. All rights reserved.
@@ -10785,6 +10897,8 @@ var _config = {
10785
10897
  _isTypedArray = typeof ArrayBuffer === "function" && ArrayBuffer.isView || function () {},
10786
10898
  // note: IE10 has ArrayBuffer, but NOT ArrayBuffer.isView().
10787
10899
  _isArray = Array.isArray,
10900
+ _randomExp = /random\([^)]+\)/g,
10901
+ _commaDelimExp = /,\s*/g,
10788
10902
  _strictNumExp = /(?:-?\.?\d|\.)+/gi,
10789
10903
  //only numbers (including negatives and decimals) but NOT relative values.
10790
10904
  _numExp$1 = /[-+=.]*\d+[.e\-+]*\d*[e\-+]*\d*/g,
@@ -11379,7 +11493,7 @@ clamp = function clamp(min, max, value) {
11379
11493
  return _isString(value) && !leaveStrings || _isArrayLike(value, 1) ? (_accumulator = accumulator).push.apply(_accumulator, toArray(value)) : accumulator.push(value);
11380
11494
  }) || accumulator;
11381
11495
  },
11382
- //takes any value and returns an array. If it's a string (and leaveStrings isn't true), it'll use document.querySelectorAll() and convert that to an array. It'll also accept iterables like jQuery objects.
11496
+ // takes any value and returns an Array. If it's a string (and leaveStrings isn't true), it'll use document.querySelectorAll() and convert that to an array. It'll also accept iterables like jQuery objects.
11383
11497
  toArray = function toArray(value, scope, leaveStrings) {
11384
11498
  return _context && !scope && _context.selector ? _context.selector(value) : _isString(value) && !leaveStrings && (_coreInitted$1 || !_wake()) ? _slice.call((scope || _doc$1).querySelectorAll(value), 0) : _isArray(value) ? _flatten(value, leaveStrings) : _isArrayLike(value) ? _slice.call(value, 0) : value ? [value] : [];
11385
11499
  },
@@ -11396,7 +11510,7 @@ toArray = function toArray(value, scope, leaveStrings) {
11396
11510
  });
11397
11511
  },
11398
11512
  // alternative that's a bit faster and more reliably diverse but bigger: for (let j, v, i = a.length; i; j = (Math.random() * i) | 0, v = a[--i], a[i] = a[j], a[j] = v); return a;
11399
- //for distributing values across an array. Can accept a number, a function or (most commonly) a function which can contain the following properties: {base, amount, from, ease, grid, axis, length, each}. Returns a function that expects the following parameters: index, target, array. Recognizes the following
11513
+ // for distributing values across an Array. Can accept a number, a function or (most commonly) an object which can contain the following properties: {base, amount, from, ease, grid, axis, length, each}. Returns a function that expects the following parameters: index, target, array.
11400
11514
  distribute = function distribute(v) {
11401
11515
  if (_isFunction(v)) {
11402
11516
  return v;
@@ -11583,24 +11697,13 @@ distribute = function distribute(v) {
11583
11697
  return min + (value > range ? total - value : value);
11584
11698
  });
11585
11699
  },
11586
- _replaceRandom = function _replaceRandom(value) {
11587
- //replaces all occurrences of random(...) in a string with the calculated random value. can be a range like random(-100, 100, 5) or an array like random([0, 100, 500])
11588
- var prev = 0,
11589
- s = "",
11590
- i,
11591
- nums,
11592
- end,
11593
- isArray;
11594
-
11595
- while (~(i = value.indexOf("random(", prev))) {
11596
- end = value.indexOf(")", i);
11597
- isArray = value.charAt(i + 7) === "[";
11598
- nums = value.substr(i + 7, end - i - 7).match(isArray ? _delimitedValueExp : _strictNumExp);
11599
- s += value.substr(prev, i - prev) + random(isArray ? nums : +nums[0], isArray ? 0 : +nums[1], +nums[2] || 1e-5);
11600
- prev = end + 1;
11601
- }
11602
-
11603
- return s + value.substr(prev, value.length - prev);
11700
+ _replaceRandom = function _replaceRandom(s) {
11701
+ return s.replace(_randomExp, function (match) {
11702
+ //replaces all occurrences of random(...) in a string with the calculated random value. can be a range like random(-100, 100, 5) or an array like random([0, 100, 500])
11703
+ var arIndex = match.indexOf("[") + 1,
11704
+ values = match.substring(arIndex || 7, arIndex ? match.indexOf("]") : match.length - 1).split(_commaDelimExp);
11705
+ return random(arIndex ? values : +values[0], arIndex ? 0 : +values[1], +values[2] || 1e-5);
11706
+ });
11604
11707
  },
11605
11708
  mapRange = function mapRange(inMin, inMax, outMin, outMax, value) {
11606
11709
  var inRange = inMax - inMin,
@@ -12436,7 +12539,7 @@ var Animation = /*#__PURE__*/function () {
12436
12539
  }
12437
12540
  }
12438
12541
 
12439
- if (this._tTime !== _totalTime || !this._dur && !suppressEvents || this._initted && Math.abs(this._zTime) === _tinyNum || !_totalTime && !this._initted && (this.add || this._ptLookup)) {
12542
+ if (this._tTime !== _totalTime || !this._dur && !suppressEvents || this._initted && Math.abs(this._zTime) === _tinyNum || !this._initted && this._dur && _totalTime || !_totalTime && !this._initted && (this.add || this._ptLookup)) {
12440
12543
  // check for _ptLookup on a Tween instance to ensure it has actually finished being instantiated, otherwise if this.reverse() gets called in the Animation constructor, it could trigger a render() here even though the _targets weren't populated, thus when _init() is called there won't be any PropTweens (it'll act like the tween is non-functional)
12441
12544
  this._ts || (this._pTime = _totalTime); // otherwise, if an animation is paused, then the playhead is moved back to zero, then resumed, it'd revert back to the original time at the pause
12442
12545
  //if (!this._lock) { // avoid endless recursion (not sure we need this yet or if it's worth the performance hit)
@@ -12531,9 +12634,9 @@ var Animation = /*#__PURE__*/function () {
12531
12634
 
12532
12635
  _proto.startTime = function startTime(value) {
12533
12636
  if (arguments.length) {
12534
- this._start = value;
12637
+ this._start = _roundPrecise(value);
12535
12638
  var parent = this.parent || this._dp;
12536
- parent && (parent._sort || !this.parent) && _addToTimeline(parent, this, value - this._delay);
12639
+ parent && (parent._sort || !this.parent) && _addToTimeline(parent, this, this._start - this._delay);
12537
12640
  return this;
12538
12641
  }
12539
12642
 
@@ -12683,13 +12786,15 @@ var Animation = /*#__PURE__*/function () {
12683
12786
  };
12684
12787
 
12685
12788
  _proto.then = function then(onFulfilled) {
12686
- var self = this;
12789
+ var self = this,
12790
+ prevProm = self._prom;
12687
12791
  return new Promise(function (resolve) {
12688
12792
  var f = _isFunction(onFulfilled) ? onFulfilled : _passThrough,
12689
12793
  _resolve = function _resolve() {
12690
12794
  var _then = self.then;
12691
12795
  self.then = null; // temporarily null the then() method to avoid an infinite loop (see https://github.com/greensock/GSAP/issues/322)
12692
12796
 
12797
+ prevProm && prevProm();
12693
12798
  _isFunction(f) && (f = f(self)) && (f.then || f === self) && (self.then = _then);
12694
12799
  resolve(f);
12695
12800
  self.then = _then;
@@ -12910,7 +13015,11 @@ var Timeline = /*#__PURE__*/function (_Animation) {
12910
13015
  this._tTime = tTime; // if a user gets the iteration() inside the onRepeat, for example, it should be accurate.
12911
13016
 
12912
13017
  !suppressEvents && this.parent && _callback(this, "onRepeat");
12913
- this.vars.repeatRefresh && !isYoyo && (this.invalidate()._lock = 1);
13018
+
13019
+ if (this.vars.repeatRefresh && !isYoyo) {
13020
+ this.invalidate()._lock = 1;
13021
+ prevIteration = iteration; // otherwise, the onStart() may fire on the 2nd iteration.
13022
+ }
12914
13023
 
12915
13024
  if (prevTime && prevTime !== this._time || prevPaused !== !this._ts || this.vars.onRepeat && !this.parent && !this._act) {
12916
13025
  // if prevTime is 0 and we render at the very end, _time will be the end, thus won't match. So in this edge case, prevTime won't match _time but that's okay. If it gets killed in the onRepeat, eject as well.
@@ -12958,7 +13067,7 @@ var Timeline = /*#__PURE__*/function (_Animation) {
12958
13067
  prevTime = 0; // upon init, the playhead should always go forward; someone could invalidate() a completed timeline and then if they restart(), that would make child tweens render in reverse order which could lock in the wrong starting values if they build on each other, like tl.to(obj, {x: 100}).to(obj, {x: 0}).
12959
13068
  }
12960
13069
 
12961
- if (!prevTime && tTime && !suppressEvents && !prevIteration) {
13070
+ if (!prevTime && tTime && dur && !suppressEvents && !prevIteration) {
12962
13071
  _callback(this, "onStart");
12963
13072
 
12964
13073
  if (this._tTime !== tTime) {
@@ -13305,6 +13414,7 @@ var Timeline = /*#__PURE__*/function (_Animation) {
13305
13414
  var child = this._first,
13306
13415
  labels = this.labels,
13307
13416
  p;
13417
+ amount = _roundPrecise(amount);
13308
13418
 
13309
13419
  while (child) {
13310
13420
  if (child._start >= ignoreBeforeTime) {
@@ -13394,7 +13504,7 @@ var Timeline = /*#__PURE__*/function (_Animation) {
13394
13504
  max -= start;
13395
13505
 
13396
13506
  if (!parent && !self._dp || parent && parent.smoothChildTiming) {
13397
- self._start += start / self._ts;
13507
+ self._start += _roundPrecise(start / self._ts);
13398
13508
  self._time -= start;
13399
13509
  self._tTime -= start;
13400
13510
  }
@@ -15194,7 +15304,7 @@ var gsap$1 = _gsap.registerPlugin({
15194
15304
  }
15195
15305
  }, _buildModifierPlugin("roundProps", _roundModifier), _buildModifierPlugin("modifiers"), _buildModifierPlugin("snap", snap)) || _gsap; //to prevent the core plugins from being dropped via aggressive tree shaking, we must include them in the variable declaration in this way.
15196
15306
 
15197
- Tween.version = Timeline.version = gsap$1.version = "3.13.0";
15307
+ Tween.version = Timeline.version = gsap$1.version = "3.14.2";
15198
15308
  _coreReady = 1;
15199
15309
  _windowExists$2() && _wake();
15200
15310
  _easeMap.Power0;
@@ -15217,7 +15327,7 @@ _easeMap.Power0;
15217
15327
  _easeMap.Circ;
15218
15328
 
15219
15329
  /*!
15220
- * CSSPlugin 3.13.0
15330
+ * CSSPlugin 3.14.2
15221
15331
  * https://gsap.com
15222
15332
  *
15223
15333
  * Copyright 2008-2025, GreenSock. All rights reserved.
@@ -15259,6 +15369,10 @@ var _win$1,
15259
15369
  return data.set(data.t, data.p, ratio ? Math.round((data.s + data.c * ratio) * 10000) / 10000 + data.u : data.b, data);
15260
15370
  },
15261
15371
  //if units change, we need a way to render the original unit/value when the tween goes all the way back to the beginning (ratio:0)
15372
+ _renderCSSPropWithBeginningAndEnd = function _renderCSSPropWithBeginningAndEnd(ratio, data) {
15373
+ return data.set(data.t, data.p, ratio === 1 ? data.e : ratio ? Math.round((data.s + data.c * ratio) * 10000) / 10000 + data.u : data.b, data);
15374
+ },
15375
+ //if units change, we need a way to render the original unit/value when the tween goes all the way back to the beginning (ratio:0)
15262
15376
  _renderRoundedCSSProp = function _renderRoundedCSSProp(ratio, data) {
15263
15377
  var value = data.s + data.c * ratio;
15264
15378
  data.set(data.t, data.p, ~~(value + (value < 0 ? -0.5 : .5)) + data.u, data);
@@ -16558,7 +16672,8 @@ var CSSPlugin = {
16558
16672
  cache,
16559
16673
  smooth,
16560
16674
  hasPriority,
16561
- inlineProps;
16675
+ inlineProps,
16676
+ finalTransformValue;
16562
16677
  _pluginInitted || _initCore$1(); // we may call init() multiple times on the same plugin instance, like when adding special properties, so make sure we don't overwrite the revert data or inlineProps
16563
16678
 
16564
16679
  this.styles = this.styles || _getStyleSaver$1(target);
@@ -16601,9 +16716,9 @@ var CSSPlugin = {
16601
16716
  // colors don't have units
16602
16717
  startUnit = getUnit(startValue);
16603
16718
  endUnit = getUnit(endValue);
16719
+ endUnit ? startUnit !== endUnit && (startValue = _convertToUnit(target, p, startValue, endUnit) + endUnit) : startUnit && (endValue += startUnit);
16604
16720
  }
16605
16721
 
16606
- endUnit ? startUnit !== endUnit && (startValue = _convertToUnit(target, p, startValue, endUnit) + endUnit) : startUnit && (endValue += startUnit);
16607
16722
  this.add(style, "setProperty", startValue, endValue, index, targets, 0, 0, p);
16608
16723
  props.push(p);
16609
16724
  inlineProps.push(p, 0, style[p]);
@@ -16647,9 +16762,18 @@ var CSSPlugin = {
16647
16762
 
16648
16763
  if (isTransformRelated) {
16649
16764
  this.styles.save(p);
16765
+ finalTransformValue = endValue; // this is always the same as endValue except when it's a var(--) value, in which case we need to calculate the end value.
16650
16766
 
16651
16767
  if (type === "string" && endValue.substring(0, 6) === "var(--") {
16652
16768
  endValue = _getComputedProperty(target, endValue.substring(4, endValue.indexOf(")")));
16769
+
16770
+ if (endValue.substring(0, 5) === "calc(") {
16771
+ var origPerspective = target.style.perspective;
16772
+ target.style.perspective = endValue;
16773
+ endValue = _getComputedProperty(target, "perspective");
16774
+ origPerspective ? target.style.perspective = origPerspective : _removeProperty(target, "perspective");
16775
+ }
16776
+
16653
16777
  endNum = parseFloat(endValue);
16654
16778
  }
16655
16779
 
@@ -16716,7 +16840,11 @@ var CSSPlugin = {
16716
16840
  this._pt = new PropTween(this._pt, isTransformRelated ? cache : style, p, startNum, (relative ? _parseRelative(startNum, relative + endNum) : endNum) - startNum, !isTransformRelated && (endUnit === "px" || p === "zIndex") && vars.autoRound !== false ? _renderRoundedCSSProp : _renderCSSProp);
16717
16841
  this._pt.u = endUnit || 0;
16718
16842
 
16719
- if (startUnit !== endUnit && endUnit !== "%") {
16843
+ if (isTransformRelated && finalTransformValue !== endValue) {
16844
+ this._pt.b = startValue;
16845
+ this._pt.e = finalTransformValue;
16846
+ this._pt.r = _renderCSSPropWithBeginningAndEnd;
16847
+ } else if (startUnit !== endUnit && endUnit !== "%") {
16720
16848
  //when the tween goes all the way back to the beginning, we need to revert it to the OLD/ORIGINAL value (with those units). We record that as a "b" (beginning) property and point to a render method that handles that. (performance optimization)
16721
16849
  this._pt.b = startValue;
16722
16850
  this._pt.r = _renderCSSPropWithBeginning;
@@ -18255,6 +18383,23 @@ class GveSnapshotRendition extends HTMLElement {
18255
18383
  background: #f5f5f5;
18256
18384
  }
18257
18385
 
18386
+ .toolbar-log-display {
18387
+ padding: 0.5rem 1rem;
18388
+ font-size: 13px;
18389
+ color: var(--gve-text-color, #333);
18390
+ font-style: italic;
18391
+ overflow: hidden;
18392
+ text-overflow: ellipsis;
18393
+ text-align: center;
18394
+ max-width: 800px;
18395
+ display: -webkit-box;
18396
+ -webkit-line-clamp: 2;
18397
+ -webkit-box-orient: vertical;
18398
+ line-height: 1.4;
18399
+ max-height: calc(1.4em * 2);
18400
+ cursor: help;
18401
+ }
18402
+
18258
18403
  .version-label {
18259
18404
  font-family: monospace;
18260
18405
  font-size: 16px;
@@ -18646,6 +18791,26 @@ class GveSnapshotRendition extends HTMLElement {
18646
18791
  overflow: hidden;
18647
18792
  }
18648
18793
 
18794
+ .details-area.collapsed .details-resize-handle {
18795
+ display: none;
18796
+ }
18797
+
18798
+ .details-resize-handle {
18799
+ position: absolute;
18800
+ bottom: 0;
18801
+ left: 0;
18802
+ right: 0;
18803
+ height: 6px;
18804
+ cursor: ns-resize;
18805
+ background: transparent;
18806
+ transition: background-color 0.2s ease;
18807
+ z-index: 10;
18808
+ }
18809
+
18810
+ .details-resize-handle:hover {
18811
+ background-color: var(--gve-border-color, #ddd);
18812
+ }
18813
+
18649
18814
  .details-header {
18650
18815
  display: flex;
18651
18816
  justify-content: space-between;
@@ -80731,7 +80896,7 @@ var libExports = /*@__PURE__*/ requireLib();
80731
80896
  var HighlightJS = /*@__PURE__*/getDefaultExportFromCjs(libExports);
80732
80897
 
80733
80898
  /*!
80734
- * DrawSVGPlugin 3.13.0
80899
+ * DrawSVGPlugin 3.14.2
80735
80900
  * https://gsap.com
80736
80901
  *
80737
80902
  * @license Copyright 2008-2025, GreenSock. All rights reserved.
@@ -80930,7 +81095,7 @@ _parse = function _parse(value, length, defaultStart) {
80930
81095
  };
80931
81096
 
80932
81097
  var DrawSVGPlugin = {
80933
- version: "3.13.0",
81098
+ version: "3.14.2",
80934
81099
  name: "drawSVG",
80935
81100
  register: function register(core) {
80936
81101
  gsap = core;
@@ -81058,7 +81223,9 @@ class GveHintDesigner extends HTMLElement {
81058
81223
  super();
81059
81224
  // Properties
81060
81225
  this._data = { hints: {}, animations: {} };
81061
- this._settings = { ...DEFAULT_HINT_DESIGNER_SETTINGS };
81226
+ this._settings = {
81227
+ ...DEFAULT_HINT_DESIGNER_SETTINGS,
81228
+ };
81062
81229
  this._hintVariables = [];
81063
81230
  // Custom zoom/pan state
81064
81231
  this._zoomLevel = 1;
@@ -81141,9 +81308,18 @@ class GveHintDesigner extends HTMLElement {
81141
81308
  return this._hintVariables;
81142
81309
  }
81143
81310
  set hintVariables(value) {
81144
- this._hintVariables = value || [];
81311
+ // Ensure value is an array
81312
+ if (!Array.isArray(value)) {
81313
+ this._logger.warn("Component", "hintVariables setter received non-array value, converting to array", value);
81314
+ this._hintVariables = [];
81315
+ }
81316
+ else {
81317
+ this._hintVariables = value || [];
81318
+ }
81145
81319
  this.refreshVariablesTable();
81146
- this.fireEvent("hintVariablesChange", { hintVariables: this._hintVariables });
81320
+ this.fireEvent("hintVariablesChange", {
81321
+ hintVariables: this._hintVariables,
81322
+ });
81147
81323
  }
81148
81324
  // ==================== RENDERING ====================
81149
81325
  /**
@@ -81202,7 +81378,8 @@ class GveHintDesigner extends HTMLElement {
81202
81378
  // Hints dropdown
81203
81379
  this._hintsDropdown = document.createElement("select");
81204
81380
  this._hintsDropdown.className = "hints-dropdown";
81205
- this._hintsDropdown.innerHTML = '<option value="">-- Select Hint --</option>';
81381
+ this._hintsDropdown.innerHTML =
81382
+ '<option value="">-- Select Hint --</option>';
81206
81383
  toolbar.appendChild(this._hintsDropdown);
81207
81384
  // Save button (accent)
81208
81385
  const saveBtn = this.createButton("save-hint", "save", "Save hint");
@@ -81260,7 +81437,6 @@ class GveHintDesigner extends HTMLElement {
81260
81437
  const width = this._settings.hintDesignWidth;
81261
81438
  const height = this._settings.hintDesignHeight;
81262
81439
  const gridSize = 10; // Grid cell size in pixels
81263
- const rulerSize = 20; // Ruler width/height in pixels
81264
81440
  // Create defs section for the grid pattern
81265
81441
  const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
81266
81442
  // Define grid pattern
@@ -81273,7 +81449,7 @@ class GveHintDesigner extends HTMLElement {
81273
81449
  const gridPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
81274
81450
  gridPath.setAttribute("d", `M ${gridSize} 0 L 0 0 0 ${gridSize}`);
81275
81451
  gridPath.setAttribute("fill", "none");
81276
- gridPath.setAttribute("stroke", "#e0e0e0");
81452
+ gridPath.setAttribute("stroke", "#999999");
81277
81453
  gridPath.setAttribute("stroke-width", "0.5");
81278
81454
  pattern.appendChild(gridPath);
81279
81455
  defs.appendChild(pattern);
@@ -81281,62 +81457,32 @@ class GveHintDesigner extends HTMLElement {
81281
81457
  // Create main group for grid and rulers (will be behind hint content)
81282
81458
  const backgroundGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
81283
81459
  backgroundGroup.setAttribute("id", "grid-and-rulers");
81284
- // Add grid background rectangle
81460
+ // Add grid background rectangle - now starting at (0,0)
81285
81461
  const gridRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
81286
- gridRect.setAttribute("x", rulerSize.toString());
81287
- gridRect.setAttribute("y", rulerSize.toString());
81288
- gridRect.setAttribute("width", (width - rulerSize).toString());
81289
- gridRect.setAttribute("height", (height - rulerSize).toString());
81462
+ gridRect.setAttribute("x", "0");
81463
+ gridRect.setAttribute("y", "0");
81464
+ gridRect.setAttribute("width", width.toString());
81465
+ gridRect.setAttribute("height", height.toString());
81290
81466
  gridRect.setAttribute("fill", "url(#grid)");
81291
81467
  backgroundGroup.appendChild(gridRect);
81292
- // Add horizontal ruler (top)
81293
- const hRuler = document.createElementNS("http://www.w3.org/2000/svg", "rect");
81294
- hRuler.setAttribute("x", rulerSize.toString());
81295
- hRuler.setAttribute("y", "0");
81296
- hRuler.setAttribute("width", (width - rulerSize).toString());
81297
- hRuler.setAttribute("height", rulerSize.toString());
81298
- hRuler.setAttribute("fill", "#f5f5f5");
81299
- hRuler.setAttribute("stroke", "#ccc");
81300
- hRuler.setAttribute("stroke-width", "1");
81301
- backgroundGroup.appendChild(hRuler);
81302
- // Add vertical ruler (left)
81303
- const vRuler = document.createElementNS("http://www.w3.org/2000/svg", "rect");
81304
- vRuler.setAttribute("x", "0");
81305
- vRuler.setAttribute("y", rulerSize.toString());
81306
- vRuler.setAttribute("width", rulerSize.toString());
81307
- vRuler.setAttribute("height", (height - rulerSize).toString());
81308
- vRuler.setAttribute("fill", "#f5f5f5");
81309
- vRuler.setAttribute("stroke", "#ccc");
81310
- vRuler.setAttribute("stroke-width", "1");
81311
- backgroundGroup.appendChild(vRuler);
81312
- // Add corner square
81313
- const corner = document.createElementNS("http://www.w3.org/2000/svg", "rect");
81314
- corner.setAttribute("x", "0");
81315
- corner.setAttribute("y", "0");
81316
- corner.setAttribute("width", rulerSize.toString());
81317
- corner.setAttribute("height", rulerSize.toString());
81318
- corner.setAttribute("fill", "#e8e8e8");
81319
- corner.setAttribute("stroke", "#ccc");
81320
- corner.setAttribute("stroke-width", "1");
81321
- backgroundGroup.appendChild(corner);
81322
81468
  // Add ruler tick marks and labels (horizontal)
81323
81469
  const tickInterval = 50; // Major tick every 50 pixels
81324
81470
  const minorTickInterval = 10; // Minor tick every 10 pixels
81325
- for (let x = 0; x <= width - rulerSize; x += minorTickInterval) {
81471
+ for (let x = 0; x <= width; x += minorTickInterval) {
81326
81472
  const isMajorTick = x % tickInterval === 0;
81327
81473
  const tickHeight = isMajorTick ? 8 : 4;
81328
81474
  const tick = document.createElementNS("http://www.w3.org/2000/svg", "line");
81329
- tick.setAttribute("x1", (rulerSize + x).toString());
81330
- tick.setAttribute("y1", (rulerSize - tickHeight).toString());
81331
- tick.setAttribute("x2", (rulerSize + x).toString());
81332
- tick.setAttribute("y2", rulerSize.toString());
81475
+ tick.setAttribute("x1", x.toString());
81476
+ tick.setAttribute("y1", "0");
81477
+ tick.setAttribute("x2", x.toString());
81478
+ tick.setAttribute("y2", tickHeight.toString());
81333
81479
  tick.setAttribute("stroke", "#666");
81334
81480
  tick.setAttribute("stroke-width", "1");
81335
81481
  backgroundGroup.appendChild(tick);
81336
- // Add label for major ticks
81482
+ // Add label for major ticks - positioned inside the grid, below the top edge
81337
81483
  if (isMajorTick && x > 0) {
81338
81484
  const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
81339
- label.setAttribute("x", (rulerSize + x).toString());
81485
+ label.setAttribute("x", x.toString());
81340
81486
  label.setAttribute("y", "12");
81341
81487
  label.setAttribute("font-size", "9");
81342
81488
  label.setAttribute("font-family", "Arial, sans-serif");
@@ -81347,26 +81493,26 @@ class GveHintDesigner extends HTMLElement {
81347
81493
  }
81348
81494
  }
81349
81495
  // Add ruler tick marks and labels (vertical)
81350
- for (let y = 0; y <= height - rulerSize; y += minorTickInterval) {
81496
+ for (let y = 0; y <= height; y += minorTickInterval) {
81351
81497
  const isMajorTick = y % tickInterval === 0;
81352
81498
  const tickWidth = isMajorTick ? 8 : 4;
81353
81499
  const tick = document.createElementNS("http://www.w3.org/2000/svg", "line");
81354
- tick.setAttribute("x1", (rulerSize - tickWidth).toString());
81355
- tick.setAttribute("y1", (rulerSize + y).toString());
81356
- tick.setAttribute("x2", rulerSize.toString());
81357
- tick.setAttribute("y2", (rulerSize + y).toString());
81500
+ tick.setAttribute("x1", "0");
81501
+ tick.setAttribute("y1", y.toString());
81502
+ tick.setAttribute("x2", tickWidth.toString());
81503
+ tick.setAttribute("y2", y.toString());
81358
81504
  tick.setAttribute("stroke", "#666");
81359
81505
  tick.setAttribute("stroke-width", "1");
81360
81506
  backgroundGroup.appendChild(tick);
81361
- // Add label for major ticks
81507
+ // Add label for major ticks - positioned inside the grid, to the right of the left edge
81362
81508
  if (isMajorTick && y > 0) {
81363
81509
  const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
81364
81510
  label.setAttribute("x", "10");
81365
- label.setAttribute("y", (rulerSize + y + 3).toString());
81511
+ label.setAttribute("y", (y + 3).toString());
81366
81512
  label.setAttribute("font-size", "9");
81367
81513
  label.setAttribute("font-family", "Arial, sans-serif");
81368
81514
  label.setAttribute("fill", "#333");
81369
- label.setAttribute("text-anchor", "middle");
81515
+ label.setAttribute("text-anchor", "start");
81370
81516
  label.textContent = y.toString();
81371
81517
  backgroundGroup.appendChild(label);
81372
81518
  }
@@ -81514,7 +81660,8 @@ class GveHintDesigner extends HTMLElement {
81514
81660
  // Animation dropdown
81515
81661
  this._animationsDropdown = document.createElement("select");
81516
81662
  this._animationsDropdown.className = "animations-dropdown";
81517
- this._animationsDropdown.innerHTML = '<option value="">-- Select Animation --</option>';
81663
+ this._animationsDropdown.innerHTML =
81664
+ '<option value="">-- Select Animation --</option>';
81518
81665
  animControls.appendChild(this._animationsDropdown);
81519
81666
  // Load button
81520
81667
  const loadAnimBtn = this.createButton("load-animation", "download-cloud", "Load animation code");
@@ -81542,10 +81689,14 @@ class GveHintDesigner extends HTMLElement {
81542
81689
  createHintVariablesList() {
81543
81690
  const panel = document.createElement("div");
81544
81691
  panel.className = "variables-panel resizable-panel";
81545
- // Header
81692
+ // Header with warning icon placeholder
81546
81693
  const header = document.createElement("div");
81547
81694
  header.className = "panel-header";
81548
- header.textContent = "Hint Variables";
81695
+ const headerText = document.createElement("span");
81696
+ headerText.textContent = "Hint Variables";
81697
+ header.appendChild(headerText);
81698
+ // Store reference to header for adding warning icon
81699
+ this._variablesPanelHeader = header;
81549
81700
  panel.appendChild(header);
81550
81701
  // Toolbar
81551
81702
  const toolbar = document.createElement("div");
@@ -81553,10 +81704,18 @@ class GveHintDesigner extends HTMLElement {
81553
81704
  const addVarBtn = this.createButton("add-variable", "plus-circle", "Add new variable");
81554
81705
  addVarBtn.classList.add("btn-primary");
81555
81706
  toolbar.appendChild(addVarBtn);
81707
+ const populateBtn = this.createButton("populate-from-hints", "download-cloud", "Populate from all hints");
81708
+ populateBtn.classList.add("btn-accent");
81709
+ toolbar.appendChild(populateBtn);
81556
81710
  const deleteAllBtn = this.createButton("delete-all-vars", "trash-2", "Delete all variables");
81557
81711
  deleteAllBtn.classList.add("btn-danger");
81558
81712
  toolbar.appendChild(deleteAllBtn);
81559
81713
  panel.appendChild(toolbar);
81714
+ // Validation message (initially hidden)
81715
+ this._variablesValidationMsg = document.createElement("div");
81716
+ this._variablesValidationMsg.className = "validation-message";
81717
+ this._variablesValidationMsg.style.display = "none";
81718
+ panel.appendChild(this._variablesValidationMsg);
81560
81719
  // Content
81561
81720
  const content = document.createElement("div");
81562
81721
  content.className = "panel-content";
@@ -81579,7 +81738,12 @@ class GveHintDesigner extends HTMLElement {
81579
81738
  // Use feather.icons to get the SVG directly (for Shadow DOM compatibility)
81580
81739
  const icon = featherExports.icons[iconName];
81581
81740
  if (icon) {
81582
- btn.innerHTML = icon.toSvg({ class: 'feather-icon', width: 16, height: 16, 'stroke-width': 2 });
81741
+ btn.innerHTML = icon.toSvg({
81742
+ class: "feather-icon",
81743
+ width: 16,
81744
+ height: 16,
81745
+ "stroke-width": 2,
81746
+ });
81583
81747
  }
81584
81748
  else {
81585
81749
  this._logger.warn("Feather icon not found:", iconName);
@@ -81649,14 +81813,14 @@ class GveHintDesigner extends HTMLElement {
81649
81813
  splitter.addEventListener("pointerdown", (e) => {
81650
81814
  const startY = e.clientY;
81651
81815
  const startHeightAbove = panelAbove.offsetHeight;
81652
- const container = this._shadow.querySelector('.hint-designer-container');
81816
+ const container = this._shadow.querySelector(".hint-designer-container");
81653
81817
  if (!container)
81654
81818
  return;
81655
81819
  const onMove = (moveEvent) => {
81656
81820
  const delta = moveEvent.clientY - startY;
81657
81821
  const newHeightAbove = Math.max(300, startHeightAbove + delta);
81658
81822
  // Determine which splitter this is based on the panel above
81659
- const isTopSplitter = panelAbove.classList.contains('top-panels-wrapper');
81823
+ const isTopSplitter = panelAbove.classList.contains("top-panels-wrapper");
81660
81824
  if (isTopSplitter) {
81661
81825
  // Splitter 1: between top and properties
81662
81826
  // Set top height, let properties and variables share remaining space
@@ -81665,7 +81829,7 @@ class GveHintDesigner extends HTMLElement {
81665
81829
  else {
81666
81830
  // Splitter 2: between properties and variables
81667
81831
  // Get top panel height and keep it fixed
81668
- const topPanel = this._shadow.querySelector('.top-panels-wrapper');
81832
+ const topPanel = this._shadow.querySelector(".top-panels-wrapper");
81669
81833
  const topHeight = topPanel ? topPanel.offsetHeight : 300;
81670
81834
  // Set top and properties heights, let variables fill remaining
81671
81835
  container.style.gridTemplateRows = `${topHeight}px 6px ${newHeightAbove}px 6px minmax(200px, 1fr)`;
@@ -81723,6 +81887,7 @@ class GveHintDesigner extends HTMLElement {
81723
81887
  // Variables panel
81724
81888
  this.addClickListener("delete-all-vars", () => this.deleteAllVariables());
81725
81889
  this.addClickListener("add-variable", () => this.addVariable());
81890
+ this.addClickListener("populate-from-hints", () => this.populateVariablesFromHints());
81726
81891
  // Word wrap toggles
81727
81892
  this.addClickListener("toggle-svg-wrap", () => this.toggleSvgWordWrap());
81728
81893
  this.addClickListener("toggle-js-wrap", () => this.toggleJsWordWrap());
@@ -81745,17 +81910,17 @@ class GveHintDesigner extends HTMLElement {
81745
81910
  * Initialize panning with mouse drag.
81746
81911
  */
81747
81912
  initializePanning() {
81748
- const panel = this._shadow.querySelector('.svg-display-panel');
81913
+ const panel = this._shadow.querySelector(".svg-display-panel");
81749
81914
  if (!panel)
81750
81915
  return;
81751
- panel.addEventListener('mousedown', (e) => {
81916
+ panel.addEventListener("mousedown", (e) => {
81752
81917
  const mouseEvent = e;
81753
81918
  this._isPanning = true;
81754
81919
  this._lastPanX = mouseEvent.clientX;
81755
81920
  this._lastPanY = mouseEvent.clientY;
81756
- panel.style.cursor = 'grabbing';
81921
+ panel.style.cursor = "grabbing";
81757
81922
  });
81758
- document.addEventListener('mousemove', (e) => {
81923
+ document.addEventListener("mousemove", (e) => {
81759
81924
  if (!this._isPanning)
81760
81925
  return;
81761
81926
  const deltaX = e.clientX - this._lastPanX;
@@ -81766,24 +81931,24 @@ class GveHintDesigner extends HTMLElement {
81766
81931
  this._lastPanY = e.clientY;
81767
81932
  this.updateSvgTransform();
81768
81933
  });
81769
- document.addEventListener('mouseup', () => {
81934
+ document.addEventListener("mouseup", () => {
81770
81935
  if (this._isPanning) {
81771
81936
  this._isPanning = false;
81772
81937
  const panelEl = panel;
81773
- panelEl.style.cursor = 'grab';
81938
+ panelEl.style.cursor = "grab";
81774
81939
  }
81775
81940
  });
81776
81941
  // Set initial cursor
81777
- panel.style.cursor = 'grab';
81942
+ panel.style.cursor = "grab";
81778
81943
  }
81779
81944
  /**
81780
81945
  * Initialize zooming with mouse wheel.
81781
81946
  */
81782
81947
  initializeWheelZoom() {
81783
- const panel = this._shadow.querySelector('.svg-display-panel');
81948
+ const panel = this._shadow.querySelector(".svg-display-panel");
81784
81949
  if (!panel)
81785
81950
  return;
81786
- panel.addEventListener('wheel', (e) => {
81951
+ panel.addEventListener("wheel", (e) => {
81787
81952
  const wheelEvent = e;
81788
81953
  wheelEvent.preventDefault();
81789
81954
  // Determine zoom direction based on wheel delta
@@ -81838,7 +82003,8 @@ class GveHintDesigner extends HTMLElement {
81838
82003
  if (!this._hintsDropdown)
81839
82004
  return;
81840
82005
  const currentValue = this._hintsDropdown.value;
81841
- this._hintsDropdown.innerHTML = '<option value="">-- Select Hint --</option>';
82006
+ this._hintsDropdown.innerHTML =
82007
+ '<option value="">-- Select Hint --</option>';
81842
82008
  const hintIds = Object.keys(this._data.hints);
81843
82009
  hintIds.forEach((hintId) => {
81844
82010
  const option = document.createElement("option");
@@ -81860,7 +82026,8 @@ class GveHintDesigner extends HTMLElement {
81860
82026
  if (!this._animationsDropdown)
81861
82027
  return;
81862
82028
  const currentValue = this._animationsDropdown.value;
81863
- this._animationsDropdown.innerHTML = '<option value="">-- Select Animation --</option>';
82029
+ this._animationsDropdown.innerHTML =
82030
+ '<option value="">-- Select Animation --</option>';
81864
82031
  Object.keys(this._data.animations).forEach((animId) => {
81865
82032
  const option = document.createElement("option");
81866
82033
  option.value = animId;
@@ -81931,7 +82098,8 @@ class GveHintDesigner extends HTMLElement {
81931
82098
  if (this._jsTextarea) {
81932
82099
  // If the ID is not found, use the template; otherwise use empty string
81933
82100
  const animCode = this._data.animations[animId];
81934
- this._jsTextarea.value = animCode !== undefined ? animCode : NEW_ANIMATION_TEMPLATE;
82101
+ this._jsTextarea.value =
82102
+ animCode !== undefined ? animCode : NEW_ANIMATION_TEMPLATE;
81935
82103
  this.highlightJs();
81936
82104
  }
81937
82105
  }
@@ -81988,6 +82156,49 @@ class GveHintDesigner extends HTMLElement {
81988
82156
  if (this._embeddedJsCheckbox)
81989
82157
  this._embeddedJsCheckbox.checked = false;
81990
82158
  }
82159
+ /**
82160
+ * Validate SVG code for well-formedness and structure.
82161
+ * Returns an error message if validation fails, or null if valid.
82162
+ */
82163
+ validateSvgCode(svgCode) {
82164
+ // Check if SVG code is empty
82165
+ if (!svgCode || !svgCode.trim()) {
82166
+ return "SVG code cannot be empty";
82167
+ }
82168
+ const trimmedCode = svgCode.trim();
82169
+ // Check that it starts with <g and ends with </g>
82170
+ if (!trimmedCode.startsWith("<g")) {
82171
+ return "SVG must start with a <g> element (e.g., <g> or <g ...>)";
82172
+ }
82173
+ if (!trimmedCode.endsWith("</g>")) {
82174
+ return "SVG must end with </g> (closing tag for the root <g> element)";
82175
+ }
82176
+ // Use DOMParser to validate XML well-formedness
82177
+ const parser = new DOMParser();
82178
+ const wrappedSvg = `<svg xmlns="http://www.w3.org/2000/svg">${svgCode}</svg>`;
82179
+ const doc = parser.parseFromString(wrappedSvg, "image/svg+xml");
82180
+ // Check for parsing errors
82181
+ const errorNode = doc.querySelector("parsererror");
82182
+ if (errorNode) {
82183
+ const errorText = errorNode.textContent || "Unknown XML parsing error";
82184
+ return `XML is not well-formed: ${errorText}`;
82185
+ }
82186
+ // Verify there is exactly one root <g> element
82187
+ const svgElement = doc.documentElement;
82188
+ const rootChildren = Array.from(svgElement.children);
82189
+ if (rootChildren.length === 0) {
82190
+ return "SVG must contain a <g> element";
82191
+ }
82192
+ if (rootChildren.length > 1) {
82193
+ return `SVG must have exactly one root <g> element, but found ${rootChildren.length} root elements. Wrap all content in a single <g> element.`;
82194
+ }
82195
+ const rootElement = rootChildren[0];
82196
+ if (rootElement.tagName.toLowerCase() !== "g") {
82197
+ return `Root element must be <g>, but found <${rootElement.tagName}>`;
82198
+ }
82199
+ // All validations passed
82200
+ return null;
82201
+ }
81991
82202
  /**
81992
82203
  * Add a new hint.
81993
82204
  */
@@ -82027,19 +82238,21 @@ class GveHintDesigner extends HTMLElement {
82027
82238
  // Validate SVG and JS
82028
82239
  const svgCode = this._svgTextarea?.value || "";
82029
82240
  const animationValue = this._jsTextarea?.value || "";
82241
+ // Validate SVG code for well-formedness and structure
82242
+ const svgValidationError = this.validateSvgCode(svgCode);
82243
+ if (svgValidationError) {
82244
+ this.showMessage(`SVG validation error: ${svgValidationError}`, "error");
82245
+ return; // Prevent saving on error
82246
+ }
82247
+ // Validate JS syntax if present
82030
82248
  try {
82031
- // Validate SVG (basic check)
82032
- if (svgCode && !svgCode.trim().startsWith("<g")) {
82033
- throw new Error("SVG must start with <g> element");
82034
- }
82035
- // Validate JS syntax if present (we'll do more specific validation below)
82036
82249
  if (animationValue && !animationValue.trim().startsWith("#")) {
82037
82250
  // Try to create a function to validate syntax
82038
82251
  new Function("gsap", "targetEl", "rootEl", animationValue);
82039
82252
  }
82040
82253
  }
82041
82254
  catch (err) {
82042
- this.showMessage(`Validation error: ${err}`, "error");
82255
+ this.showMessage(`JS validation error: ${err}`, "error");
82043
82256
  return; // Prevent saving on error
82044
82257
  }
82045
82258
  // Build hint object
@@ -82178,23 +82391,33 @@ class GveHintDesigner extends HTMLElement {
82178
82391
  if (node.nodeType === Node.ELEMENT_NODE) {
82179
82392
  const element = node;
82180
82393
  const tagName = element.tagName.toLowerCase();
82181
- const id = element.getAttribute('id');
82394
+ const id = element.getAttribute("id");
82182
82395
  // Keep defs and grid-and-rulers, remove everything else
82183
- if (tagName !== 'defs' && id !== 'grid-and-rulers') {
82396
+ if (tagName !== "defs" && id !== "grid-and-rulers") {
82184
82397
  nodesToRemove.push(node);
82185
82398
  }
82186
82399
  }
82187
82400
  }
82188
- nodesToRemove.forEach(node => this._svgDisplay.removeChild(node));
82401
+ nodesToRemove.forEach((node) => this._svgDisplay.removeChild(node));
82189
82402
  // If grid and rulers don't exist yet, add them
82190
- if (!this._svgDisplay.querySelector('#grid-and-rulers')) {
82403
+ if (!this._svgDisplay.querySelector("#grid-and-rulers")) {
82191
82404
  this.addGridAndRulers(this._svgDisplay);
82192
82405
  }
82193
- if (!this._hintId || !this._data.hints[this._hintId]) {
82406
+ if (!this._hintId) {
82194
82407
  return;
82195
82408
  }
82196
- const hint = this._data.hints[this._hintId];
82197
- let svgCode = hint.svg;
82409
+ // Get SVG code from textarea if it exists, otherwise from saved data
82410
+ let svgCode = this._svgTextarea?.value || "";
82411
+ // Fallback to saved data if textarea is empty and we have saved data
82412
+ if (!svgCode && this._data.hints[this._hintId]) {
82413
+ svgCode = this._data.hints[this._hintId].svg;
82414
+ }
82415
+ // If still no SVG code, return early
82416
+ if (!svgCode) {
82417
+ return;
82418
+ }
82419
+ // Validate variables before resolving them
82420
+ this.validateSvgVariables(svgCode);
82198
82421
  // Resolve variables
82199
82422
  svgCode = this.resolveVariables(svgCode);
82200
82423
  try {
@@ -82244,13 +82467,103 @@ class GveHintDesigner extends HTMLElement {
82244
82467
  });
82245
82468
  return resolved;
82246
82469
  }
82470
+ /**
82471
+ * Extract variable names from SVG code (variables in {{...}} placeholders).
82472
+ */
82473
+ extractVariablesFromSvg(svgCode) {
82474
+ const variablePattern = /\{\{([^}]+)\}\}/g;
82475
+ const variables = [];
82476
+ let match;
82477
+ while ((match = variablePattern.exec(svgCode)) !== null) {
82478
+ const varName = match[1].trim();
82479
+ if (varName && !variables.includes(varName)) {
82480
+ variables.push(varName);
82481
+ }
82482
+ }
82483
+ return variables;
82484
+ }
82485
+ /**
82486
+ * Validate SVG variables and update validation message.
82487
+ * Shows a warning if any variables used in SVG are not defined in hint variables.
82488
+ */
82489
+ validateSvgVariables(svgCode) {
82490
+ if (!this._variablesValidationMsg || !this._variablesPanelHeader) {
82491
+ return;
82492
+ }
82493
+ // Extract variables from SVG
82494
+ const usedVariables = this.extractVariablesFromSvg(svgCode);
82495
+ if (usedVariables.length === 0) {
82496
+ // No variables used - hide validation message and warning icon
82497
+ this._variablesValidationMsg.style.display = "none";
82498
+ this.updateVariablesHeaderWarning(false);
82499
+ return;
82500
+ }
82501
+ // Get defined variable names
82502
+ const definedVariables = this._hintVariables.map((v) => v.name);
82503
+ // Find undefined variables
82504
+ const undefinedVariables = usedVariables.filter((v) => !definedVariables.includes(v));
82505
+ if (undefinedVariables.length === 0) {
82506
+ // All variables are defined - hide validation message and warning icon
82507
+ this._variablesValidationMsg.style.display = "none";
82508
+ this.updateVariablesHeaderWarning(false);
82509
+ return;
82510
+ }
82511
+ // Show validation message with list of undefined variables
82512
+ this._variablesValidationMsg.style.display = "block";
82513
+ this._variablesValidationMsg.innerHTML = `
82514
+ <strong>⚠️ Undefined Variables:</strong>
82515
+ <div style="margin-top: 0.5rem;">
82516
+ The following variables are used in the SVG code but not defined in Hint Variables:
82517
+ <ul style="margin: 0.5rem 0; padding-left: 1.5rem;">
82518
+ ${undefinedVariables
82519
+ .map((v) => `<li><code>{{${v}}}</code></li>`)
82520
+ .join("")}
82521
+ </ul>
82522
+ <em>Add these variables to the list below to ensure the hint displays correctly.</em>
82523
+ </div>
82524
+ `;
82525
+ // Show warning icon in header
82526
+ this.updateVariablesHeaderWarning(true);
82527
+ }
82528
+ /**
82529
+ * Update warning icon in variables panel header.
82530
+ */
82531
+ updateVariablesHeaderWarning(showWarning) {
82532
+ if (!this._variablesPanelHeader)
82533
+ return;
82534
+ // Remove existing warning icon if present
82535
+ const existingWarning = this._variablesPanelHeader.querySelector(".warning-icon");
82536
+ if (existingWarning) {
82537
+ existingWarning.remove();
82538
+ }
82539
+ if (showWarning) {
82540
+ // Add warning icon
82541
+ const warningIcon = document.createElement("span");
82542
+ warningIcon.className = "warning-icon";
82543
+ warningIcon.title = "Some variables used in SVG are not defined";
82544
+ const icon = featherExports.icons["alert-triangle"];
82545
+ if (icon) {
82546
+ warningIcon.innerHTML = icon.toSvg({
82547
+ class: "feather-icon",
82548
+ width: 16,
82549
+ height: 16,
82550
+ "stroke-width": 2,
82551
+ color: "#ff9800",
82552
+ });
82553
+ }
82554
+ else {
82555
+ warningIcon.textContent = "⚠️";
82556
+ }
82557
+ this._variablesPanelHeader.appendChild(warningIcon);
82558
+ }
82559
+ }
82247
82560
  /**
82248
82561
  * Play animation.
82249
82562
  */
82250
82563
  playAnimation() {
82251
82564
  this._logger.debug("Animation", "playAnimation called", {
82252
82565
  hasTimeline: !!this._currentTimeline,
82253
- isPlaying: this._isPlaying
82566
+ isPlaying: this._isPlaying,
82254
82567
  });
82255
82568
  if (this._currentTimeline) {
82256
82569
  // If timeline exists, just play it
@@ -82288,7 +82601,7 @@ class GveHintDesigner extends HTMLElement {
82288
82601
  createTimeline() {
82289
82602
  this._logger.debug("Animation", "createTimeline called", {
82290
82603
  hasSvgDisplay: !!this._svgDisplay,
82291
- hintId: this._hintId
82604
+ hintId: this._hintId,
82292
82605
  });
82293
82606
  if (!this._svgDisplay || !this._hintId || !this._data.hints[this._hintId]) {
82294
82607
  this.showMessage("No hint to animate!", "error");
@@ -82302,11 +82615,16 @@ class GveHintDesigner extends HTMLElement {
82302
82615
  if (hint.animation.startsWith("#")) {
82303
82616
  const animId = hint.animation.substring(1);
82304
82617
  jsCode = this._data.animations[animId] || "";
82305
- this._logger.debug("Animation", "Using referenced animation", { animId, hasCode: !!jsCode });
82618
+ this._logger.debug("Animation", "Using referenced animation", {
82619
+ animId,
82620
+ hasCode: !!jsCode,
82621
+ });
82306
82622
  }
82307
82623
  else {
82308
82624
  jsCode = hint.animation;
82309
- this._logger.debug("Animation", "Using embedded animation", { codeLength: jsCode.length });
82625
+ this._logger.debug("Animation", "Using embedded animation", {
82626
+ codeLength: jsCode.length,
82627
+ });
82310
82628
  }
82311
82629
  }
82312
82630
  if (!jsCode) {
@@ -82323,7 +82641,10 @@ class GveHintDesigner extends HTMLElement {
82323
82641
  if (!hintEl) {
82324
82642
  throw new Error("No hint <g> element found in SVG");
82325
82643
  }
82326
- this._logger.debug("Animation", "Found hint element", { tagName: hintEl.tagName, id: hintEl.getAttribute('id') });
82644
+ this._logger.debug("Animation", "Found hint element", {
82645
+ tagName: hintEl.tagName,
82646
+ id: hintEl.getAttribute("id"),
82647
+ });
82327
82648
  // Set initial opacity to 0 (on style attribute)
82328
82649
  hintEl.style.opacity = "0";
82329
82650
  // Check if GSAP is available
@@ -82369,12 +82690,12 @@ class GveHintDesigner extends HTMLElement {
82369
82690
  }
82370
82691
  this._logger.debug("Animation", "Animation function executed", {
82371
82692
  resultType: typeof result,
82372
- isPromise: result && typeof result.then === 'function',
82373
- isTimeline: result && typeof result.progress === 'function',
82374
- capturedTweensCount: capturedTweens.length
82693
+ isPromise: result && typeof result.then === "function",
82694
+ isTimeline: result && typeof result.progress === "function",
82695
+ capturedTweensCount: capturedTweens.length,
82375
82696
  });
82376
82697
  // Check what was returned
82377
- if (result && typeof result.progress === 'function') {
82698
+ if (result && typeof result.progress === "function") {
82378
82699
  // Direct timeline/tween return
82379
82700
  this._currentTimeline = result;
82380
82701
  this._currentTimeline.pause();
@@ -82384,7 +82705,7 @@ class GveHintDesigner extends HTMLElement {
82384
82705
  // Animation called gsap methods - create timeline with captured tweens
82385
82706
  this._logger.debug("Animation", "Creating timeline from captured tweens", {
82386
82707
  tweenCount: capturedTweens.length,
82387
- tweenDurations: capturedTweens.map(t => t.duration())
82708
+ tweenDurations: capturedTweens.map((t) => t.duration()),
82388
82709
  });
82389
82710
  // The tweens have already started playing, so we need to:
82390
82711
  // 1. Kill them to stop auto-playing
@@ -82399,14 +82720,14 @@ class GveHintDesigner extends HTMLElement {
82399
82720
  duration: duration,
82400
82721
  totalDuration: totalDuration,
82401
82722
  targets: tween.targets(),
82402
- vars: tween.vars
82723
+ vars: tween.vars,
82403
82724
  });
82404
82725
  // Get the animation method that was used (to, from, fromTo)
82405
82726
  const targets = tween.targets();
82406
82727
  const vars = { ...tween.vars };
82407
82728
  // Get the initial values if this was a fromTo or from tween
82408
82729
  const isFromTo = tween._from !== undefined;
82409
- const isFrom = !isFromTo && tween.vars.hasOwnProperty('startAt');
82730
+ const isFrom = !isFromTo && tween.vars.hasOwnProperty("startAt");
82410
82731
  // Kill the original tween to stop it from playing
82411
82732
  tween.kill();
82412
82733
  // Recreate the tween in our timeline using the appropriate method
@@ -82425,7 +82746,7 @@ class GveHintDesigner extends HTMLElement {
82425
82746
  }
82426
82747
  });
82427
82748
  this._logger.debug("Animation", "Timeline built from tweens", {
82428
- timelineDuration: this._currentTimeline.duration()
82749
+ timelineDuration: this._currentTimeline.duration(),
82429
82750
  });
82430
82751
  }
82431
82752
  else {
@@ -82436,7 +82757,7 @@ class GveHintDesigner extends HTMLElement {
82436
82757
  this._logger.debug("Animation", "Timeline created", {
82437
82758
  duration: this._currentTimeline.duration(),
82438
82759
  progress: this._currentTimeline.progress(),
82439
- paused: this._currentTimeline.paused()
82760
+ paused: this._currentTimeline.paused(),
82440
82761
  });
82441
82762
  // Check if timeline has duration
82442
82763
  if (this._currentTimeline.duration() === 0) {
@@ -82448,7 +82769,7 @@ class GveHintDesigner extends HTMLElement {
82448
82769
  this._logger.debug("Animation", "Animation completed callback", {
82449
82770
  finalProgress: this._currentTimeline.progress(),
82450
82771
  finalTime: this._currentTimeline.time(),
82451
- duration: this._currentTimeline.duration()
82772
+ duration: this._currentTimeline.duration(),
82452
82773
  });
82453
82774
  this._isPlaying = false;
82454
82775
  if (this._playButton)
@@ -82464,7 +82785,7 @@ class GveHintDesigner extends HTMLElement {
82464
82785
  });
82465
82786
  this.showMessage("", "");
82466
82787
  this._logger.info("Animation", "Timeline created successfully", {
82467
- duration: this._currentTimeline.duration()
82788
+ duration: this._currentTimeline.duration(),
82468
82789
  });
82469
82790
  }
82470
82791
  catch (err) {
@@ -82483,7 +82804,7 @@ class GveHintDesigner extends HTMLElement {
82483
82804
  pauseAnimation() {
82484
82805
  this._logger.debug("Animation", "pauseAnimation called", {
82485
82806
  hasTimeline: !!this._currentTimeline,
82486
- isPlaying: this._isPlaying
82807
+ isPlaying: this._isPlaying,
82487
82808
  });
82488
82809
  if (this._currentTimeline) {
82489
82810
  this._currentTimeline.pause();
@@ -82494,7 +82815,7 @@ class GveHintDesigner extends HTMLElement {
82494
82815
  this._pauseButton.disabled = true;
82495
82816
  this.stopProgressUpdate();
82496
82817
  this._logger.debug("Animation", "Animation paused", {
82497
- progress: this._currentTimeline.progress()
82818
+ progress: this._currentTimeline.progress(),
82498
82819
  });
82499
82820
  }
82500
82821
  }
@@ -82504,7 +82825,7 @@ class GveHintDesigner extends HTMLElement {
82504
82825
  restartAnimation() {
82505
82826
  this._logger.debug("Animation", "restartAnimation called", {
82506
82827
  hasTimeline: !!this._currentTimeline,
82507
- isPlaying: this._isPlaying
82828
+ isPlaying: this._isPlaying,
82508
82829
  });
82509
82830
  if (this._currentTimeline) {
82510
82831
  this._logger.debug("Animation", "Restarting existing timeline");
@@ -82529,7 +82850,7 @@ class GveHintDesigner extends HTMLElement {
82529
82850
  this._logger.debug("Animation", "seekAnimation called", {
82530
82851
  hasProgressBar: !!this._progressBar,
82531
82852
  hasTimeline: !!this._currentTimeline,
82532
- progressBarValue: this._progressBar?.value
82853
+ progressBarValue: this._progressBar?.value,
82533
82854
  });
82534
82855
  if (!this._progressBar)
82535
82856
  return;
@@ -82547,7 +82868,7 @@ class GveHintDesigner extends HTMLElement {
82547
82868
  progressBarValue: this._progressBar.value,
82548
82869
  calculatedProgress: progress,
82549
82870
  wasPlaying: this._isPlaying,
82550
- timelineDuration: this._currentTimeline.duration()
82871
+ timelineDuration: this._currentTimeline.duration(),
82551
82872
  });
82552
82873
  // Pause the timeline to prevent it from continuing to play
82553
82874
  this._isPlaying;
@@ -82556,7 +82877,7 @@ class GveHintDesigner extends HTMLElement {
82556
82877
  this._currentTimeline.progress(progress);
82557
82878
  this._logger.debug("Animation", "Seek completed", {
82558
82879
  newProgress: this._currentTimeline.progress(),
82559
- timelinePaused: this._currentTimeline.paused()
82880
+ timelinePaused: this._currentTimeline.paused(),
82560
82881
  });
82561
82882
  // Update playing state
82562
82883
  this._isPlaying = false;
@@ -82580,7 +82901,7 @@ class GveHintDesigner extends HTMLElement {
82580
82901
  }
82581
82902
  }, 50); // Update every 50ms
82582
82903
  this._logger.debug("Animation", "Progress update started", {
82583
- intervalId: this._progressUpdateInterval
82904
+ intervalId: this._progressUpdateInterval,
82584
82905
  });
82585
82906
  }
82586
82907
  /**
@@ -82589,7 +82910,7 @@ class GveHintDesigner extends HTMLElement {
82589
82910
  stopProgressUpdate() {
82590
82911
  if (this._progressUpdateInterval) {
82591
82912
  this._logger.debug("Animation", "Stopping progress update", {
82592
- intervalId: this._progressUpdateInterval
82913
+ intervalId: this._progressUpdateInterval,
82593
82914
  });
82594
82915
  clearInterval(this._progressUpdateInterval);
82595
82916
  this._progressUpdateInterval = undefined;
@@ -82616,7 +82937,7 @@ class GveHintDesigner extends HTMLElement {
82616
82937
  if (!this._svgDisplay)
82617
82938
  return;
82618
82939
  // Get panel size to center the SVG
82619
- const panel = this._shadow.querySelector('.svg-display-panel');
82940
+ const panel = this._shadow.querySelector(".svg-display-panel");
82620
82941
  if (panel) {
82621
82942
  const panelWidth = panel.clientWidth;
82622
82943
  const panelHeight = panel.clientHeight;
@@ -82647,8 +82968,8 @@ class GveHintDesigner extends HTMLElement {
82647
82968
  panY: this._panY,
82648
82969
  zoomLevel: this._zoomLevel,
82649
82970
  transformString,
82650
- svgWidth: this._svgDisplay.getAttribute('width'),
82651
- svgHeight: this._svgDisplay.getAttribute('height'),
82971
+ svgWidth: this._svgDisplay.getAttribute("width"),
82972
+ svgHeight: this._svgDisplay.getAttribute("height"),
82652
82973
  svgBBox: this._svgDisplay.getBBox(),
82653
82974
  svgBoundingClientRect: {
82654
82975
  x: svgBoundingRect.x,
@@ -82658,18 +82979,18 @@ class GveHintDesigner extends HTMLElement {
82658
82979
  top: svgBoundingRect.top,
82659
82980
  left: svgBoundingRect.left,
82660
82981
  right: svgBoundingRect.right,
82661
- bottom: svgBoundingRect.bottom
82662
- }
82982
+ bottom: svgBoundingRect.bottom,
82983
+ },
82663
82984
  });
82664
82985
  // Apply transform: translate first, then scale from the SVG's own center
82665
82986
  this._svgDisplay.style.transform = transformString;
82666
- this._svgDisplay.style.transformOrigin = '0 0';
82987
+ this._svgDisplay.style.transformOrigin = "0 0";
82667
82988
  // Only apply transition during zoom, not during pan (to avoid sluggishness)
82668
82989
  if (!this._isPanning) {
82669
- this._svgDisplay.style.transition = 'transform 0.15s ease-out';
82990
+ this._svgDisplay.style.transition = "transform 0.15s ease-out";
82670
82991
  }
82671
82992
  else {
82672
- this._svgDisplay.style.transition = 'none';
82993
+ this._svgDisplay.style.transition = "none";
82673
82994
  }
82674
82995
  }
82675
82996
  /**
@@ -82689,7 +83010,7 @@ class GveHintDesigner extends HTMLElement {
82689
83010
  // Get the bounding box of the hint content
82690
83011
  const bbox = hintContent.getBBox();
82691
83012
  // Get the SVG display panel size
82692
- const panel = this._shadow.querySelector('.svg-display-panel');
83013
+ const panel = this._shadow.querySelector(".svg-display-panel");
82693
83014
  if (!panel)
82694
83015
  return;
82695
83016
  const panelWidth = panel.clientWidth;
@@ -82710,8 +83031,8 @@ class GveHintDesigner extends HTMLElement {
82710
83031
  // We want this to be at the center of the panel: (panelWidth/2, panelHeight/2)
82711
83032
  // So: panX + (bboxCenterX * scale) = panelWidth / 2
82712
83033
  // panY + (bboxCenterY * scale) = panelHeight / 2
82713
- const panX = (panelWidth / 2) - (bboxCenterX * scale);
82714
- const panY = (panelHeight / 2) - (bboxCenterY * scale);
83034
+ const panX = panelWidth / 2 - bboxCenterX * scale;
83035
+ const panY = panelHeight / 2 - bboxCenterY * scale;
82715
83036
  this._zoomLevel = scale;
82716
83037
  this._panX = panX;
82717
83038
  this._panY = panY;
@@ -82724,7 +83045,7 @@ class GveHintDesigner extends HTMLElement {
82724
83045
  bboxCenterX,
82725
83046
  bboxCenterY,
82726
83047
  panelWidth,
82727
- panelHeight
83048
+ panelHeight,
82728
83049
  });
82729
83050
  }
82730
83051
  catch (err) {
@@ -82737,13 +83058,13 @@ class GveHintDesigner extends HTMLElement {
82737
83058
  */
82738
83059
  toggleSvgWordWrap() {
82739
83060
  this._svgWordWrapEnabled = !this._svgWordWrapEnabled;
82740
- const wrapper = this._shadow.getElementById('svg-wrapper');
83061
+ const wrapper = this._shadow.getElementById("svg-wrapper");
82741
83062
  if (wrapper) {
82742
83063
  if (this._svgWordWrapEnabled) {
82743
- wrapper.classList.add('word-wrap-enabled');
83064
+ wrapper.classList.add("word-wrap-enabled");
82744
83065
  }
82745
83066
  else {
82746
- wrapper.classList.remove('word-wrap-enabled');
83067
+ wrapper.classList.remove("word-wrap-enabled");
82747
83068
  }
82748
83069
  }
82749
83070
  this._logger.debug("Component", "SVG word wrap toggled", this._svgWordWrapEnabled);
@@ -82753,13 +83074,13 @@ class GveHintDesigner extends HTMLElement {
82753
83074
  */
82754
83075
  toggleJsWordWrap() {
82755
83076
  this._jsWordWrapEnabled = !this._jsWordWrapEnabled;
82756
- const wrapper = this._shadow.getElementById('js-wrapper');
83077
+ const wrapper = this._shadow.getElementById("js-wrapper");
82757
83078
  if (wrapper) {
82758
83079
  if (this._jsWordWrapEnabled) {
82759
- wrapper.classList.add('word-wrap-enabled');
83080
+ wrapper.classList.add("word-wrap-enabled");
82760
83081
  }
82761
83082
  else {
82762
- wrapper.classList.remove('word-wrap-enabled');
83083
+ wrapper.classList.remove("word-wrap-enabled");
82763
83084
  }
82764
83085
  }
82765
83086
  this._logger.debug("Component", "JS word wrap toggled", this._jsWordWrapEnabled);
@@ -82855,7 +83176,9 @@ class GveHintDesigner extends HTMLElement {
82855
83176
  }
82856
83177
  this._hintVariables.push({ name, value: "" });
82857
83178
  this.refreshVariablesTable();
82858
- this.fireEvent("hintVariablesChange", { hintVariables: this._hintVariables });
83179
+ this.fireEvent("hintVariablesChange", {
83180
+ hintVariables: this._hintVariables,
83181
+ });
82859
83182
  }
82860
83183
  /**
82861
83184
  * Delete a variable.
@@ -82863,7 +83186,9 @@ class GveHintDesigner extends HTMLElement {
82863
83186
  deleteVariable(index) {
82864
83187
  this._hintVariables.splice(index, 1);
82865
83188
  this.refreshVariablesTable();
82866
- this.fireEvent("hintVariablesChange", { hintVariables: this._hintVariables });
83189
+ this.fireEvent("hintVariablesChange", {
83190
+ hintVariables: this._hintVariables,
83191
+ });
82867
83192
  }
82868
83193
  /**
82869
83194
  * Edit a variable.
@@ -82874,7 +83199,9 @@ class GveHintDesigner extends HTMLElement {
82874
83199
  if (newValue !== null) {
82875
83200
  variable.value = newValue;
82876
83201
  this.refreshVariablesTable();
82877
- this.fireEvent("hintVariablesChange", { hintVariables: this._hintVariables });
83202
+ this.fireEvent("hintVariablesChange", {
83203
+ hintVariables: this._hintVariables,
83204
+ });
82878
83205
  }
82879
83206
  }
82880
83207
  /**
@@ -82886,7 +83213,61 @@ class GveHintDesigner extends HTMLElement {
82886
83213
  return;
82887
83214
  this._hintVariables = [];
82888
83215
  this.refreshVariablesTable();
82889
- this.fireEvent("hintVariablesChange", { hintVariables: this._hintVariables });
83216
+ this.fireEvent("hintVariablesChange", {
83217
+ hintVariables: this._hintVariables,
83218
+ });
83219
+ }
83220
+ /**
83221
+ * Extract all variable names from all hints in data.
83222
+ * Returns a sorted array of unique variable names.
83223
+ */
83224
+ extractAllVariablesFromHints() {
83225
+ const allVariables = new Set();
83226
+ // Iterate through all hints in data
83227
+ Object.values(this._data.hints).forEach((hint) => {
83228
+ if (hint.svg) {
83229
+ // Extract variables from this hint's SVG
83230
+ const variables = this.extractVariablesFromSvg(hint.svg);
83231
+ variables.forEach((varName) => allVariables.add(varName));
83232
+ }
83233
+ });
83234
+ // Convert to array and sort alphabetically
83235
+ return Array.from(allVariables).sort();
83236
+ }
83237
+ /**
83238
+ * Populate hint variables from all hints.
83239
+ * Extracts all variables from all hints' SVG code and merges them into hintVariables.
83240
+ * Only adds variables that don't already exist.
83241
+ */
83242
+ populateVariablesFromHints() {
83243
+ // Extract all variables from hints
83244
+ const extractedVariables = this.extractAllVariablesFromHints();
83245
+ if (extractedVariables.length === 0) {
83246
+ this.showMessage("No variables found in hints", "info");
83247
+ return;
83248
+ }
83249
+ // Get existing variable names for quick lookup
83250
+ const existingNames = new Set(this._hintVariables.map((v) => v.name));
83251
+ // Add new variables (those not already in hintVariables)
83252
+ let addedCount = 0;
83253
+ extractedVariables.forEach((varName) => {
83254
+ if (!existingNames.has(varName)) {
83255
+ this._hintVariables.push({ name: varName, value: "" });
83256
+ addedCount++;
83257
+ }
83258
+ });
83259
+ // Refresh UI and notify
83260
+ this.refreshVariablesTable();
83261
+ this.fireEvent("hintVariablesChange", {
83262
+ hintVariables: this._hintVariables,
83263
+ });
83264
+ // Show feedback message
83265
+ if (addedCount === 0) {
83266
+ this.showMessage(`All ${extractedVariables.length} variable(s) already defined`, "info");
83267
+ }
83268
+ else {
83269
+ this.showMessage(`Added ${addedCount} new variable(s) from hints (${extractedVariables.length} total found)`, "success");
83270
+ }
82890
83271
  }
82891
83272
  /**
82892
83273
  * Refresh the variables table.
@@ -82895,6 +83276,11 @@ class GveHintDesigner extends HTMLElement {
82895
83276
  if (!this._variablesTable)
82896
83277
  return;
82897
83278
  this._variablesTable.innerHTML = "";
83279
+ // Ensure _hintVariables is an array
83280
+ if (!Array.isArray(this._hintVariables)) {
83281
+ this._logger.warn("Component", "hintVariables is not an array, resetting to empty array", this._hintVariables);
83282
+ this._hintVariables = [];
83283
+ }
82898
83284
  if (this._hintVariables.length === 0) {
82899
83285
  const row = this._variablesTable.insertRow();
82900
83286
  const cell = row.insertCell();
@@ -82902,6 +83288,11 @@ class GveHintDesigner extends HTMLElement {
82902
83288
  cell.textContent = "No variables defined";
82903
83289
  cell.style.fontStyle = "italic";
82904
83290
  cell.style.color = "#999";
83291
+ // Re-validate SVG variables after clearing variables
83292
+ const svgCode = this._svgTextarea?.value || "";
83293
+ if (svgCode) {
83294
+ this.validateSvgVariables(svgCode);
83295
+ }
82905
83296
  return;
82906
83297
  }
82907
83298
  this._hintVariables.forEach((variable, index) => {
@@ -82922,6 +83313,11 @@ class GveHintDesigner extends HTMLElement {
82922
83313
  const valueCell = row.insertCell();
82923
83314
  valueCell.textContent = variable.value;
82924
83315
  });
83316
+ // Re-validate SVG variables after updating the variables table
83317
+ const svgCode = this._svgTextarea?.value || "";
83318
+ if (svgCode) {
83319
+ this.validateSvgVariables(svgCode);
83320
+ }
82925
83321
  }
82926
83322
  /**
82927
83323
  * Show a message in the message panel.
@@ -82942,16 +83338,18 @@ class GveHintDesigner extends HTMLElement {
82942
83338
  if (!this._svgTextarea || !this._svgHighlightContainer) {
82943
83339
  this._logger.debug("Component", "Cannot highlight SVG - missing elements", {
82944
83340
  hasTextarea: !!this._svgTextarea,
82945
- hasContainer: !!this._svgHighlightContainer
83341
+ hasContainer: !!this._svgHighlightContainer,
82946
83342
  });
82947
83343
  return;
82948
83344
  }
82949
83345
  const code = this._svgTextarea.value;
82950
- this._logger.debug("Component", "Highlighting SVG", { codeLength: code.length });
83346
+ this._logger.debug("Component", "Highlighting SVG", {
83347
+ codeLength: code.length,
83348
+ });
82951
83349
  try {
82952
83350
  // Highlight as XML
82953
- const highlighted = HighlightJS.highlight(code, { language: 'xml' }).value;
82954
- this._svgHighlightContainer.innerHTML = highlighted + '\n';
83351
+ const highlighted = HighlightJS.highlight(code, { language: "xml" }).value;
83352
+ this._svgHighlightContainer.innerHTML = highlighted + "\n";
82955
83353
  this._logger.debug("Component", "SVG highlighted successfully");
82956
83354
  }
82957
83355
  catch (err) {
@@ -82969,16 +83367,20 @@ class GveHintDesigner extends HTMLElement {
82969
83367
  if (!this._jsTextarea || !this._jsHighlightContainer) {
82970
83368
  this._logger.debug("Component", "Cannot highlight JS - missing elements", {
82971
83369
  hasTextarea: !!this._jsTextarea,
82972
- hasContainer: !!this._jsHighlightContainer
83370
+ hasContainer: !!this._jsHighlightContainer,
82973
83371
  });
82974
83372
  return;
82975
83373
  }
82976
83374
  const code = this._jsTextarea.value;
82977
- this._logger.debug("Component", "Highlighting JS", { codeLength: code.length });
83375
+ this._logger.debug("Component", "Highlighting JS", {
83376
+ codeLength: code.length,
83377
+ });
82978
83378
  try {
82979
83379
  // Highlight as JavaScript
82980
- const highlighted = HighlightJS.highlight(code, { language: 'javascript' }).value;
82981
- this._jsHighlightContainer.innerHTML = highlighted + '\n';
83380
+ const highlighted = HighlightJS.highlight(code, {
83381
+ language: "javascript",
83382
+ }).value;
83383
+ this._jsHighlightContainer.innerHTML = highlighted + "\n";
82982
83384
  this._logger.debug("Component", "JS highlighted successfully");
82983
83385
  }
82984
83386
  catch (err) {
@@ -83576,6 +83978,60 @@ class GveHintDesigner extends HTMLElement {
83576
83978
  background: #ffcdd2;
83577
83979
  }
83578
83980
 
83981
+ /* Validation Message */
83982
+ .validation-message {
83983
+ padding: 0.75rem;
83984
+ margin: 0.5rem;
83985
+ background: #fff3cd;
83986
+ border: 1px solid #ffc107;
83987
+ border-radius: 4px;
83988
+ color: #856404;
83989
+ font-size: 13px;
83990
+ line-height: 1.5;
83991
+ }
83992
+
83993
+ .validation-message strong {
83994
+ display: block;
83995
+ margin-bottom: 0.5rem;
83996
+ color: #ff9800;
83997
+ }
83998
+
83999
+ .validation-message code {
84000
+ background: #fff;
84001
+ padding: 0.2rem 0.4rem;
84002
+ border-radius: 3px;
84003
+ font-family: 'Consolas', 'Monaco', monospace;
84004
+ font-size: 12px;
84005
+ border: 1px solid #e0e0e0;
84006
+ }
84007
+
84008
+ .validation-message ul {
84009
+ margin: 0.5rem 0;
84010
+ }
84011
+
84012
+ .validation-message li {
84013
+ margin: 0.25rem 0;
84014
+ }
84015
+
84016
+ .validation-message em {
84017
+ display: block;
84018
+ margin-top: 0.5rem;
84019
+ font-size: 12px;
84020
+ color: #666;
84021
+ }
84022
+
84023
+ /* Warning Icon in Panel Header */
84024
+ .warning-icon {
84025
+ display: inline-flex;
84026
+ align-items: center;
84027
+ margin-left: 0.5rem;
84028
+ color: #ff9800;
84029
+ }
84030
+
84031
+ .warning-icon .feather-icon {
84032
+ display: block;
84033
+ }
84034
+
83579
84035
  /* Responsive layout - use nested grids for each column */
83580
84036
  @media (min-width: 900px) {
83581
84037
  .properties-panel .panel-content {