easter-egg-quest 1.0.11 โ†’ 1.0.13

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.
@@ -141,19 +141,21 @@ function resolveConfig(raw = {}) {
141
141
  theme,
142
142
  hiddenEntry: {
143
143
  mode: ((_b2 = raw.hiddenEntry) == null ? void 0 : _b2.mode) ?? "auto",
144
- selector: (_c = raw.hiddenEntry) == null ? void 0 : _c.selector,
144
+ selector: sanitizeSelector((_c = raw.hiddenEntry) == null ? void 0 : _c.selector),
145
145
  excludeSelectors: [
146
146
  ...DEFAULT_EXCLUDE_SELECTORS,
147
- ...((_d = raw.hiddenEntry) == null ? void 0 : _d.excludeSelectors) ?? []
147
+ ...(((_d = raw.hiddenEntry) == null ? void 0 : _d.excludeSelectors) ?? []).filter(
148
+ (s) => typeof s === "string" && s.length < 200
149
+ )
148
150
  ]
149
151
  },
150
152
  hud: typeof raw.hud === "boolean" ? { enabled: raw.hud, position: "top-left" } : { enabled: ((_e = raw.hud) == null ? void 0 : _e.enabled) ?? true, position: ((_f = raw.hud) == null ? void 0 : _f.position) ?? "top-left" },
151
153
  shrine: typeof raw.shrine === "boolean" ? { enabled: raw.shrine, position: "bottom-right" } : { enabled: ((_g = raw.shrine) == null ? void 0 : _g.enabled) ?? true, position: ((_h = raw.shrine) == null ? void 0 : _h.position) ?? "bottom-right" },
152
154
  sounds: raw.sounds ?? false,
153
155
  stageDurations: {
154
- stillnessMs: ((_i = raw.stageDurations) == null ? void 0 : _i.stillnessMs) ?? 2e4,
155
- motionMs: ((_j = raw.stageDurations) == null ? void 0 : _j.motionMs) ?? 2e4,
156
- rhythmCycles: ((_k = raw.stageDurations) == null ? void 0 : _k.rhythmCycles) ?? 6
156
+ stillnessMs: clampDuration((_i = raw.stageDurations) == null ? void 0 : _i.stillnessMs, 2e4),
157
+ motionMs: clampDuration((_j = raw.stageDurations) == null ? void 0 : _j.motionMs, 2e4),
158
+ rhythmCycles: clampInt((_k = raw.stageDurations) == null ? void 0 : _k.rhythmCycles, 6, 1, 50)
157
159
  },
158
160
  renderer: raw.renderer ?? "auto",
159
161
  scoring: {
@@ -169,11 +171,48 @@ function resolveConfig(raw = {}) {
169
171
  callbacks: raw.callbacks ?? {}
170
172
  };
171
173
  }
174
+ function clampDuration(val, def) {
175
+ if (val === void 0) return def;
176
+ const n = Number(val);
177
+ if (!Number.isFinite(n) || n < 1e3 || n > 3e5) return def;
178
+ return n;
179
+ }
180
+ function clampInt(val, def, min, max) {
181
+ if (val === void 0) return def;
182
+ const n = Math.round(Number(val));
183
+ if (!Number.isFinite(n) || n < min || n > max) return def;
184
+ return n;
185
+ }
186
+ function sanitizeSelector(sel) {
187
+ if (!sel || typeof sel !== "string") return void 0;
188
+ if (sel.length > 200) return void 0;
189
+ try {
190
+ document.querySelector(sel);
191
+ } catch {
192
+ return void 0;
193
+ }
194
+ return sel;
195
+ }
172
196
  function resolveTheme(input) {
173
197
  if (!input || input === "auto" || input === "light" || input === "dark") {
174
198
  return { ...THEME_DEFAULTS };
175
199
  }
176
- return { ...THEME_DEFAULTS, ...input };
200
+ const allowed = [
201
+ "overlayBg",
202
+ "textColor",
203
+ "textSecondary",
204
+ "accent",
205
+ "accentGlow",
206
+ "hudBg",
207
+ "hudText"
208
+ ];
209
+ const filtered = {};
210
+ for (const key of allowed) {
211
+ if (key in input && typeof input[key] === "string") {
212
+ filtered[key] = input[key];
213
+ }
214
+ }
215
+ return { ...THEME_DEFAULTS, ...filtered };
177
216
  }
178
217
  function resolveNarrative(overrides) {
179
218
  if (!overrides) return { ...DEFAULT_SCRIPT };
@@ -524,6 +563,10 @@ class HiddenEntry {
524
563
  this.fallbackButton = null;
525
564
  this._injectedElement = null;
526
565
  this._destroyed = false;
566
+ this._hintVisibleElapsed = 0;
567
+ this._hintVisibleStart = 0;
568
+ this._hintCheckInterval = null;
569
+ this._hintVisibilityHandler = null;
527
570
  this.config = config;
528
571
  this.script = script;
529
572
  this.onFound = onFound;
@@ -547,6 +590,7 @@ class HiddenEntry {
547
590
  cleanup() {
548
591
  var _a2, _b2, _c, _d;
549
592
  this._destroyed = true;
593
+ this._stopHintTimer();
550
594
  for (const t of this.hintTimers) clearTimeout(t);
551
595
  this.hintTimers = [];
552
596
  if (this.clickHandler && this.targetElement) {
@@ -728,45 +772,126 @@ class HiddenEntry {
728
772
  this._injectShimmerStyles();
729
773
  this._createHintContainer();
730
774
  const hints = this.script.hiddenEntryHints;
731
- const FIRST_HINT_DELAY = 12e4;
775
+ const FIRST_HINT_DELAY = 6e4;
732
776
  const HINT_INTERVAL = 6e4;
733
- for (let i = 0; i < hints.length; i++) {
734
- const delay = FIRST_HINT_DELAY + i * HINT_INTERVAL;
735
- const timer = setTimeout(() => {
736
- if (this._destroyed) return;
737
- this._showHintWhenVisible(hints[i]);
738
- }, delay);
739
- this.hintTimers.push(timer);
740
- }
741
- const shimmerDelay = 24e4;
742
- const shimmerTimer = setTimeout(() => {
743
- if (this._destroyed || !this.targetElement) return;
744
- this.targetElement.classList.add("eeq-entry-target");
745
- }, shimmerDelay);
746
- this.hintTimers.push(shimmerTimer);
747
- }
748
- /** Show hint only when the page is visible. If hidden, wait for visibility change. */
749
- _showHintWhenVisible(text) {
750
- if (this._destroyed) return;
751
- if (document.visibilityState === "visible" && document.hasFocus()) {
752
- this._showHint(text);
753
- } else {
754
- const handler = () => {
755
- if (this._destroyed) {
756
- document.removeEventListener("visibilitychange", handler);
757
- window.removeEventListener("focus", handler);
758
- return;
777
+ const SHIMMER_DELAY = 12e4;
778
+ const showFirstHint = () => {
779
+ this._stopHintTimer();
780
+ this._showFirstHintWithConfirm(hints[0] ?? "", () => {
781
+ const remaining = hints.slice(1);
782
+ const thresholds = [];
783
+ for (let i = 0; i < remaining.length; i++) {
784
+ thresholds.push({
785
+ at: (i + 1) * HINT_INTERVAL,
786
+ fired: false,
787
+ action: () => this._showHint(remaining[i])
788
+ });
759
789
  }
760
- if (document.visibilityState === "visible" && document.hasFocus()) {
761
- document.removeEventListener("visibilitychange", handler);
762
- window.removeEventListener("focus", handler);
763
- setTimeout(() => {
764
- if (!this._destroyed) this._showHint(text);
765
- }, 800);
790
+ thresholds.push({
791
+ at: SHIMMER_DELAY,
792
+ fired: false,
793
+ action: () => {
794
+ if (this.targetElement) {
795
+ this.targetElement.classList.add("eeq-entry-target");
796
+ }
797
+ }
798
+ });
799
+ if (thresholds.length === 0) return;
800
+ this._startVisibilityTimer(thresholds);
801
+ });
802
+ };
803
+ const phase1 = [
804
+ { at: FIRST_HINT_DELAY, fired: false, action: showFirstHint }
805
+ ];
806
+ this._startVisibilityTimer(phase1);
807
+ }
808
+ _startVisibilityTimer(thresholds) {
809
+ this._hintVisibleElapsed = 0;
810
+ this._hintVisibleStart = 0;
811
+ const getVisibleElapsed = () => this._hintVisibleElapsed + (this._hintVisibleStart ? Date.now() - this._hintVisibleStart : 0);
812
+ const checkThresholds = () => {
813
+ if (this._destroyed) return;
814
+ const elapsed = getVisibleElapsed();
815
+ for (const t of thresholds) {
816
+ if (!t.fired && elapsed >= t.at) {
817
+ t.fired = true;
818
+ t.action();
766
819
  }
767
- };
768
- document.addEventListener("visibilitychange", handler);
769
- window.addEventListener("focus", handler);
820
+ }
821
+ if (thresholds.every((t) => t.fired)) this._stopHintTimer();
822
+ };
823
+ const startTimer = () => {
824
+ if (this._hintCheckInterval) return;
825
+ this._hintVisibleStart = Date.now();
826
+ this._hintCheckInterval = setInterval(checkThresholds, 1e3);
827
+ };
828
+ const pauseTimer = () => {
829
+ if (this._hintVisibleStart) {
830
+ this._hintVisibleElapsed += Date.now() - this._hintVisibleStart;
831
+ this._hintVisibleStart = 0;
832
+ }
833
+ if (this._hintCheckInterval) {
834
+ clearInterval(this._hintCheckInterval);
835
+ this._hintCheckInterval = null;
836
+ }
837
+ };
838
+ this._hintVisibilityHandler = () => {
839
+ if (this._destroyed) return;
840
+ if (document.visibilityState === "visible") startTimer();
841
+ else pauseTimer();
842
+ };
843
+ document.addEventListener("visibilitychange", this._hintVisibilityHandler);
844
+ if (document.visibilityState === "visible") startTimer();
845
+ }
846
+ _showFirstHintWithConfirm(text, onConfirm) {
847
+ if (!this.hintContainer) return;
848
+ const snack = document.createElement("div");
849
+ snack.className = "eeq-snackbar";
850
+ const msg = document.createElement("span");
851
+ msg.textContent = text;
852
+ snack.appendChild(msg);
853
+ const goBtn = document.createElement("button");
854
+ goBtn.className = "eeq-snackbar-btn";
855
+ goBtn.textContent = "Let's go!";
856
+ Object.assign(goBtn.style, {
857
+ background: "rgba(76,175,80,0.85)",
858
+ color: "#fff"
859
+ });
860
+ goBtn.addEventListener("click", () => {
861
+ dismiss();
862
+ onConfirm();
863
+ });
864
+ snack.appendChild(goBtn);
865
+ const noBtn = document.createElement("button");
866
+ noBtn.className = "eeq-snackbar-btn";
867
+ noBtn.textContent = "Not interested";
868
+ noBtn.addEventListener("click", () => {
869
+ try {
870
+ localStorage.setItem("eeq_optout", "1");
871
+ } catch {
872
+ }
873
+ this.cleanup();
874
+ });
875
+ snack.appendChild(noBtn);
876
+ this.hintContainer.appendChild(snack);
877
+ const dismiss = () => {
878
+ snack.style.transform = "translateY(20px)";
879
+ snack.style.opacity = "0";
880
+ setTimeout(() => snack.remove(), 400);
881
+ };
882
+ requestAnimationFrame(() => {
883
+ snack.style.transform = "translateY(0)";
884
+ snack.style.opacity = "1";
885
+ });
886
+ }
887
+ _stopHintTimer() {
888
+ if (this._hintCheckInterval) {
889
+ clearInterval(this._hintCheckInterval);
890
+ this._hintCheckInterval = null;
891
+ }
892
+ if (this._hintVisibilityHandler) {
893
+ document.removeEventListener("visibilitychange", this._hintVisibilityHandler);
894
+ this._hintVisibilityHandler = null;
770
895
  }
771
896
  }
772
897
  _showHint(text) {
@@ -1418,7 +1543,7 @@ class ThreeRenderer {
1418
1543
  const egg2 = this.eggs[bestIdx];
1419
1544
  this._dragRotX = egg2.rotation.x;
1420
1545
  this._dragRotY = egg2.rotation.y;
1421
- document.body.style.cursor = "grabbing";
1546
+ if (this.canvas) this.canvas.style.cursor = "grabbing";
1422
1547
  return;
1423
1548
  }
1424
1549
  if (this._interactiveEgg === null) return;
@@ -1431,7 +1556,7 @@ class ThreeRenderer {
1431
1556
  if (dist > 120) return;
1432
1557
  e.preventDefault();
1433
1558
  e.stopPropagation();
1434
- if (this.canvas) document.body.style.cursor = "grabbing";
1559
+ if (this.canvas) this.canvas.style.cursor = "grabbing";
1435
1560
  this._isDragging = true;
1436
1561
  this._dragMoved = false;
1437
1562
  this._dragStartX = e.clientX;
@@ -1481,7 +1606,7 @@ class ThreeRenderer {
1481
1606
  this._finaleDragTarget = null;
1482
1607
  this._cancelLongPress();
1483
1608
  if (this._interactiveEgg !== null || this._finaleInteractive) {
1484
- document.body.style.cursor = "grab";
1609
+ if (this.canvas) this.canvas.style.cursor = "grab";
1485
1610
  }
1486
1611
  };
1487
1612
  this._onWheel = (e) => {
@@ -1623,7 +1748,7 @@ class ThreeRenderer {
1623
1748
  /** Make an egg interactively draggable. */
1624
1749
  enableInteraction(index) {
1625
1750
  this._interactiveEgg = index;
1626
- document.body.style.cursor = "grab";
1751
+ if (this.canvas) this.canvas.style.cursor = "grab";
1627
1752
  }
1628
1753
  /** Disable drag interaction. */
1629
1754
  disableInteraction() {
@@ -1632,7 +1757,7 @@ class ThreeRenderer {
1632
1757
  this._finaleDragTarget = null;
1633
1758
  this._finaleInteractive = false;
1634
1759
  this._cancelLongPress();
1635
- document.body.style.cursor = "";
1760
+ if (this.canvas) this.canvas.style.cursor = "";
1636
1761
  }
1637
1762
  /** Trigger spectacular egg reveal. */
1638
1763
  revealEgg(index) {
@@ -1776,7 +1901,7 @@ class ThreeRenderer {
1776
1901
  this._finaleInteractive = true;
1777
1902
  this._finaleDragTarget = null;
1778
1903
  this._finalePausedRotation = [false, false, false];
1779
- document.body.style.cursor = "grab";
1904
+ if (this.canvas) this.canvas.style.cursor = "grab";
1780
1905
  for (let i = 0; i < 3; i++) {
1781
1906
  const state = this._eggStates[i];
1782
1907
  state.phase = "finale";
@@ -1800,14 +1925,27 @@ class ThreeRenderer {
1800
1925
  fontFamily: "'Georgia', 'Times New Roman', serif",
1801
1926
  textAlign: "center"
1802
1927
  });
1803
- this._greetingOverlay.innerHTML = `
1804
- <div style="font-size: 48px; color: #fff8e8; text-shadow: 0 0 60px rgba(212,165,80,0.7), 0 0 20px rgba(212,165,80,0.4), 0 2px 8px rgba(0,0,0,0.6); margin-bottom: 10px; letter-spacing: 0.03em; font-weight: bold;">
1805
- Happy Easter! ๐Ÿฐ๐Ÿฅš
1806
- </div>
1807
- <div style="font-size: 17px; color: rgba(245,240,230,0.85); font-style: italic; text-shadow: 0 0 12px rgba(212,165,80,0.3), 0 1px 6px rgba(0,0,0,0.4);">
1808
- Wishing you joy, peace, and light
1809
- </div>
1810
- `;
1928
+ this._greetingOverlay.innerHTML = "";
1929
+ const headingDiv = document.createElement("div");
1930
+ Object.assign(headingDiv.style, {
1931
+ fontSize: "48px",
1932
+ color: "#fff8e8",
1933
+ textShadow: "0 0 60px rgba(212,165,80,0.7), 0 0 20px rgba(212,165,80,0.4), 0 2px 8px rgba(0,0,0,0.6)",
1934
+ marginBottom: "10px",
1935
+ letterSpacing: "0.03em",
1936
+ fontWeight: "bold"
1937
+ });
1938
+ headingDiv.textContent = "Happy Easter! ๐Ÿฐ๐Ÿฅš";
1939
+ this._greetingOverlay.appendChild(headingDiv);
1940
+ const subDiv = document.createElement("div");
1941
+ Object.assign(subDiv.style, {
1942
+ fontSize: "17px",
1943
+ color: "rgba(245,240,230,0.85)",
1944
+ fontStyle: "italic",
1945
+ textShadow: "0 0 12px rgba(212,165,80,0.3), 0 1px 6px rgba(0,0,0,0.4)"
1946
+ });
1947
+ subDiv.textContent = "Wishing you joy, peace, and light";
1948
+ this._greetingOverlay.appendChild(subDiv);
1811
1949
  document.body.appendChild(this._greetingOverlay);
1812
1950
  requestAnimationFrame(() => {
1813
1951
  requestAnimationFrame(() => {
@@ -3244,7 +3382,7 @@ const _ResultsRenderer = class _ResultsRenderer {
3244
3382
  this.shadow.appendChild(style);
3245
3383
  const panel = document.createElement("div");
3246
3384
  panel.className = "eeq-results";
3247
- panel.innerHTML = this._buildContent(score);
3385
+ this._buildContentDOM(panel, score);
3248
3386
  this.shadow.appendChild(panel);
3249
3387
  requestAnimationFrame(() => {
3250
3388
  requestAnimationFrame(() => {
@@ -3264,7 +3402,7 @@ const _ResultsRenderer = class _ResultsRenderer {
3264
3402
  this.shadow = null;
3265
3403
  }
3266
3404
  // โ”€โ”€โ”€ Content โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3267
- _buildContent(s) {
3405
+ _buildContentDOM(parent, s) {
3268
3406
  const bp = s.behaviorProfile;
3269
3407
  const actionIcons = {
3270
3408
  clicker: "๐Ÿ–ฑ",
@@ -3285,31 +3423,56 @@ const _ResultsRenderer = class _ResultsRenderer {
3285
3423
  explorer: "๐Ÿงญ",
3286
3424
  philosopher: "๐Ÿ•Š"
3287
3425
  };
3288
- const badges = [];
3289
- const ai = actionIcons[bp.dominantAction] ?? "";
3290
- const mi = mouseIcons[bp.mousePersonality] ?? "";
3291
- const pi = paceIcons[bp.pace] ?? "";
3292
- if (ai) badges.push(`<span class="eeq-badge" title="${bp.dominantAction}">${ai}</span>`);
3293
- if (mi) badges.push(`<span class="eeq-badge" title="${bp.mousePersonality}">${mi}</span>`);
3294
- if (pi) badges.push(`<span class="eeq-badge" title="${bp.pace}">${pi}</span>`);
3295
- return `
3296
- <div class="eeq-results-inner">
3297
- <div class="eeq-results-title">ยซ ${this._escapeHtml(bp.title)} ยป</div>
3298
- <div class="eeq-time">${formatTime(s.totalTime)}</div>
3299
- <div class="eeq-badges">${badges.join("")}</div>
3300
- <div class="eeq-share-invite">share your result with friends ๐Ÿฅš</div>
3301
- <div class="eeq-results-actions">
3302
- <button class="eeq-btn eeq-btn-share" title="share result">
3303
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4.5 10.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm7-4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" stroke="currentColor" stroke-width="1.2"/><path d="M6.3 9.2l3.4 1.6M6.3 7.8l3.4-1.6" stroke="currentColor" stroke-width="1.2"/></svg>
3304
- <span>share</span>
3305
- </button>
3306
- <button class="eeq-btn eeq-btn-finish" title="finish">
3307
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 8.5l3 3 5-6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
3308
- <span>finish</span>
3309
- </button>
3310
- </div>
3311
- </div>
3312
- `;
3426
+ const inner = document.createElement("div");
3427
+ inner.className = "eeq-results-inner";
3428
+ const title = document.createElement("div");
3429
+ title.className = "eeq-results-title";
3430
+ title.textContent = `ยซ ${bp.title} ยป`;
3431
+ inner.appendChild(title);
3432
+ const time = document.createElement("div");
3433
+ time.className = "eeq-time";
3434
+ time.textContent = formatTime(s.totalTime);
3435
+ inner.appendChild(time);
3436
+ const badgesDiv = document.createElement("div");
3437
+ badgesDiv.className = "eeq-badges";
3438
+ const icons = [
3439
+ { icon: actionIcons[bp.dominantAction], label: bp.dominantAction },
3440
+ { icon: mouseIcons[bp.mousePersonality], label: bp.mousePersonality },
3441
+ { icon: paceIcons[bp.pace], label: bp.pace }
3442
+ ];
3443
+ for (const { icon, label } of icons) {
3444
+ if (!icon) continue;
3445
+ const span = document.createElement("span");
3446
+ span.className = "eeq-badge";
3447
+ span.title = label;
3448
+ span.textContent = icon;
3449
+ badgesDiv.appendChild(span);
3450
+ }
3451
+ inner.appendChild(badgesDiv);
3452
+ const invite = document.createElement("div");
3453
+ invite.className = "eeq-share-invite";
3454
+ invite.textContent = "share your result with friends ๐Ÿฅš";
3455
+ inner.appendChild(invite);
3456
+ const actions = document.createElement("div");
3457
+ actions.className = "eeq-results-actions";
3458
+ const shareBtn = document.createElement("button");
3459
+ shareBtn.className = "eeq-btn eeq-btn-share";
3460
+ shareBtn.title = "share result";
3461
+ shareBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4.5 10.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm7-4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" stroke="currentColor" stroke-width="1.2"/><path d="M6.3 9.2l3.4 1.6M6.3 7.8l3.4-1.6" stroke="currentColor" stroke-width="1.2"/></svg>';
3462
+ const shareLabel = document.createElement("span");
3463
+ shareLabel.textContent = "share";
3464
+ shareBtn.appendChild(shareLabel);
3465
+ actions.appendChild(shareBtn);
3466
+ const finishBtn = document.createElement("button");
3467
+ finishBtn.className = "eeq-btn eeq-btn-finish";
3468
+ finishBtn.title = "finish";
3469
+ finishBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 8.5l3 3 5-6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
3470
+ const finishLabel = document.createElement("span");
3471
+ finishLabel.textContent = "finish";
3472
+ finishBtn.appendChild(finishLabel);
3473
+ actions.appendChild(finishBtn);
3474
+ inner.appendChild(actions);
3475
+ parent.appendChild(inner);
3313
3476
  }
3314
3477
  // โ”€โ”€โ”€ 2D Egg Drawing (type-accurate) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3315
3478
  _makeRand(seed) {
@@ -3674,9 +3837,6 @@ const _ResultsRenderer = class _ResultsRenderer {
3674
3837
  span.textContent = orig;
3675
3838
  }, 1500);
3676
3839
  }
3677
- _escapeHtml(s) {
3678
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3679
- }
3680
3840
  // โ”€โ”€โ”€ Styles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3681
3841
  _getStyles() {
3682
3842
  const t = this.config.theme;
@@ -4063,7 +4223,11 @@ class Persistence {
4063
4223
  try {
4064
4224
  const raw = localStorage.getItem(STORAGE_KEY);
4065
4225
  if (!raw) return [];
4066
- return JSON.parse(raw);
4226
+ const parsed = JSON.parse(raw);
4227
+ if (!Array.isArray(parsed)) return [];
4228
+ return parsed.filter(
4229
+ (item) => typeof item === "object" && item !== null && typeof item.totalTime === "number" && Number.isFinite(item.totalTime)
4230
+ );
4067
4231
  } catch {
4068
4232
  return [];
4069
4233
  }