easter-egg-quest 1.0.12 โ†’ 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 };
@@ -733,26 +772,42 @@ class HiddenEntry {
733
772
  this._injectShimmerStyles();
734
773
  this._createHintContainer();
735
774
  const hints = this.script.hiddenEntryHints;
736
- const FIRST_HINT_DELAY = 12e4;
775
+ const FIRST_HINT_DELAY = 6e4;
737
776
  const HINT_INTERVAL = 6e4;
738
- const SHIMMER_DELAY = 24e4;
739
- const thresholds = [];
740
- for (let i = 0; i < hints.length; i++) {
741
- thresholds.push({
742
- at: FIRST_HINT_DELAY + i * HINT_INTERVAL,
743
- fired: false,
744
- action: () => this._showHint(hints[i])
745
- });
746
- }
747
- thresholds.push({
748
- at: SHIMMER_DELAY,
749
- fired: false,
750
- action: () => {
751
- if (this.targetElement) {
752
- this.targetElement.classList.add("eeq-entry-target");
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
+ });
753
789
  }
754
- }
755
- });
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;
756
811
  const getVisibleElapsed = () => this._hintVisibleElapsed + (this._hintVisibleStart ? Date.now() - this._hintVisibleStart : 0);
757
812
  const checkThresholds = () => {
758
813
  if (this._destroyed) return;
@@ -788,6 +843,47 @@ class HiddenEntry {
788
843
  document.addEventListener("visibilitychange", this._hintVisibilityHandler);
789
844
  if (document.visibilityState === "visible") startTimer();
790
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
+ }
791
887
  _stopHintTimer() {
792
888
  if (this._hintCheckInterval) {
793
889
  clearInterval(this._hintCheckInterval);
@@ -1447,7 +1543,7 @@ class ThreeRenderer {
1447
1543
  const egg2 = this.eggs[bestIdx];
1448
1544
  this._dragRotX = egg2.rotation.x;
1449
1545
  this._dragRotY = egg2.rotation.y;
1450
- document.body.style.cursor = "grabbing";
1546
+ if (this.canvas) this.canvas.style.cursor = "grabbing";
1451
1547
  return;
1452
1548
  }
1453
1549
  if (this._interactiveEgg === null) return;
@@ -1460,7 +1556,7 @@ class ThreeRenderer {
1460
1556
  if (dist > 120) return;
1461
1557
  e.preventDefault();
1462
1558
  e.stopPropagation();
1463
- if (this.canvas) document.body.style.cursor = "grabbing";
1559
+ if (this.canvas) this.canvas.style.cursor = "grabbing";
1464
1560
  this._isDragging = true;
1465
1561
  this._dragMoved = false;
1466
1562
  this._dragStartX = e.clientX;
@@ -1510,7 +1606,7 @@ class ThreeRenderer {
1510
1606
  this._finaleDragTarget = null;
1511
1607
  this._cancelLongPress();
1512
1608
  if (this._interactiveEgg !== null || this._finaleInteractive) {
1513
- document.body.style.cursor = "grab";
1609
+ if (this.canvas) this.canvas.style.cursor = "grab";
1514
1610
  }
1515
1611
  };
1516
1612
  this._onWheel = (e) => {
@@ -1652,7 +1748,7 @@ class ThreeRenderer {
1652
1748
  /** Make an egg interactively draggable. */
1653
1749
  enableInteraction(index) {
1654
1750
  this._interactiveEgg = index;
1655
- document.body.style.cursor = "grab";
1751
+ if (this.canvas) this.canvas.style.cursor = "grab";
1656
1752
  }
1657
1753
  /** Disable drag interaction. */
1658
1754
  disableInteraction() {
@@ -1661,7 +1757,7 @@ class ThreeRenderer {
1661
1757
  this._finaleDragTarget = null;
1662
1758
  this._finaleInteractive = false;
1663
1759
  this._cancelLongPress();
1664
- document.body.style.cursor = "";
1760
+ if (this.canvas) this.canvas.style.cursor = "";
1665
1761
  }
1666
1762
  /** Trigger spectacular egg reveal. */
1667
1763
  revealEgg(index) {
@@ -1805,7 +1901,7 @@ class ThreeRenderer {
1805
1901
  this._finaleInteractive = true;
1806
1902
  this._finaleDragTarget = null;
1807
1903
  this._finalePausedRotation = [false, false, false];
1808
- document.body.style.cursor = "grab";
1904
+ if (this.canvas) this.canvas.style.cursor = "grab";
1809
1905
  for (let i = 0; i < 3; i++) {
1810
1906
  const state = this._eggStates[i];
1811
1907
  state.phase = "finale";
@@ -1829,14 +1925,27 @@ class ThreeRenderer {
1829
1925
  fontFamily: "'Georgia', 'Times New Roman', serif",
1830
1926
  textAlign: "center"
1831
1927
  });
1832
- this._greetingOverlay.innerHTML = `
1833
- <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;">
1834
- Happy Easter! ๐Ÿฐ๐Ÿฅš
1835
- </div>
1836
- <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);">
1837
- Wishing you joy, peace, and light
1838
- </div>
1839
- `;
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);
1840
1949
  document.body.appendChild(this._greetingOverlay);
1841
1950
  requestAnimationFrame(() => {
1842
1951
  requestAnimationFrame(() => {
@@ -3273,7 +3382,7 @@ const _ResultsRenderer = class _ResultsRenderer {
3273
3382
  this.shadow.appendChild(style);
3274
3383
  const panel = document.createElement("div");
3275
3384
  panel.className = "eeq-results";
3276
- panel.innerHTML = this._buildContent(score);
3385
+ this._buildContentDOM(panel, score);
3277
3386
  this.shadow.appendChild(panel);
3278
3387
  requestAnimationFrame(() => {
3279
3388
  requestAnimationFrame(() => {
@@ -3293,7 +3402,7 @@ const _ResultsRenderer = class _ResultsRenderer {
3293
3402
  this.shadow = null;
3294
3403
  }
3295
3404
  // โ”€โ”€โ”€ Content โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3296
- _buildContent(s) {
3405
+ _buildContentDOM(parent, s) {
3297
3406
  const bp = s.behaviorProfile;
3298
3407
  const actionIcons = {
3299
3408
  clicker: "๐Ÿ–ฑ",
@@ -3314,31 +3423,56 @@ const _ResultsRenderer = class _ResultsRenderer {
3314
3423
  explorer: "๐Ÿงญ",
3315
3424
  philosopher: "๐Ÿ•Š"
3316
3425
  };
3317
- const badges = [];
3318
- const ai = actionIcons[bp.dominantAction] ?? "";
3319
- const mi = mouseIcons[bp.mousePersonality] ?? "";
3320
- const pi = paceIcons[bp.pace] ?? "";
3321
- if (ai) badges.push(`<span class="eeq-badge" title="${bp.dominantAction}">${ai}</span>`);
3322
- if (mi) badges.push(`<span class="eeq-badge" title="${bp.mousePersonality}">${mi}</span>`);
3323
- if (pi) badges.push(`<span class="eeq-badge" title="${bp.pace}">${pi}</span>`);
3324
- return `
3325
- <div class="eeq-results-inner">
3326
- <div class="eeq-results-title">ยซ ${this._escapeHtml(bp.title)} ยป</div>
3327
- <div class="eeq-time">${formatTime(s.totalTime)}</div>
3328
- <div class="eeq-badges">${badges.join("")}</div>
3329
- <div class="eeq-share-invite">share your result with friends ๐Ÿฅš</div>
3330
- <div class="eeq-results-actions">
3331
- <button class="eeq-btn eeq-btn-share" title="share result">
3332
- <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>
3333
- <span>share</span>
3334
- </button>
3335
- <button class="eeq-btn eeq-btn-finish" title="finish">
3336
- <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>
3337
- <span>finish</span>
3338
- </button>
3339
- </div>
3340
- </div>
3341
- `;
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);
3342
3476
  }
3343
3477
  // โ”€โ”€โ”€ 2D Egg Drawing (type-accurate) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3344
3478
  _makeRand(seed) {
@@ -3703,9 +3837,6 @@ const _ResultsRenderer = class _ResultsRenderer {
3703
3837
  span.textContent = orig;
3704
3838
  }, 1500);
3705
3839
  }
3706
- _escapeHtml(s) {
3707
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3708
- }
3709
3840
  // โ”€โ”€โ”€ Styles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3710
3841
  _getStyles() {
3711
3842
  const t = this.config.theme;
@@ -4092,7 +4223,11 @@ class Persistence {
4092
4223
  try {
4093
4224
  const raw = localStorage.getItem(STORAGE_KEY);
4094
4225
  if (!raw) return [];
4095
- 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
+ );
4096
4231
  } catch {
4097
4232
  return [];
4098
4233
  }