easter-egg-quest 1.0.12 โ†’ 1.0.14

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,43 @@ 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
+ goBtn.addEventListener("click", () => {
857
+ dismiss();
858
+ onConfirm();
859
+ });
860
+ snack.appendChild(goBtn);
861
+ const noBtn = document.createElement("button");
862
+ noBtn.className = "eeq-snackbar-btn";
863
+ noBtn.textContent = "Not interested";
864
+ noBtn.addEventListener("click", () => {
865
+ try {
866
+ localStorage.setItem("eeq_optout", "1");
867
+ } catch {
868
+ }
869
+ this.cleanup();
870
+ });
871
+ snack.appendChild(noBtn);
872
+ this.hintContainer.appendChild(snack);
873
+ const dismiss = () => {
874
+ snack.style.transform = "translateY(20px)";
875
+ snack.style.opacity = "0";
876
+ setTimeout(() => snack.remove(), 400);
877
+ };
878
+ requestAnimationFrame(() => {
879
+ snack.style.transform = "translateY(0)";
880
+ snack.style.opacity = "1";
881
+ });
882
+ }
791
883
  _stopHintTimer() {
792
884
  if (this._hintCheckInterval) {
793
885
  clearInterval(this._hintCheckInterval);
@@ -1447,7 +1539,7 @@ class ThreeRenderer {
1447
1539
  const egg2 = this.eggs[bestIdx];
1448
1540
  this._dragRotX = egg2.rotation.x;
1449
1541
  this._dragRotY = egg2.rotation.y;
1450
- document.body.style.cursor = "grabbing";
1542
+ if (this.canvas) this.canvas.style.cursor = "grabbing";
1451
1543
  return;
1452
1544
  }
1453
1545
  if (this._interactiveEgg === null) return;
@@ -1460,7 +1552,7 @@ class ThreeRenderer {
1460
1552
  if (dist > 120) return;
1461
1553
  e.preventDefault();
1462
1554
  e.stopPropagation();
1463
- if (this.canvas) document.body.style.cursor = "grabbing";
1555
+ if (this.canvas) this.canvas.style.cursor = "grabbing";
1464
1556
  this._isDragging = true;
1465
1557
  this._dragMoved = false;
1466
1558
  this._dragStartX = e.clientX;
@@ -1510,7 +1602,7 @@ class ThreeRenderer {
1510
1602
  this._finaleDragTarget = null;
1511
1603
  this._cancelLongPress();
1512
1604
  if (this._interactiveEgg !== null || this._finaleInteractive) {
1513
- document.body.style.cursor = "grab";
1605
+ if (this.canvas) this.canvas.style.cursor = "grab";
1514
1606
  }
1515
1607
  };
1516
1608
  this._onWheel = (e) => {
@@ -1652,7 +1744,7 @@ class ThreeRenderer {
1652
1744
  /** Make an egg interactively draggable. */
1653
1745
  enableInteraction(index) {
1654
1746
  this._interactiveEgg = index;
1655
- document.body.style.cursor = "grab";
1747
+ if (this.canvas) this.canvas.style.cursor = "grab";
1656
1748
  }
1657
1749
  /** Disable drag interaction. */
1658
1750
  disableInteraction() {
@@ -1661,7 +1753,7 @@ class ThreeRenderer {
1661
1753
  this._finaleDragTarget = null;
1662
1754
  this._finaleInteractive = false;
1663
1755
  this._cancelLongPress();
1664
- document.body.style.cursor = "";
1756
+ if (this.canvas) this.canvas.style.cursor = "";
1665
1757
  }
1666
1758
  /** Trigger spectacular egg reveal. */
1667
1759
  revealEgg(index) {
@@ -1805,7 +1897,7 @@ class ThreeRenderer {
1805
1897
  this._finaleInteractive = true;
1806
1898
  this._finaleDragTarget = null;
1807
1899
  this._finalePausedRotation = [false, false, false];
1808
- document.body.style.cursor = "grab";
1900
+ if (this.canvas) this.canvas.style.cursor = "grab";
1809
1901
  for (let i = 0; i < 3; i++) {
1810
1902
  const state = this._eggStates[i];
1811
1903
  state.phase = "finale";
@@ -1829,14 +1921,27 @@ class ThreeRenderer {
1829
1921
  fontFamily: "'Georgia', 'Times New Roman', serif",
1830
1922
  textAlign: "center"
1831
1923
  });
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
- `;
1924
+ this._greetingOverlay.innerHTML = "";
1925
+ const headingDiv = document.createElement("div");
1926
+ Object.assign(headingDiv.style, {
1927
+ fontSize: "48px",
1928
+ color: "#fff8e8",
1929
+ 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)",
1930
+ marginBottom: "10px",
1931
+ letterSpacing: "0.03em",
1932
+ fontWeight: "bold"
1933
+ });
1934
+ headingDiv.textContent = "Happy Easter! ๐Ÿฐ๐Ÿฅš";
1935
+ this._greetingOverlay.appendChild(headingDiv);
1936
+ const subDiv = document.createElement("div");
1937
+ Object.assign(subDiv.style, {
1938
+ fontSize: "17px",
1939
+ color: "rgba(245,240,230,0.85)",
1940
+ fontStyle: "italic",
1941
+ textShadow: "0 0 12px rgba(212,165,80,0.3), 0 1px 6px rgba(0,0,0,0.4)"
1942
+ });
1943
+ subDiv.textContent = "Wishing you joy, peace, and light";
1944
+ this._greetingOverlay.appendChild(subDiv);
1840
1945
  document.body.appendChild(this._greetingOverlay);
1841
1946
  requestAnimationFrame(() => {
1842
1947
  requestAnimationFrame(() => {
@@ -3273,7 +3378,7 @@ const _ResultsRenderer = class _ResultsRenderer {
3273
3378
  this.shadow.appendChild(style);
3274
3379
  const panel = document.createElement("div");
3275
3380
  panel.className = "eeq-results";
3276
- panel.innerHTML = this._buildContent(score);
3381
+ this._buildContentDOM(panel, score);
3277
3382
  this.shadow.appendChild(panel);
3278
3383
  requestAnimationFrame(() => {
3279
3384
  requestAnimationFrame(() => {
@@ -3293,7 +3398,7 @@ const _ResultsRenderer = class _ResultsRenderer {
3293
3398
  this.shadow = null;
3294
3399
  }
3295
3400
  // โ”€โ”€โ”€ Content โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3296
- _buildContent(s) {
3401
+ _buildContentDOM(parent, s) {
3297
3402
  const bp = s.behaviorProfile;
3298
3403
  const actionIcons = {
3299
3404
  clicker: "๐Ÿ–ฑ",
@@ -3314,31 +3419,56 @@ const _ResultsRenderer = class _ResultsRenderer {
3314
3419
  explorer: "๐Ÿงญ",
3315
3420
  philosopher: "๐Ÿ•Š"
3316
3421
  };
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
- `;
3422
+ const inner = document.createElement("div");
3423
+ inner.className = "eeq-results-inner";
3424
+ const title = document.createElement("div");
3425
+ title.className = "eeq-results-title";
3426
+ title.textContent = `ยซ ${bp.title} ยป`;
3427
+ inner.appendChild(title);
3428
+ const time = document.createElement("div");
3429
+ time.className = "eeq-time";
3430
+ time.textContent = formatTime(s.totalTime);
3431
+ inner.appendChild(time);
3432
+ const badgesDiv = document.createElement("div");
3433
+ badgesDiv.className = "eeq-badges";
3434
+ const icons = [
3435
+ { icon: actionIcons[bp.dominantAction], label: bp.dominantAction },
3436
+ { icon: mouseIcons[bp.mousePersonality], label: bp.mousePersonality },
3437
+ { icon: paceIcons[bp.pace], label: bp.pace }
3438
+ ];
3439
+ for (const { icon, label } of icons) {
3440
+ if (!icon) continue;
3441
+ const span = document.createElement("span");
3442
+ span.className = "eeq-badge";
3443
+ span.title = label;
3444
+ span.textContent = icon;
3445
+ badgesDiv.appendChild(span);
3446
+ }
3447
+ inner.appendChild(badgesDiv);
3448
+ const invite = document.createElement("div");
3449
+ invite.className = "eeq-share-invite";
3450
+ invite.textContent = "share your result with friends ๐Ÿฅš";
3451
+ inner.appendChild(invite);
3452
+ const actions = document.createElement("div");
3453
+ actions.className = "eeq-results-actions";
3454
+ const shareBtn = document.createElement("button");
3455
+ shareBtn.className = "eeq-btn eeq-btn-share";
3456
+ shareBtn.title = "share result";
3457
+ 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>';
3458
+ const shareLabel = document.createElement("span");
3459
+ shareLabel.textContent = "share";
3460
+ shareBtn.appendChild(shareLabel);
3461
+ actions.appendChild(shareBtn);
3462
+ const finishBtn = document.createElement("button");
3463
+ finishBtn.className = "eeq-btn eeq-btn-finish";
3464
+ finishBtn.title = "finish";
3465
+ 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>';
3466
+ const finishLabel = document.createElement("span");
3467
+ finishLabel.textContent = "finish";
3468
+ finishBtn.appendChild(finishLabel);
3469
+ actions.appendChild(finishBtn);
3470
+ inner.appendChild(actions);
3471
+ parent.appendChild(inner);
3342
3472
  }
3343
3473
  // โ”€โ”€โ”€ 2D Egg Drawing (type-accurate) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3344
3474
  _makeRand(seed) {
@@ -3703,9 +3833,6 @@ const _ResultsRenderer = class _ResultsRenderer {
3703
3833
  span.textContent = orig;
3704
3834
  }, 1500);
3705
3835
  }
3706
- _escapeHtml(s) {
3707
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3708
- }
3709
3836
  // โ”€โ”€โ”€ Styles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3710
3837
  _getStyles() {
3711
3838
  const t = this.config.theme;
@@ -4092,7 +4219,11 @@ class Persistence {
4092
4219
  try {
4093
4220
  const raw = localStorage.getItem(STORAGE_KEY);
4094
4221
  if (!raw) return [];
4095
- return JSON.parse(raw);
4222
+ const parsed = JSON.parse(raw);
4223
+ if (!Array.isArray(parsed)) return [];
4224
+ return parsed.filter(
4225
+ (item) => typeof item === "object" && item !== null && typeof item.totalTime === "number" && Number.isFinite(item.totalTime)
4226
+ );
4096
4227
  } catch {
4097
4228
  return [];
4098
4229
  }