db-gn 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/v2.html +1314 -369
package/v2.html CHANGED
@@ -257,6 +257,219 @@
257
257
  cursor: not-allowed;
258
258
  }
259
259
 
260
+ #autoclick-menu {
261
+ position: fixed;
262
+ z-index: 60;
263
+ display: none;
264
+ flex-direction: column;
265
+ gap: 4px;
266
+ min-width: 240px;
267
+ max-width: 320px;
268
+ max-height: 60vh;
269
+ overflow-y: auto;
270
+ padding: 6px;
271
+ background: rgba(30, 26, 46, 0.97);
272
+ border: 2px solid rgba(176, 0, 230, 0.45);
273
+ border-radius: 10px;
274
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
275
+ backdrop-filter: blur(6px);
276
+ font-family: 'Comic Sans MS', sans-serif;
277
+ }
278
+
279
+ #autoclick-menu.open {
280
+ display: flex;
281
+ }
282
+
283
+ #autoclick-menu .ac-item {
284
+ appearance: none;
285
+ border: 0;
286
+ background: none;
287
+ font-family: inherit;
288
+ font-size: 15px;
289
+ text-align: left;
290
+ color: #fff;
291
+ padding: 9px 10px;
292
+ border-radius: 6px;
293
+ cursor: pointer;
294
+ transition: background 0.15s ease, color 0.15s ease;
295
+ }
296
+
297
+ #autoclick-menu .ac-item:hover:not(:disabled),
298
+ #autoclick-menu .ac-item:focus-visible {
299
+ background: rgba(176, 0, 230, 0.35);
300
+ outline: none;
301
+ }
302
+
303
+ #autoclick-menu .ac-item:disabled {
304
+ opacity: 0.4;
305
+ cursor: not-allowed;
306
+ }
307
+
308
+ #autoclick-menu .ac-new {
309
+ font-weight: 700;
310
+ border-bottom: 1px solid rgba(255, 255, 255, 0.15);
311
+ border-radius: 6px 6px 0 0;
312
+ }
313
+
314
+ #autoclick-menu .ac-list {
315
+ display: flex;
316
+ flex-direction: column;
317
+ gap: 2px;
318
+ }
319
+
320
+ #autoclick-menu .ac-row {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 4px;
324
+ padding: 2px 4px 2px 0;
325
+ }
326
+
327
+ #autoclick-menu .ac-row.off .ac-name,
328
+ #autoclick-menu .ac-row.off .ac-loc {
329
+ opacity: 0.45;
330
+ }
331
+
332
+ #autoclick-menu .ac-name {
333
+ flex: 1;
334
+ appearance: none;
335
+ border: 0;
336
+ background: none;
337
+ font-family: inherit;
338
+ font-size: 15px;
339
+ text-align: left;
340
+ color: #fff;
341
+ padding: 7px 8px;
342
+ border-radius: 6px;
343
+ cursor: text;
344
+ min-width: 0;
345
+ overflow: hidden;
346
+ text-overflow: ellipsis;
347
+ white-space: nowrap;
348
+ }
349
+
350
+ #autoclick-menu .ac-name:hover,
351
+ #autoclick-menu .ac-name:focus-visible {
352
+ background: rgba(176, 0, 230, 0.35);
353
+ outline: none;
354
+ }
355
+
356
+ #autoclick-menu .ac-name-input {
357
+ flex: 1;
358
+ min-width: 0;
359
+ font-family: inherit;
360
+ font-size: 15px;
361
+ color: #fff;
362
+ padding: 6px 7px;
363
+ border-radius: 6px;
364
+ border: 1px solid rgba(176, 0, 230, 0.7);
365
+ background: rgba(0, 0, 0, 0.35);
366
+ outline: none;
367
+ }
368
+
369
+ #autoclick-menu .ac-loc {
370
+ flex: none;
371
+ appearance: none;
372
+ border: 0;
373
+ background: none;
374
+ font-family: inherit;
375
+ font-size: 13px;
376
+ color: rgba(255, 255, 255, 0.6);
377
+ padding: 4px 6px;
378
+ border-radius: 6px;
379
+ cursor: pointer;
380
+ }
381
+
382
+ #autoclick-menu .ac-loc:hover {
383
+ background: rgba(176, 0, 230, 0.35);
384
+ color: #fff;
385
+ }
386
+
387
+ #autoclick-menu .ac-all {
388
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
389
+ margin-bottom: 2px;
390
+ padding-bottom: 6px;
391
+ }
392
+
393
+ #autoclick-menu .ac-all-label {
394
+ flex: 1;
395
+ padding: 4px 8px;
396
+ font-size: 14px;
397
+ font-weight: 700;
398
+ color: rgba(255, 255, 255, 0.85);
399
+ }
400
+
401
+ #autoclick-menu .ac-switch {
402
+ flex: none;
403
+ appearance: none;
404
+ border: 0;
405
+ width: 34px;
406
+ height: 18px;
407
+ padding: 0;
408
+ border-radius: 999px;
409
+ background: rgba(255, 255, 255, 0.25);
410
+ cursor: pointer;
411
+ position: relative;
412
+ transition: background 0.15s ease;
413
+ }
414
+
415
+ #autoclick-menu .ac-switch.on {
416
+ background: rgba(0, 220, 120, 0.85);
417
+ }
418
+
419
+ #autoclick-menu .ac-switch:disabled {
420
+ opacity: 0.35;
421
+ cursor: not-allowed;
422
+ }
423
+
424
+ #autoclick-menu .ac-knob {
425
+ position: absolute;
426
+ top: 3px;
427
+ left: 3px;
428
+ width: 12px;
429
+ height: 12px;
430
+ border-radius: 50%;
431
+ background: #fff;
432
+ transition: transform 0.15s ease;
433
+ }
434
+
435
+ #autoclick-menu .ac-switch.on .ac-knob {
436
+ transform: translateX(16px);
437
+ }
438
+
439
+ #autoclick-menu .ac-remove {
440
+ appearance: none;
441
+ border: 0;
442
+ background: none;
443
+ color: rgba(255, 255, 255, 0.6);
444
+ font-family: inherit;
445
+ font-size: 18px;
446
+ line-height: 1;
447
+ padding: 6px 8px;
448
+ border-radius: 6px;
449
+ cursor: pointer;
450
+ }
451
+
452
+ #autoclick-menu .ac-remove:hover {
453
+ background: rgba(255, 60, 60, 0.3);
454
+ color: #fff;
455
+ }
456
+
457
+ #autoclick-menu .ac-group {
458
+ padding: 8px 10px 2px;
459
+ font-size: 13px;
460
+ font-weight: 700;
461
+ color: rgba(214, 140, 255, 0.9);
462
+ overflow: hidden;
463
+ text-overflow: ellipsis;
464
+ white-space: nowrap;
465
+ }
466
+
467
+ #autoclick-menu .ac-empty {
468
+ padding: 9px 10px;
469
+ font-size: 14px;
470
+ color: rgba(255, 255, 255, 0.55);
471
+ }
472
+
260
473
  .autoclick-overlay {
261
474
  position: absolute;
262
475
  inset: 0;
@@ -292,6 +505,14 @@
292
505
  z-index: 51;
293
506
  }
294
507
 
508
+ .autoclick-marker.paused {
509
+ border-color: rgba(255, 255, 255, 0.55);
510
+ }
511
+
512
+ .autoclick-marker.paused::after {
513
+ background: rgba(255, 255, 255, 0.55);
514
+ }
515
+
295
516
  .autoclick-marker::after {
296
517
  content: "";
297
518
  position: absolute;
@@ -413,10 +634,122 @@
413
634
 
414
635
  .main-content {
415
636
  min-height: 100%;
416
- padding: 20px 22px 50px;
637
+ /* extra bottom room so the fixed corner links never sit on content */
638
+ padding: 20px 22px 80px;
417
639
  position: relative;
418
640
  }
419
641
 
642
+ /* Pinned to the viewport, so the row holds its corner while the list
643
+ scrolls. Lives inside #home-pane so it hides along with it. */
644
+ #game-context-menu {
645
+ position: fixed;
646
+ z-index: 60;
647
+ display: none;
648
+ flex-direction: column;
649
+ min-width: 210px;
650
+ padding: 6px;
651
+ background: rgba(30, 26, 46, 0.97);
652
+ border: 2px solid rgba(176, 0, 230, 0.45);
653
+ border-radius: 10px;
654
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
655
+ backdrop-filter: blur(6px);
656
+ font-family: 'Comic Sans MS', sans-serif;
657
+ }
658
+
659
+ #game-context-menu.open {
660
+ display: flex;
661
+ }
662
+
663
+ #game-context-menu .ctx-item {
664
+ appearance: none;
665
+ border: 0;
666
+ background: none;
667
+ font-family: inherit;
668
+ font-size: 15px;
669
+ text-align: left;
670
+ color: #fff;
671
+ padding: 9px 10px;
672
+ border-radius: 6px;
673
+ cursor: pointer;
674
+ transition: background 0.15s ease, color 0.15s ease;
675
+ }
676
+
677
+ #game-context-menu .ctx-item:hover,
678
+ #game-context-menu .ctx-item:focus-visible {
679
+ background: rgba(176, 0, 230, 0.35);
680
+ color: #fff;
681
+ outline: none;
682
+ }
683
+
684
+ #corner-links {
685
+ position: fixed;
686
+ right: 20px;
687
+ bottom: 14px;
688
+ z-index: 5;
689
+ display: flex;
690
+ flex-wrap: wrap;
691
+ justify-content: flex-end;
692
+ align-items: baseline;
693
+ gap: 6px 20px;
694
+ }
695
+
696
+ .corner-link {
697
+ display: inline-flex;
698
+ align-items: baseline;
699
+ gap: 6px;
700
+ margin: 0;
701
+ padding: 0;
702
+ border: 0;
703
+ background: none;
704
+ font-family: 'Comic Sans MS', sans-serif;
705
+ font-size: 16px;
706
+ line-height: 1.4;
707
+ cursor: pointer;
708
+ color: rgba(255, 255, 255, 0.6);
709
+ text-decoration: none;
710
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
711
+ transition: color 0.2s ease;
712
+ }
713
+
714
+ /* Underline lives on the text itself: text-decoration on an inline-flex
715
+ container does not reliably reach its flex items. It stays
716
+ transparent until hover/focus so the row reads clean at rest. */
717
+ .corner-link span {
718
+ text-decoration: underline;
719
+ text-decoration-color: transparent;
720
+ text-decoration-thickness: 1px;
721
+ text-underline-offset: 3px;
722
+ transition: color 0.2s ease, text-decoration-color 0.2s ease;
723
+ }
724
+
725
+ #github-link svg {
726
+ width: 17px;
727
+ height: 17px;
728
+ flex-shrink: 0;
729
+ /* nudge the icon down so it sits on the text baseline */
730
+ transform: translateY(2px);
731
+ }
732
+
733
+ .corner-link:hover,
734
+ .corner-link:focus-visible {
735
+ color: #d98cff;
736
+ }
737
+
738
+ .corner-link:hover span,
739
+ .corner-link:focus-visible span {
740
+ text-decoration-color: #d98cff;
741
+ }
742
+
743
+ #github-link:visited {
744
+ color: rgba(255, 255, 255, 0.6);
745
+ }
746
+
747
+ #github-link:visited:hover {
748
+ color: #d98cff;
749
+ }
750
+
751
+
752
+
420
753
 
421
754
  /* ==========================================================================
422
755
  6. Typography & Headings
@@ -609,112 +942,6 @@
609
942
  font-size: 20px;
610
943
  }
611
944
 
612
- .recent-pane {
613
- height: 100%;
614
- overflow-y: auto;
615
- box-sizing: border-box;
616
- padding: 32px 20px 48px;
617
- color: white;
618
- background: linear-gradient(135deg, #2e3244 30%, #561a86 100%);
619
- background-attachment: fixed;
620
- }
621
-
622
- .recent-inner {
623
- max-width: 900px;
624
- margin: 0 auto;
625
- }
626
-
627
- .recent-header {
628
- text-align: center;
629
- margin-bottom: 8px;
630
- font-size: 32px;
631
- font-weight: bold;
632
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
633
- }
634
-
635
- .recent-subheader {
636
- text-align: center;
637
- margin: 0 0 24px;
638
- font-size: 15px;
639
- color: rgba(255, 255, 255, 0.55);
640
- padding-bottom: 18px;
641
- border-bottom: 3px solid rgba(255, 255, 255, 0.15);
642
- }
643
-
644
- .recent-list {
645
- display: grid;
646
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
647
- gap: 12px;
648
- }
649
-
650
- .recent-card {
651
- display: flex;
652
- align-items: center;
653
- gap: 14px;
654
- width: 100%;
655
- padding: 14px 16px;
656
- box-sizing: border-box;
657
- text-align: left;
658
- font-size: 16px;
659
- font-weight: 500;
660
- color: white;
661
- background: rgba(255, 255, 255, 0.08);
662
- border: 2px solid rgba(255, 255, 255, 0.12);
663
- border-radius: 12px;
664
- cursor: pointer;
665
- backdrop-filter: blur(5px);
666
- transition: all 0.25s ease;
667
- }
668
-
669
- .recent-card:hover {
670
- background: rgba(176, 0, 230, 0.25);
671
- border-color: rgba(176, 0, 230, 0.7);
672
- transform: translateY(-2px);
673
- box-shadow: 0 6px 15px rgba(176, 0, 230, 0.35);
674
- }
675
-
676
- .recent-card:active {
677
- transform: translateY(0);
678
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
679
- }
680
-
681
- .recent-rank {
682
- display: inline-flex;
683
- align-items: center;
684
- justify-content: center;
685
- flex-shrink: 0;
686
- width: 32px;
687
- height: 32px;
688
- border-radius: 50%;
689
- background: rgba(0, 0, 0, 0.3);
690
- border: 1px solid rgba(255, 255, 255, 0.15);
691
- font-size: 14px;
692
- color: rgba(255, 255, 255, 0.6);
693
- }
694
-
695
- .recent-card:hover .recent-rank {
696
- color: white;
697
- border-color: rgba(255, 255, 255, 0.5);
698
- }
699
-
700
- .recent-name {
701
- flex: 1;
702
- min-width: 0;
703
- overflow: hidden;
704
- text-overflow: ellipsis;
705
- white-space: nowrap;
706
- }
707
-
708
- .recent-empty {
709
- text-align: center;
710
- padding: 60px 20px;
711
- color: rgba(255, 255, 255, 0.5);
712
- font-size: 17px;
713
- line-height: 1.7;
714
- border: 2px dashed rgba(255, 255, 255, 0.15);
715
- border-radius: 14px;
716
- }
717
-
718
945
  .runner-pane {
719
946
  display: flex;
720
947
  flex-direction: column;
@@ -771,6 +998,24 @@
771
998
  background: #006094;
772
999
  }
773
1000
 
1001
+ .runner-check {
1002
+ display: flex;
1003
+ align-items: center;
1004
+ gap: 8px;
1005
+ margin-top: 10px;
1006
+ color: rgba(255, 255, 255, 0.85);
1007
+ font-size: 14px;
1008
+ cursor: pointer;
1009
+ user-select: none;
1010
+ }
1011
+
1012
+ .runner-check input {
1013
+ width: 16px;
1014
+ height: 16px;
1015
+ accent-color: #03a5fc;
1016
+ cursor: pointer;
1017
+ }
1018
+
774
1019
  .runner-frame {
775
1020
  width: 100%;
776
1021
  height: 100%;
@@ -780,13 +1025,9 @@
780
1025
  }
781
1026
 
782
1027
  /* Common layout configuration for external operational utility keys */
783
- #open-runner-btn,
784
1028
  #open-proxy-btn,
785
1029
  #force-open-proxy-btn,
786
- #open-form-btn,
787
- #doc-btn,
788
- #open-recent-btn,
789
- #open-bookmarklet-runner-btn {
1030
+ #doc-btn {
790
1031
  font-family: Comic Sans MS, sans-serif;
791
1032
  background: none;
792
1033
  border: 2px solid rgba(176, 0, 230 0.25);
@@ -801,12 +1042,8 @@
801
1042
  margin-left: 8px;
802
1043
  }
803
1044
 
804
- #open-runner-btn:hover,
805
1045
  #open-proxy-btn:hover,
806
- #open-form-btn:hover,
807
- #doc-btn:hover,
808
- #open-recent-btn:hover,
809
- #open-bookmarklet-runner-btn:hover {
1046
+ #doc-btn:hover {
810
1047
  transform: translateY(-2px);
811
1048
  box-shadow: 0 6px 15px rgba(176, 0, 230, 0.4);
812
1049
  }
@@ -851,7 +1088,7 @@
851
1088
  }
852
1089
 
853
1090
  .main-content {
854
- padding: 16px 12px 42px;
1091
+ padding: 16px 12px 110px;
855
1092
  }
856
1093
 
857
1094
  .runner-grid {
@@ -875,62 +1112,6 @@
875
1112
  Made by a Turtle
876
1113
  </h2>
877
1114
 
878
- <h2 class="subheading">
879
- Documentation:
880
- <button id="doc-btn" onclick="
881
- fetch('https://cdn.jsdelivr.net/gh/ltorl/dancing-bunny@latest/docs/documentation.html')
882
- .then(response => response.text())
883
- .then(text => {
884
- const newWin = window.open('about:blank', '_blank');
885
- if (newWin) {
886
- newWin.document.open();
887
- newWin.document.write(text);
888
- newWin.document.close();
889
- }
890
- });">
891
- Dancing Bunny GN Doc
892
- </button>
893
- </h2>
894
-
895
- <h2 class="subheading">
896
- Run HTML Code:
897
- <button id="open-runner-btn" class="btn-outline">Open Code Runner</button>
898
- </h2>
899
-
900
- <h2 class="subheading">
901
- Run Bookmarklet Code:
902
- <button id="open-bookmarklet-runner-btn" style="line-height: 2; margin-left: 8px;">
903
- Open Bookmarklet Runner
904
- </button>
905
- </h2>
906
-
907
- <!--
908
- <h2 class="subheading">
909
- Other Apps:
910
- <select id="proxy-selector">
911
- <option value="https://truffled.lol/">Truffled</option>
912
- <option value="https://incog.works/">Incognito</option>
913
- <option value="https://gointospace.app/">Interstellar</option>
914
- <option value="https://anura.pro/">Anura</option>
915
- <option value="https://definitelyscience.com/">Definitely Science</option>
916
- </select>
917
-
918
- <button class="btn-outline" id="open-proxy-btn" onclick="openSelectedProxy()">Open</button>
919
- <button class="btn-outline" onclick="openSelectedProxyForceIframe()" id="force-open-proxy-btn">Force
920
- Iframe</button>
921
- </h2>
922
- -->
923
-
924
- <h2 class="subheading">
925
- Recently Played:
926
- <button id="open-recent-btn" class="btn-outline">Open Recent Games</button>
927
- </h2>
928
-
929
- <h2 class="subheading">
930
- Make a sugestion:
931
- <button id="open-form-btn" class="btn-outline">Open Form</button>
932
- </h2>
933
-
934
1115
  <br /><br />
935
1116
 
936
1117
  <div class="search-container">
@@ -940,6 +1121,20 @@
940
1121
 
941
1122
  <div id="sections-container"></div>
942
1123
  </div>
1124
+
1125
+ <div id="corner-links">
1126
+ <button type="button" id="open-runner-btn" class="corner-link"><span>HTML Runner</span></button>
1127
+ <button type="button" id="open-bookmarklet-runner-btn" class="corner-link"><span>Bookmarklet Runner</span></button>
1128
+ <button type="button" id="open-form-btn" class="corner-link"><span>Make a Suggestion</span></button>
1129
+ <a id="github-link" class="corner-link" href="https://github.com/ltorl/dancing-bunny" target="_blank" rel="noopener noreferrer"
1130
+ title="View the source on GitHub" aria-label="View the source on GitHub">
1131
+ <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
1132
+ <path fill="currentColor"
1133
+ d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
1134
+ </svg>
1135
+ <span>Github</span>
1136
+ </a>
1137
+ </div>
943
1138
  </div>
944
1139
  </div>
945
1140
 
@@ -971,7 +1166,7 @@
971
1166
  const MUTE_KEY = "dancing-bunny-muted-v1";
972
1167
  const RECENT_KEY = "dancing-bunny-recent-v1";
973
1168
  const BGRUN_KEY = "dancing-bunny-bgrun-v1";
974
- const RECENT_MAX = 20;
1169
+ const RECENT_MAX = 15;
975
1170
 
976
1171
  // When true, game tabs keep rendering (and therefore keep running) while
977
1172
  // another tab is on screen. Turning it off pauses them to save CPU.
@@ -1016,7 +1211,7 @@
1016
1211
  function recordRecent(file) {
1017
1212
  recent = [file, ...recent.filter(f => f !== file)].slice(0, RECENT_MAX);
1018
1213
  saveRecent();
1019
- renderRecentPane();
1214
+ renderRecentSection();
1020
1215
  }
1021
1216
 
1022
1217
  function saveMute() {
@@ -1080,9 +1275,9 @@
1080
1275
  btn.id = "autoclick-btn";
1081
1276
  btn.type = "button";
1082
1277
  btn.innerHTML = ICON_AUTOCLICK;
1083
- btn.title = "Auto Clicker";
1084
- btn.setAttribute("aria-label", "Auto Clicker");
1085
- btn.onclick = toggleAutoclick;
1278
+ btn.title = "Auto Clickers";
1279
+ btn.setAttribute("aria-label", "Auto Clickers");
1280
+ btn.onclick = toggleAutoclickMenu;
1086
1281
  tabBar.appendChild(btn);
1087
1282
  }
1088
1283
 
@@ -1105,11 +1300,21 @@
1105
1300
 
1106
1301
  function applyBackgroundRunState() {
1107
1302
  document.body.classList.toggle("no-bg-run", !bgRunEnabled);
1303
+
1304
+ document.querySelectorAll("iframe").forEach(frame => {
1305
+ try {
1306
+ frame.contentWindow?.postMessage({ type: "OFF_TAB_RUN", enabled: bgRunEnabled }, "*");
1307
+ if (typeof frame.contentWindow?.__setOffTabRun === "function") {
1308
+ frame.contentWindow.__setOffTabRun(bgRunEnabled);
1309
+ }
1310
+ } catch (e) { }
1311
+ });
1312
+
1108
1313
  const btn = document.getElementById("bgrun-btn");
1109
1314
  if (btn) {
1110
1315
  btn.classList.toggle("off", !bgRunEnabled);
1111
1316
  btn.title = bgRunEnabled
1112
- ? "Background games: ON (they keep running on other tabs)"
1317
+ ? "Background games: ON (they keep running on other subtabs and while this browser tab is hidden)"
1113
1318
  : "Background games: OFF (paused when not visible)";
1114
1319
  btn.setAttribute("aria-label", btn.title);
1115
1320
  btn.setAttribute("aria-pressed", bgRunEnabled ? "true" : "false");
@@ -1191,9 +1396,66 @@
1191
1396
  background games keep getting clicked; only an unplaced picker is
1192
1397
  cancelled when you leave its tab. */
1193
1398
 
1194
- const AUTOCLICK_INTERVAL_MS = 100;
1399
+ const AUTOCLICK_INTERVAL_MS = 1;
1195
1400
 
1196
- const autoclickers = new Map(); // tab id -> clicker state
1401
+ // clicker id -> clicker state. A tab can hold any number of them.
1402
+ const autoclickers = new Map();
1403
+ let autoclickSeq = 0;
1404
+
1405
+ /* Placed clickers survive closing a game: tab id -> [{ name, x, y }].
1406
+ Only the spots are kept, never the on/off state, so a reopened game
1407
+ always comes back with every clicker switched off. */
1408
+ const CLICKERS_KEY = "dancing-bunny-clickers-v1";
1409
+ let savedClickers = {};
1410
+ try {
1411
+ const parsed = JSON.parse(localStorage.getItem(CLICKERS_KEY) || "{}");
1412
+ if (parsed && typeof parsed === "object") savedClickers = parsed;
1413
+ } catch (e) { savedClickers = {}; }
1414
+
1415
+ function persistClickers() {
1416
+ try { localStorage.setItem(CLICKERS_KEY, JSON.stringify(savedClickers)); } catch (e) { }
1417
+ }
1418
+
1419
+ function saveClickersForTab(tabId) {
1420
+ if (!tabId) return;
1421
+ const placed = clickersForTab(tabId)
1422
+ .filter(c => c.mode === "running")
1423
+ .map(c => ({ name: c.name, x: c.x, y: c.y }));
1424
+ if (placed.length) savedClickers[tabId] = placed;
1425
+ else delete savedClickers[tabId];
1426
+ persistClickers();
1427
+ }
1428
+
1429
+ /* Rebuilds a game's saved clickers onto a freshly loaded iframe. They
1430
+ come back parked (off) with their markers in place. */
1431
+ function restoreClickersForTab(tabId, panel, iframe) {
1432
+ const saved = savedClickers[tabId];
1433
+ if (!Array.isArray(saved) || saved.length === 0) return;
1434
+
1435
+ saved.forEach((entry, i) => {
1436
+ if (!entry || typeof entry.x !== "number" || typeof entry.y !== "number") return;
1437
+ const clicker = {
1438
+ id: `ac-${++autoclickSeq}`,
1439
+ tabId,
1440
+ name: entry.name || `Clicker ${i + 1}`,
1441
+ enabled: false,
1442
+ panel,
1443
+ iframe,
1444
+ overlay: null,
1445
+ marker: null,
1446
+ timer: null,
1447
+ x: entry.x,
1448
+ y: entry.y,
1449
+ mode: "running"
1450
+ };
1451
+ autoclickers.set(clicker.id, clicker);
1452
+ clicker.fire = makeClickerFire(clicker);
1453
+ placeClickerMarker(clicker);
1454
+ });
1455
+
1456
+ setAutoclickButtonState();
1457
+ refreshAutoclickMenu();
1458
+ }
1197
1459
 
1198
1460
  function panelTabId(panel) {
1199
1461
  if (!panel || !panel.id.startsWith("panel-")) return null;
@@ -1207,145 +1469,443 @@
1207
1469
  return iframe ? { id: panelTabId(panel), panel, iframe } : null;
1208
1470
  }
1209
1471
 
1210
- function currentAutoclicker() {
1472
+ function clickersForTab(tabId) {
1473
+ return [...autoclickers.values()].filter(c => c.tabId === tabId);
1474
+ }
1475
+
1476
+ function currentAutoclickers() {
1211
1477
  const target = activeGamePanel();
1212
- return target ? autoclickers.get(target.id) || null : null;
1478
+ return target ? clickersForTab(target.id) : [];
1213
1479
  }
1214
1480
 
1215
1481
  function runningAutoclickCount() {
1216
1482
  let count = 0;
1217
- autoclickers.forEach(c => { if (c.mode === "running") count++; });
1483
+ autoclickers.forEach(c => { if (c.mode === "running" && c.enabled) count++; });
1218
1484
  return count;
1219
1485
  }
1220
1486
 
1221
1487
  function setAutoclickButtonState() {
1222
1488
  const btn = document.getElementById("autoclick-btn");
1223
1489
  if (!btn) return;
1224
- const current = currentAutoclicker();
1225
- const mode = current ? current.mode : "off";
1226
- const elsewhere = runningAutoclickCount() - (mode === "running" ? 1 : 0);
1490
+ const mine = currentAutoclickers();
1491
+ const arming = mine.some(c => c.mode === "arming");
1492
+ const runningHere = mine.filter(c => c.mode === "running" && c.enabled).length;
1493
+ const elsewhere = runningAutoclickCount() - runningHere;
1494
+
1495
+ btn.classList.toggle("arming", arming);
1496
+ btn.classList.toggle("running", runningHere > 0);
1497
+
1498
+ let title =
1499
+ arming ? "Auto Clicker: pick a spot (Esc to cancel)" :
1500
+ runningHere > 0 ? `Auto Clickers: ${runningHere} running on this tab` :
1501
+ "Auto Clickers";
1502
+ if (elsewhere > 0) {
1503
+ title += ` ‧ ${elsewhere} running in background`;
1504
+ }
1505
+ btn.title = title;
1506
+ btn.setAttribute("aria-label", title);
1507
+ btn.setAttribute("aria-pressed", (arming || runningHere > 0) ? "true" : "false");
1508
+ btn.classList.toggle("background-active", runningHere === 0 && !arming && elsewhere > 0);
1509
+ }
1510
+
1511
+ function stopAutoclick(clicker, opts = {}) {
1512
+ if (!clicker) return;
1513
+ if (clicker.timer) clearInterval(clicker.timer);
1514
+ clicker.timer = null;
1515
+ clicker.overlay?.remove();
1516
+ clicker.marker?.remove();
1517
+ clicker.overlay = null;
1518
+ clicker.marker = null;
1519
+ clicker.mode = "off";
1520
+ autoclickers.delete(clicker.id);
1521
+ // Closing a tab tears its clickers down but must not forget them.
1522
+ if (!opts.keepSaved) saveClickersForTab(clicker.tabId);
1523
+ setAutoclickButtonState();
1524
+ refreshAutoclickMenu();
1525
+ }
1526
+
1527
+ function stopAutoclickForPanel(panel) {
1528
+ const id = panelTabId(panel);
1529
+ if (!id) return;
1530
+ clickersForTab(id).forEach(c => stopAutoclick(c, { keepSaved: true }));
1531
+ }
1532
+
1533
+ // A picker that never got a spot shouldn't linger on a tab you left.
1534
+ function cancelArmingAutoclickers() {
1535
+ [...autoclickers.values()].forEach(clicker => {
1536
+ if (clicker.mode === "arming") stopAutoclick(clicker);
1537
+ });
1538
+ }
1539
+
1540
+ function armAutoclick() {
1541
+ const target = activeGamePanel();
1542
+ if (!target) {
1543
+ const btn = document.getElementById("autoclick-btn");
1544
+ if (btn) {
1545
+ const prev = btn.title;
1546
+ btn.title = "Open a game tab first";
1547
+ setTimeout(() => { btn.title = prev; }, 1500);
1548
+ }
1549
+ return;
1550
+ }
1551
+
1552
+ const clicker = {
1553
+ id: `ac-${++autoclickSeq}`,
1554
+ tabId: target.id,
1555
+ name: `Clicker ${clickersForTab(target.id).length + 1}`,
1556
+ enabled: true,
1557
+ panel: target.panel,
1558
+ iframe: target.iframe,
1559
+ overlay: null,
1560
+ marker: null,
1561
+ timer: null,
1562
+ x: 0,
1563
+ y: 0,
1564
+ mode: "arming"
1565
+ };
1566
+ autoclickers.set(clicker.id, clicker);
1567
+
1568
+ const overlay = document.createElement("div");
1569
+ overlay.className = "autoclick-overlay";
1570
+ overlay.innerHTML = `<div class="autoclick-hint">Click a spot to auto-click ‧ Esc to cancel</div>`;
1571
+ overlay.addEventListener("click", (e) => {
1572
+ const rect = clicker.iframe.getBoundingClientRect();
1573
+ startAutoclick(clicker, e.clientX - rect.left, e.clientY - rect.top);
1574
+ });
1575
+ overlay.addEventListener("contextmenu", (e) => { e.preventDefault(); stopAutoclick(clicker); });
1576
+
1577
+ clicker.overlay = overlay;
1578
+ target.panel.appendChild(overlay);
1579
+ setAutoclickButtonState();
1580
+ }
1581
+
1582
+ function makeClickerFire(clicker) {
1583
+ return () => {
1584
+ const frame = clicker.iframe;
1585
+ // The game was closed or reloaded out from under us.
1586
+ if (!frame || !frame.isConnected) { stopAutoclick(clicker, { keepSaved: true }); return; }
1587
+ try {
1588
+ frame.contentWindow?.postMessage(
1589
+ { type: "AUTO_CLICK", x: clicker.x, y: clicker.y }, "*"
1590
+ );
1591
+ } catch (e) { }
1592
+ };
1593
+ }
1594
+
1595
+ function placeClickerMarker(clicker) {
1596
+ clicker.marker?.remove();
1597
+ const marker = document.createElement("div");
1598
+ marker.className = "autoclick-marker";
1599
+ marker.classList.toggle("paused", !clicker.enabled);
1600
+
1601
+ // A hidden panel measures as 0x0; the frame fills the panel anyway,
1602
+ // so fall back to the raw coordinates.
1603
+ const rect = clicker.iframe.getBoundingClientRect();
1604
+ const panelRect = clicker.panel.getBoundingClientRect();
1605
+ const offsetX = rect.width ? rect.left - panelRect.left : 0;
1606
+ const offsetY = rect.height ? rect.top - panelRect.top : 0;
1607
+ marker.style.left = `${offsetX + clicker.x}px`;
1608
+ marker.style.top = `${offsetY + clicker.y}px`;
1609
+
1610
+ clicker.marker = marker;
1611
+ clicker.panel.appendChild(marker);
1612
+ }
1613
+
1614
+ function startAutoclick(clicker, x, y) {
1615
+ clicker.overlay?.remove();
1616
+ clicker.overlay = null;
1617
+ clicker.x = x;
1618
+ clicker.y = y;
1619
+ clicker.mode = "running";
1620
+
1621
+ placeClickerMarker(clicker);
1622
+ clicker.fire = makeClickerFire(clicker);
1623
+
1624
+ if (clicker.enabled) {
1625
+ clicker.fire();
1626
+ clicker.timer = setInterval(clicker.fire, AUTOCLICK_INTERVAL_MS);
1627
+ }
1628
+ saveClickersForTab(clicker.tabId);
1629
+ setAutoclickButtonState();
1630
+ refreshAutoclickMenu();
1631
+ }
1632
+
1633
+ /* Switching a clicker off parks it: the spot is kept, only the firing
1634
+ stops, so it can be switched back on without picking again. */
1635
+ function setClickerEnabled(clicker, enabled) {
1636
+ if (!clicker || clicker.enabled === enabled) return;
1637
+ clicker.enabled = enabled;
1638
+
1639
+ if (!enabled) {
1640
+ if (clicker.timer) clearInterval(clicker.timer);
1641
+ clicker.timer = null;
1642
+ } else if (clicker.mode === "running" && !clicker.timer && clicker.fire) {
1643
+ clicker.fire();
1644
+ clicker.timer = setInterval(clicker.fire, AUTOCLICK_INTERVAL_MS);
1645
+ }
1646
+
1647
+ clicker.marker?.classList.toggle("paused", !enabled);
1648
+ setAutoclickButtonState();
1649
+ }
1650
+
1651
+ /* ---------------- Auto clicker menu ----------------
1652
+ The toolbar button opens a list: a "New Auto Clicker" action on top,
1653
+ then the clickers themselves. On a game tab only that game's clickers
1654
+ are listed; on the home tab every clicker is listed under its game. */
1655
+
1656
+ function ensureAutoclickMenu() {
1657
+ let menu = document.getElementById("autoclick-menu");
1658
+ if (menu) return menu;
1659
+
1660
+ menu = document.createElement("div");
1661
+ menu.id = "autoclick-menu";
1662
+ menu.setAttribute("role", "menu");
1663
+ document.body.appendChild(menu);
1664
+
1665
+ document.addEventListener("mousedown", (e) => {
1666
+ if (!isAutoclickMenuOpen()) return;
1667
+ if (menu.contains(e.target)) return;
1668
+ if (e.target.closest("#autoclick-btn")) return;
1669
+ closeAutoclickMenu();
1670
+ });
1671
+ window.addEventListener("resize", closeAutoclickMenu);
1672
+ window.addEventListener("blur", closeAutoclickMenu);
1673
+
1674
+ return menu;
1675
+ }
1676
+
1677
+ function isAutoclickMenuOpen() {
1678
+ return !!document.getElementById("autoclick-menu")?.classList.contains("open");
1679
+ }
1680
+
1681
+ function closeAutoclickMenu() {
1682
+ document.getElementById("autoclick-menu")?.classList.remove("open");
1683
+ }
1684
+
1685
+ function clickerLocationLabel(clicker) {
1686
+ if (clicker.mode === "arming") return "picking a spot…";
1687
+ return `${Math.round(clicker.x)}, ${Math.round(clicker.y)}`;
1688
+ }
1689
+
1690
+ function tabTitleFor(tabId) {
1691
+ return tabs.get(tabId)?.title || "Game";
1692
+ }
1693
+
1694
+ /* A small on/off switch. `state` is the current value; `onToggle` gets
1695
+ the value it was flipped to. */
1696
+ function makeAutoclickSwitch(state, label, onToggle, disabled = false) {
1697
+ const sw = document.createElement("button");
1698
+ sw.type = "button";
1699
+ sw.className = "ac-switch";
1700
+ sw.setAttribute("role", "switch");
1701
+ sw.setAttribute("aria-checked", state ? "true" : "false");
1702
+ sw.setAttribute("aria-label", label);
1703
+ sw.title = label;
1704
+ sw.classList.toggle("on", !!state);
1705
+ sw.disabled = disabled;
1706
+ sw.innerHTML = `<span class="ac-knob"></span>`;
1707
+ sw.onclick = () => { onToggle(!state); };
1708
+ return sw;
1709
+ }
1710
+
1711
+ /* Clicking a clicker's name swaps it for a text field. Enter or blur
1712
+ commits, Escape puts the old name back. */
1713
+ function startClickerRename(clicker, nameBtn) {
1714
+ const input = document.createElement("input");
1715
+ input.type = "text";
1716
+ input.className = "ac-name-input";
1717
+ input.value = clicker.name;
1718
+ input.maxLength = 40;
1719
+
1720
+ let done = false;
1721
+ const finish = (save) => {
1722
+ if (done) return;
1723
+ done = true;
1724
+ if (save) {
1725
+ const next = input.value.replace(/\s+/g, " ").trim();
1726
+ if (next && next !== clicker.name) {
1727
+ clicker.name = next;
1728
+ saveClickersForTab(clicker.tabId);
1729
+ }
1730
+ }
1731
+ nameBtn.textContent = clicker.name;
1732
+ input.replaceWith(nameBtn);
1733
+ };
1734
+
1735
+ input.addEventListener("keydown", (e) => {
1736
+ e.stopPropagation();
1737
+ if (e.key === "Enter") { e.preventDefault(); finish(true); }
1738
+ else if (e.key === "Escape") { e.preventDefault(); finish(false); }
1739
+ });
1740
+ input.addEventListener("blur", () => finish(true));
1741
+
1742
+ nameBtn.replaceWith(input);
1743
+ input.focus();
1744
+ input.select();
1745
+ }
1746
+
1747
+ function makeAutoclickRow(clicker) {
1748
+ const row = document.createElement("div");
1749
+ row.className = "ac-row";
1750
+ row.classList.toggle("off", !clicker.enabled);
1751
+
1752
+ const nameBtn = document.createElement("button");
1753
+ nameBtn.type = "button";
1754
+ nameBtn.className = "ac-name";
1755
+ nameBtn.textContent = clicker.name;
1756
+ nameBtn.title = "Click to rename";
1757
+ nameBtn.onclick = () => startClickerRename(clicker, nameBtn);
1758
+
1759
+ const loc = document.createElement("button");
1760
+ loc.type = "button";
1761
+ loc.className = "ac-loc";
1762
+ loc.textContent = clickerLocationLabel(clicker);
1763
+ loc.title = "Go to this clicker's tab";
1764
+ loc.onclick = () => {
1765
+ closeAutoclickMenu();
1766
+ if (tabs.has(clicker.tabId)) activateTab(clicker.tabId);
1767
+ };
1227
1768
 
1228
- btn.classList.toggle("arming", mode === "arming");
1229
- btn.classList.toggle("running", mode === "running");
1769
+ const sw = makeAutoclickSwitch(clicker.enabled, `Turn ${clicker.name} ${clicker.enabled ? "off" : "on"}`, (on) => {
1770
+ setClickerEnabled(clicker, on);
1771
+ renderAutoclickMenu();
1772
+ });
1230
1773
 
1231
- let title =
1232
- mode === "arming" ? "Auto Clicker: pick a spot (Esc to cancel)" :
1233
- mode === "running" ? "Auto Clicker: running on this tab (click to stop)" :
1234
- "Auto Clicker";
1235
- if (elsewhere > 0) {
1236
- title += ` \u2027 ${elsewhere} running in background`;
1237
- }
1238
- btn.title = title;
1239
- btn.setAttribute("aria-label", title);
1240
- btn.setAttribute("aria-pressed", mode !== "off" ? "true" : "false");
1241
- btn.classList.toggle("background-active", mode === "off" && elsewhere > 0);
1242
- }
1774
+ const remove = document.createElement("button");
1775
+ remove.type = "button";
1776
+ remove.className = "ac-remove";
1777
+ remove.textContent = "×";
1778
+ remove.title = "Delete this auto clicker";
1779
+ remove.setAttribute("aria-label", "Delete this auto clicker");
1780
+ remove.onclick = () => { stopAutoclick(clicker); };
1243
1781
 
1244
- function stopAutoclick(clicker = currentAutoclicker()) {
1245
- if (!clicker) return;
1246
- if (clicker.timer) clearInterval(clicker.timer);
1247
- clicker.timer = null;
1248
- clicker.overlay?.remove();
1249
- clicker.marker?.remove();
1250
- clicker.overlay = null;
1251
- clicker.marker = null;
1252
- clicker.mode = "off";
1253
- autoclickers.delete(clicker.id);
1254
- setAutoclickButtonState();
1782
+ row.appendChild(nameBtn);
1783
+ row.appendChild(loc);
1784
+ row.appendChild(sw);
1785
+ row.appendChild(remove);
1786
+ return row;
1255
1787
  }
1256
1788
 
1257
- function stopAutoclickForPanel(panel) {
1258
- const id = panelTabId(panel);
1259
- const clicker = id ? autoclickers.get(id) : null;
1260
- if (clicker) stopAutoclick(clicker);
1261
- }
1789
+ /* The "all of this game's clickers" switch: on only when every clicker
1790
+ in the group is on. */
1791
+ function makeAutoclickAllRow(label, clickers) {
1792
+ const row = document.createElement("div");
1793
+ row.className = "ac-row ac-all";
1262
1794
 
1263
- // A picker that never got a spot shouldn't linger on a tab you left.
1264
- function cancelArmingAutoclickers() {
1265
- autoclickers.forEach(clicker => {
1266
- if (clicker.mode === "arming") stopAutoclick(clicker);
1267
- });
1795
+ const text = document.createElement("span");
1796
+ text.className = "ac-all-label";
1797
+ text.textContent = label;
1798
+
1799
+ const allOn = clickers.length > 0 && clickers.every(c => c.enabled);
1800
+ const sw = makeAutoclickSwitch(allOn, `Turn all ${label.toLowerCase()} ${allOn ? "off" : "on"}`, (on) => {
1801
+ clickers.forEach(c => setClickerEnabled(c, on));
1802
+ renderAutoclickMenu();
1803
+ }, clickers.length === 0);
1804
+
1805
+ row.appendChild(text);
1806
+ row.appendChild(sw);
1807
+ return row;
1268
1808
  }
1269
1809
 
1270
- function armAutoclick() {
1810
+ function renderAutoclickMenu() {
1811
+ const menu = ensureAutoclickMenu();
1812
+ menu.innerHTML = "";
1813
+
1271
1814
  const target = activeGamePanel();
1272
- if (!target) {
1273
- const btn = document.getElementById("autoclick-btn");
1274
- if (btn) {
1275
- const prev = btn.title;
1276
- btn.title = "Open a game tab first";
1277
- setTimeout(() => { btn.title = prev; }, 1500);
1278
- }
1279
- return;
1815
+
1816
+ const newBtn = document.createElement("button");
1817
+ newBtn.type = "button";
1818
+ newBtn.className = "ac-item ac-new";
1819
+ newBtn.textContent = "New Auto Clicker";
1820
+ if (target) {
1821
+ newBtn.onclick = () => { closeAutoclickMenu(); armAutoclick(); };
1822
+ } else {
1823
+ newBtn.disabled = true;
1824
+ newBtn.title = "Open a game tab first";
1280
1825
  }
1826
+ menu.appendChild(newBtn);
1281
1827
 
1282
- const clicker = {
1283
- id: target.id,
1284
- panel: target.panel,
1285
- iframe: target.iframe,
1286
- overlay: null,
1287
- marker: null,
1288
- timer: null,
1289
- x: 0,
1290
- y: 0,
1291
- mode: "arming"
1292
- };
1293
- autoclickers.set(clicker.id, clicker);
1828
+ const list = document.createElement("div");
1829
+ list.className = "ac-list";
1294
1830
 
1295
- const overlay = document.createElement("div");
1296
- overlay.className = "autoclick-overlay";
1297
- overlay.innerHTML = `<div class="autoclick-hint">Click a spot to auto-click \u2027 Esc to cancel</div>`;
1298
- overlay.addEventListener("click", (e) => {
1299
- const rect = clicker.iframe.getBoundingClientRect();
1300
- startAutoclick(clicker, e.clientX - rect.left, e.clientY - rect.top);
1301
- });
1302
- overlay.addEventListener("contextmenu", (e) => { e.preventDefault(); stopAutoclick(clicker); });
1831
+ if (target) {
1832
+ // On a game tab: just this game's clickers, with a master switch.
1833
+ const mine = clickersForTab(target.id);
1834
+ list.appendChild(makeAutoclickAllRow("All Clickers", mine));
1835
+ if (mine.length === 0) {
1836
+ list.appendChild(makeAutoclickEmpty("No auto clickers on this game."));
1837
+ } else {
1838
+ mine.forEach(clicker => list.appendChild(makeAutoclickRow(clicker)));
1839
+ }
1840
+ } else {
1841
+ // On the home tab: everything, grouped under its game's name,
1842
+ // each group with its own master switch.
1843
+ const byTab = new Map();
1844
+ autoclickers.forEach(clicker => {
1845
+ if (!byTab.has(clicker.tabId)) byTab.set(clicker.tabId, []);
1846
+ byTab.get(clicker.tabId).push(clicker);
1847
+ });
1303
1848
 
1304
- clicker.overlay = overlay;
1305
- target.panel.appendChild(overlay);
1306
- setAutoclickButtonState();
1849
+ if (byTab.size === 0) {
1850
+ list.appendChild(makeAutoclickEmpty("No auto clickers running."));
1851
+ } else {
1852
+ byTab.forEach((clickers, tabId) => {
1853
+ const header = document.createElement("div");
1854
+ header.className = "ac-group";
1855
+ header.textContent = tabTitleFor(tabId);
1856
+ list.appendChild(header);
1857
+ list.appendChild(makeAutoclickAllRow("All", clickers));
1858
+ clickers.forEach(clicker => list.appendChild(makeAutoclickRow(clicker)));
1859
+ });
1860
+ }
1861
+ }
1862
+
1863
+ menu.appendChild(list);
1307
1864
  }
1308
1865
 
1309
- function startAutoclick(clicker, x, y) {
1310
- clicker.overlay?.remove();
1311
- clicker.overlay = null;
1312
- clicker.x = x;
1313
- clicker.y = y;
1314
- clicker.mode = "running";
1866
+ /* Live updates skip the redraw while a name is being typed. */
1867
+ function refreshAutoclickMenu() {
1868
+ const menu = document.getElementById("autoclick-menu");
1869
+ if (!menu || !menu.classList.contains("open")) return;
1870
+ if (menu.querySelector(".ac-name-input")) return;
1871
+ renderAutoclickMenu();
1872
+ }
1315
1873
 
1316
- const marker = document.createElement("div");
1317
- marker.className = "autoclick-marker";
1318
- const rect = clicker.iframe.getBoundingClientRect();
1319
- const panelRect = clicker.panel.getBoundingClientRect();
1320
- marker.style.left = `${rect.left - panelRect.left + x}px`;
1321
- marker.style.top = `${rect.top - panelRect.top + y}px`;
1322
- clicker.marker = marker;
1323
- clicker.panel.appendChild(marker);
1874
+ function makeAutoclickEmpty(text) {
1875
+ const empty = document.createElement("div");
1876
+ empty.className = "ac-empty";
1877
+ empty.textContent = text;
1878
+ return empty;
1879
+ }
1324
1880
 
1325
- const fire = () => {
1326
- const frame = clicker.iframe;
1327
- // The game was closed or reloaded out from under us.
1328
- if (!frame || !frame.isConnected) { stopAutoclick(clicker); return; }
1329
- try {
1330
- frame.contentWindow?.postMessage(
1331
- { type: "AUTO_CLICK", x: clicker.x, y: clicker.y }, "*"
1332
- );
1333
- } catch (e) { }
1334
- };
1881
+ function openAutoclickMenu() {
1882
+ const btn = document.getElementById("autoclick-btn");
1883
+ const menu = ensureAutoclickMenu();
1884
+ renderAutoclickMenu();
1885
+ menu.classList.add("open");
1335
1886
 
1336
- fire();
1337
- clicker.timer = setInterval(fire, AUTOCLICK_INTERVAL_MS);
1338
- setAutoclickButtonState();
1887
+ if (!btn) return;
1888
+ const anchor = btn.getBoundingClientRect();
1889
+ const rect = menu.getBoundingClientRect();
1890
+ const margin = 8;
1891
+ let left = anchor.right - rect.width;
1892
+ let top = anchor.bottom + 6;
1893
+ left = Math.max(margin, Math.min(left, window.innerWidth - rect.width - margin));
1894
+ top = Math.max(margin, Math.min(top, window.innerHeight - rect.height - margin));
1895
+ menu.style.left = `${left}px`;
1896
+ menu.style.top = `${top}px`;
1339
1897
  }
1340
1898
 
1341
- function toggleAutoclick() {
1342
- const current = currentAutoclicker();
1343
- if (current) stopAutoclick(current);
1344
- else armAutoclick();
1899
+ function toggleAutoclickMenu() {
1900
+ if (isAutoclickMenuOpen()) closeAutoclickMenu();
1901
+ else openAutoclickMenu();
1345
1902
  }
1346
1903
 
1347
1904
  document.addEventListener("keydown", (e) => {
1348
- if (e.key === "Escape") stopAutoclick();
1905
+ if (e.key !== "Escape") return;
1906
+ if (document.activeElement?.classList.contains("ac-name-input")) return;
1907
+ closeAutoclickMenu();
1908
+ cancelArmingAutoclickers();
1349
1909
  });
1350
1910
 
1351
1911
  (function protectTabIdentity() {
@@ -1455,6 +2015,7 @@
1455
2015
 
1456
2016
  function activateTab(id) {
1457
2017
  setTabStatus(id, null);
2018
+ closeAutoclickMenu();
1458
2019
  cancelArmingAutoclickers();
1459
2020
  setActiveTabButton(id);
1460
2021
  setActivePanel(id);
@@ -1668,6 +2229,138 @@
1668
2229
  return `${muteScript}${html}`;
1669
2230
  }
1670
2231
 
2232
+ /* Browsers freeze requestAnimationFrame and clamp timers once the whole
2233
+ browser tab is hidden, which stalls idle/clicker games even though our
2234
+ in-page subtabs keep them mounted. This shim keeps them ticking off-tab
2235
+ by driving their callbacks from a Web Worker's clock instead. It only
2236
+ takes over while the page is hidden, so visible games run on the real
2237
+ rAF, and it dies with the page like anything else. */
2238
+ function injectOffTabSupport(html, enabled = true) {
2239
+ const script = `
2240
+ <script>
2241
+ (function() {
2242
+ if (window.__OFF_TAB_TICKER) return;
2243
+ window.__OFF_TAB_TICKER = true;
2244
+
2245
+ var TICK_MS = 16;
2246
+ var enabled = ${enabled ? "true" : "false"};
2247
+
2248
+ window.addEventListener("message", function(e) {
2249
+ var data = e.data;
2250
+ if (data && data.type === "OFF_TAB_RUN") enabled = !!data.enabled;
2251
+ });
2252
+ window.__setOffTabRun = function(value) { enabled = !!value; };
2253
+
2254
+ function hidden() { return document.visibilityState === "hidden"; }
2255
+ function active() { return enabled && hidden(); }
2256
+
2257
+ /* Worker timers keep running at full rate in a backgrounded page far longer
2258
+ than page timers do; setInterval is the fallback when Workers are blocked. */
2259
+ var tickHandlers = [];
2260
+ function onTick() {
2261
+ for (var i = 0; i < tickHandlers.length; i++) {
2262
+ try { tickHandlers[i](); } catch (e) {}
2263
+ }
2264
+ }
2265
+
2266
+ try {
2267
+ var workerSrc = "var id=null;onmessage=function(e){if(e.data==='start'){if(id===null)id=setInterval(function(){postMessage(0);}," + TICK_MS + ");}else{clearInterval(id);id=null;}};";
2268
+ var worker = new Worker(URL.createObjectURL(new Blob([workerSrc], { type: "text/javascript" })));
2269
+ worker.onmessage = onTick;
2270
+ worker.postMessage("start");
2271
+ } catch (e) {
2272
+ setInterval(onTick, TICK_MS);
2273
+ }
2274
+
2275
+ /* ---------- requestAnimationFrame ---------- */
2276
+ var nativeRaf = window.requestAnimationFrame && window.requestAnimationFrame.bind(window);
2277
+ var nativeCaf = window.cancelAnimationFrame && window.cancelAnimationFrame.bind(window);
2278
+
2279
+ if (nativeRaf) {
2280
+ var SHIM_BASE = 1000000000; // keep our ids clear of the native ones
2281
+ var nextRafId = SHIM_BASE;
2282
+ var rafQueue = new Map();
2283
+
2284
+ window.requestAnimationFrame = function(cb) {
2285
+ if (!active()) return nativeRaf(cb);
2286
+ var id = nextRafId++;
2287
+ rafQueue.set(id, cb);
2288
+ return id;
2289
+ };
2290
+
2291
+ window.cancelAnimationFrame = function(id) {
2292
+ if (id >= SHIM_BASE) { rafQueue.delete(id); return; }
2293
+ if (nativeCaf) nativeCaf(id);
2294
+ };
2295
+
2296
+ tickHandlers.push(function() {
2297
+ if (rafQueue.size === 0) return;
2298
+ var due = rafQueue;
2299
+ rafQueue = new Map();
2300
+ var stamp = performance.now();
2301
+ due.forEach(function(cb) {
2302
+ try { cb(stamp); } catch (e) {}
2303
+ });
2304
+ });
2305
+ }
2306
+
2307
+ /* ---------- setTimeout / setInterval ---------- */
2308
+ var nativeSetTimeout = window.setTimeout.bind(window);
2309
+ var nativeClearTimeout = window.clearTimeout.bind(window);
2310
+ var nativeSetInterval = window.setInterval.bind(window);
2311
+ var nativeClearInterval = window.clearInterval.bind(window);
2312
+
2313
+ var TIMER_BASE = 2000000000;
2314
+ var nextTimerId = TIMER_BASE;
2315
+ var timers = new Map();
2316
+
2317
+ function schedule(fn, delay, args, repeating) {
2318
+ var wait = Math.max(0, Number(delay) || 0);
2319
+ var id = nextTimerId++;
2320
+ timers.set(id, { fn: fn, args: args, due: performance.now() + wait, every: repeating ? wait : null });
2321
+ return id;
2322
+ }
2323
+
2324
+ window.setTimeout = function(fn, delay) {
2325
+ var args = Array.prototype.slice.call(arguments, 2);
2326
+ if (!active() || typeof fn !== "function") return nativeSetTimeout.apply(window, arguments);
2327
+ return schedule(fn, delay, args, false);
2328
+ };
2329
+
2330
+ window.setInterval = function(fn, delay) {
2331
+ var args = Array.prototype.slice.call(arguments, 2);
2332
+ if (!active() || typeof fn !== "function") return nativeSetInterval.apply(window, arguments);
2333
+ return schedule(fn, delay, args, true);
2334
+ };
2335
+
2336
+ window.clearTimeout = function(id) {
2337
+ if (id >= TIMER_BASE) { timers.delete(id); return; }
2338
+ nativeClearTimeout(id);
2339
+ };
2340
+
2341
+ window.clearInterval = function(id) {
2342
+ if (id >= TIMER_BASE) { timers.delete(id); return; }
2343
+ nativeClearInterval(id);
2344
+ };
2345
+
2346
+ tickHandlers.push(function() {
2347
+ if (timers.size === 0) return;
2348
+ var now = performance.now();
2349
+ timers.forEach(function(timer, id) {
2350
+ if (now < timer.due) return;
2351
+ if (timer.every === null) timers.delete(id);
2352
+ else timer.due = now + timer.every;
2353
+ try { timer.fn.apply(window, timer.args); } catch (e) {}
2354
+ });
2355
+ });
2356
+ })();
2357
+ <\/script>`;
2358
+
2359
+ if (/<\/body>/i.test(html)) return html.replace(/<\/body>/i, `${script}</body>`);
2360
+ if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, `${script}</head>`);
2361
+ return `${html}${script}`;
2362
+ }
2363
+
1671
2364
  function injectAutoclickSupport(html) {
1672
2365
  const clickScript = `
1673
2366
  <script>
@@ -1741,6 +2434,164 @@
1741
2434
  e.preventDefault();
1742
2435
  openGameTab(file, { background: true });
1743
2436
  });
2437
+ el.addEventListener("contextmenu", (e) => {
2438
+ e.preventDefault();
2439
+ openGameContextMenu(e, file);
2440
+ });
2441
+ }
2442
+
2443
+ /* ---------------- right-click menu ---------------- */
2444
+
2445
+ let ctxMenu = null;
2446
+
2447
+ function hideGameContextMenu() {
2448
+ if (ctxMenu) ctxMenu.classList.remove("open");
2449
+ }
2450
+
2451
+ function ensureGameContextMenu() {
2452
+ if (ctxMenu) return ctxMenu;
2453
+
2454
+ ctxMenu = document.createElement("div");
2455
+ ctxMenu.id = "game-context-menu";
2456
+ ctxMenu.setAttribute("role", "menu");
2457
+ document.body.appendChild(ctxMenu);
2458
+
2459
+ // Any interaction outside the menu dismisses it.
2460
+ document.addEventListener("mousedown", (e) => {
2461
+ if (ctxMenu.classList.contains("open") && !ctxMenu.contains(e.target)) hideGameContextMenu();
2462
+ });
2463
+ document.addEventListener("keydown", (e) => {
2464
+ if (e.key === "Escape") hideGameContextMenu();
2465
+ });
2466
+ window.addEventListener("resize", hideGameContextMenu);
2467
+ window.addEventListener("blur", hideGameContextMenu);
2468
+ document.addEventListener("scroll", hideGameContextMenu, true);
2469
+
2470
+ return ctxMenu;
2471
+ }
2472
+
2473
+ function openGameContextMenu(e, file) {
2474
+ const menu = ensureGameContextMenu();
2475
+ menu.innerHTML = "";
2476
+
2477
+ const items = [
2478
+ ["Open", () => openGameTab(file)],
2479
+ ["Open Game in New Subtab", () => openGameTab(file, { background: true })],
2480
+ ["Open Game in New Tab", () => openGameInNewWindow(file)],
2481
+ ];
2482
+
2483
+ items.forEach(([label, action]) => {
2484
+ const item = document.createElement("button");
2485
+ item.type = "button";
2486
+ item.className = "ctx-item";
2487
+ item.setAttribute("role", "menuitem");
2488
+ item.textContent = label;
2489
+ item.onclick = () => { hideGameContextMenu(); action(); };
2490
+ menu.appendChild(item);
2491
+ });
2492
+
2493
+ // Show first so the box has a measurable size, then clamp on-screen.
2494
+ menu.classList.add("open");
2495
+ menu.style.left = "0px";
2496
+ menu.style.top = "0px";
2497
+ const rect = menu.getBoundingClientRect();
2498
+ const x = Math.min(e.clientX, window.innerWidth - rect.width - 8);
2499
+ const y = Math.min(e.clientY, window.innerHeight - rect.height - 8);
2500
+ menu.style.left = `${Math.max(8, x)}px`;
2501
+ menu.style.top = `${Math.max(8, y)}px`;
2502
+
2503
+ const first = menu.querySelector(".ctx-item");
2504
+ if (first) first.focus();
2505
+ }
2506
+
2507
+ /* Plain fetch across the proxy list — the in-tab loader logs its progress
2508
+ into the tab, but a popup has nowhere to put that. */
2509
+ async function fetchGameSource(file) {
2510
+ const encoded = encodeURIComponent(normalizeFileName(file));
2511
+ for (const base of GAME_BASES) {
2512
+ try {
2513
+ const response = await fetch(`${base}${encoded}?t=${Date.now()}`);
2514
+ if (!response.ok) continue;
2515
+ const text = await response.text();
2516
+ const clean = text.trim().toLowerCase();
2517
+ if (clean === "404: not found" || clean === "not found" || clean.includes("404 page not found")) continue;
2518
+ return { text, base };
2519
+ } catch (e) { /* try the next proxy */ }
2520
+ }
2521
+ return null;
2522
+ }
2523
+
2524
+ /* Opens the game in a real browser tab on about:blank, wearing the home
2525
+ tab's title and favicon rather than the game's. */
2526
+ async function openGameInNewWindow(file) {
2527
+ const win = window.open("about:blank", "_blank");
2528
+ if (!win) {
2529
+ alert("The popup was blocked. Allow popups for this page to open games in a new tab.");
2530
+ return;
2531
+ }
2532
+
2533
+ const title = displayNameFromFile(file);
2534
+ const icons = [...document.querySelectorAll("link[rel='icon']")].map(l => l.outerHTML).join("\n");
2535
+
2536
+ win.document.write(`<!DOCTYPE html>
2537
+ <html>
2538
+ <head>
2539
+ <meta charset="utf-8">
2540
+ <title>New tab</title>
2541
+ ${icons}
2542
+ <style>
2543
+ html, body { margin: 0; height: 100%; background: linear-gradient(135deg, #2e3244 30%, #561a86 100%); }
2544
+ iframe { border: 0; display: block; width: 100%; height: 100%; }
2545
+ #loading { display: grid; place-items: center; height: 100%; color: #fff; font-family: 'Comic Sans MS', sans-serif; font-size: 20px; }
2546
+ </style>
2547
+ </head>
2548
+ <body><div id="loading">Loading ${sanitizeTitle(title)}...</div>
2549
+ <script>
2550
+ // Same trick as the main page: re-append the icon links so the browser
2551
+ // picks the favicon back up after the document is written.
2552
+ function refreshFavicon() {
2553
+ const existingIcons = document.querySelectorAll("link[rel='icon']");
2554
+ existingIcons.forEach(oldIcon => {
2555
+ const newIcon = oldIcon.cloneNode(true);
2556
+ oldIcon.remove();
2557
+ document.head.appendChild(newIcon);
2558
+ });
2559
+ }
2560
+ setTimeout(refreshFavicon, 51);
2561
+ <\/script>
2562
+ </body>
2563
+ </html>`);
2564
+ win.document.close();
2565
+
2566
+ // document.write resets the title in some browsers; pin it afterwards.
2567
+ try { win.document.title = "New tab"; } catch (err) { }
2568
+
2569
+ recordRecent(file);
2570
+
2571
+ const source = await fetchGameSource(file);
2572
+ if (win.closed) return;
2573
+
2574
+ const loading = win.document.getElementById("loading");
2575
+
2576
+ if (!source) {
2577
+ if (loading) {
2578
+ loading.textContent = `Failed to load ${sanitizeTitle(title)}.`;
2579
+ loading.style.color = "#ff5555";
2580
+ }
2581
+ return;
2582
+ }
2583
+
2584
+ let html = injectOffTabSupport(injectAutoclickSupport(injectMuteSupport(source.text, isMuted)), bgRunEnabled);
2585
+ html = injectBaseHref(html, source.base);
2586
+
2587
+ if (loading) loading.remove();
2588
+ const iframe = win.document.createElement("iframe");
2589
+ iframe.srcdoc = html;
2590
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-modals");
2591
+ iframe.referrerPolicy = "no-referrer";
2592
+ win.document.body.appendChild(iframe);
2593
+
2594
+ try { win.refreshFavicon && win.setTimeout(win.refreshFavicon, 51); } catch (err) { }
1744
2595
  }
1745
2596
 
1746
2597
  async function openGameTab(file, opts = {}) {
@@ -1821,7 +2672,7 @@
1821
2672
 
1822
2673
  if (!text) throw new Error("All proxy servers failed, were skipped, or the file does not exist.");
1823
2674
 
1824
- let html = injectAutoclickSupport(injectMuteSupport(text, isMuted));
2675
+ let html = injectOffTabSupport(injectAutoclickSupport(injectMuteSupport(text, isMuted)), bgRunEnabled);
1825
2676
  html = injectBaseHref(html, successBase);
1826
2677
 
1827
2678
  tab.panel.innerHTML = "";
@@ -1832,6 +2683,7 @@
1832
2683
  iframe.referrerPolicy = "no-referrer";
1833
2684
  applyMuteToIframe(iframe);
1834
2685
  tab.panel.appendChild(iframe);
2686
+ restoreClickersForTab(id, tab.panel, iframe);
1835
2687
  setTabStatus(id, isTabActive(id) ? null : "ready");
1836
2688
 
1837
2689
  } catch (err) {
@@ -1845,14 +2697,119 @@
1845
2697
  }
1846
2698
  }
1847
2699
 
2700
+ /* Pulls the text of the code's own <title> tag, entities decoded. */
2701
+ function extractDocTitle(htmlCode) {
2702
+ const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(htmlCode || "");
2703
+ if (!match) return "";
2704
+ const decoder = document.createElement("textarea");
2705
+ decoder.innerHTML = match[1];
2706
+ return decoder.value.replace(/\s+/g, " ").trim();
2707
+ }
2708
+
2709
+ function escapeHtmlText(text) {
2710
+ return String(text)
2711
+ .replace(/&/g, "&amp;")
2712
+ .replace(/</g, "&lt;")
2713
+ .replace(/>/g, "&gt;")
2714
+ .replace(/"/g, "&quot;");
2715
+ }
2716
+
2717
+ /* The home tab's own favicon links, as markup. */
2718
+ function homeIconsHtml() {
2719
+ return [...document.querySelectorAll("link[rel='icon']")].map(l => l.outerHTML).join("\n");
2720
+ }
2721
+
2722
+ /* Icon links declared in the code. Relative hrefs are dropped: the popup
2723
+ lives on about:blank, so they would resolve to nothing. */
2724
+ function extractIconLinksHtml(htmlCode) {
2725
+ const links = [];
2726
+ const tagPattern = /<link\b[^>]*>/gi;
2727
+ let tag;
2728
+ while ((tag = tagPattern.exec(htmlCode || "")) !== null) {
2729
+ const text = tag[0];
2730
+ const rel = /\brel\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(text);
2731
+ const relValue = rel ? (rel[2] ?? rel[3] ?? rel[4] ?? "") : "";
2732
+ if (!/\bicon\b/i.test(relValue)) continue;
2733
+ const href = /\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(text);
2734
+ const hrefValue = href ? (href[2] ?? href[3] ?? href[4] ?? "").trim() : "";
2735
+ if (!/^(data:|https?:|\/\/)/i.test(hrefValue)) continue;
2736
+ links.push(text);
2737
+ }
2738
+ return links.join("\n");
2739
+ }
2740
+
2741
+ /* Runs the typed HTML in a real browser tab. With cloaking on (or when the
2742
+ code declares no title/icon of its own) the tab wears the home page's
2743
+ title and favicon instead. */
2744
+ function openHtmlInNewWindow(htmlCode, keepCloaking) {
2745
+ const win = window.open("about:blank", "_blank");
2746
+ if (!win) {
2747
+ alert("The popup was blocked. Allow popups for this page to open code in a new tab.");
2748
+ return;
2749
+ }
2750
+
2751
+ const codeTitle = keepCloaking ? "" : extractDocTitle(htmlCode);
2752
+ const codeIcons = keepCloaking ? "" : extractIconLinksHtml(htmlCode);
2753
+ const title = codeTitle || "New tab";
2754
+ const icons = codeIcons || homeIconsHtml();
2755
+
2756
+ const prepared = injectBaseHref(
2757
+ injectOffTabSupport(injectAutoclickSupport(injectMuteSupport(htmlCode, isMuted)), bgRunEnabled),
2758
+ document.baseURI
2759
+ );
2760
+
2761
+ win.document.write(`<!DOCTYPE html>
2762
+ <html>
2763
+ <head>
2764
+ <meta charset="utf-8">
2765
+ <title>${escapeHtmlText(title)}</title>
2766
+ ${icons}
2767
+ <style>
2768
+ html, body { margin: 0; height: 100%; background: #fff; }
2769
+ iframe { border: 0; display: block; width: 100%; height: 100%; }
2770
+ </style>
2771
+ </head>
2772
+ <body>
2773
+ <script>
2774
+ // Same trick as the main page: re-append the icon links so the browser
2775
+ // picks the favicon back up after the document is written.
2776
+ function refreshFavicon() {
2777
+ const existingIcons = document.querySelectorAll("link[rel='icon']");
2778
+ existingIcons.forEach(oldIcon => {
2779
+ const newIcon = oldIcon.cloneNode(true);
2780
+ oldIcon.remove();
2781
+ document.head.appendChild(newIcon);
2782
+ });
2783
+ }
2784
+ setTimeout(refreshFavicon, 51);
2785
+ <\/script>
2786
+ </body>
2787
+ </html>`);
2788
+ win.document.close();
2789
+
2790
+ // document.write resets the title in some browsers; pin it afterwards.
2791
+ try { win.document.title = title; } catch (err) { }
2792
+
2793
+ const iframe = win.document.createElement("iframe");
2794
+ iframe.srcdoc = prepared;
2795
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-modals");
2796
+ iframe.referrerPolicy = "no-referrer";
2797
+ win.document.body.appendChild(iframe);
2798
+
2799
+ try { win.refreshFavicon && win.setTimeout(win.refreshFavicon, 51); } catch (err) { }
2800
+ }
2801
+
1848
2802
  function openHtmlPreviewTab(htmlCode) {
1849
2803
  const id = "html-preview";
1850
- const title = "HTML Code";
2804
+ const title = extractDocTitle(htmlCode) || "HTML Code";
1851
2805
 
1852
2806
  const prepared = injectBaseHref(injectAutoclickSupport(injectMuteSupport(htmlCode, isMuted)), document.baseURI);
1853
2807
 
1854
2808
  if (tabs.has(id)) {
1855
2809
  const existing = tabs.get(id);
2810
+ existing.title = title;
2811
+ const label = existing.button.querySelector(".tab-title");
2812
+ if (label) label.textContent = sanitizeTitle(title);
1856
2813
  existing.panel.innerHTML = "";
1857
2814
  const iframe = document.createElement("iframe");
1858
2815
  iframe.className = "game-frame";
@@ -1894,82 +2851,6 @@
1894
2851
  activateTab(id);
1895
2852
  }
1896
2853
 
1897
- function renderRecentPane() {
1898
- const list = document.getElementById("recent-list");
1899
- if (!list) return;
1900
-
1901
- list.innerHTML = "";
1902
-
1903
- const count = document.getElementById("recent-count");
1904
-
1905
- if (recent.length === 0) {
1906
- list.style.display = "block";
1907
- if (count) count.textContent = " ";
1908
- const empty = document.createElement("div");
1909
- empty.className = "recent-empty";
1910
- empty.textContent = "No games played yet";
1911
- list.appendChild(empty);
1912
- return;
1913
- }
1914
-
1915
- list.style.display = "";
1916
- if (count) count.textContent = ` `;
1917
-
1918
- recent.forEach((file, i) => {
1919
- const card = document.createElement("button");
1920
- card.className = "recent-card";
1921
- card.dataset.file = file;
1922
- bindGameLauncher(card, file);
1923
-
1924
- const rank = document.createElement("span");
1925
- rank.className = "recent-rank";
1926
- rank.textContent = i + 1;
1927
-
1928
- const name = document.createElement("span");
1929
- name.className = "recent-name";
1930
- name.textContent = displayNameFromFile(file);
1931
- name.title = name.textContent;
1932
-
1933
- card.appendChild(rank);
1934
- card.appendChild(name);
1935
- list.appendChild(card);
1936
- });
1937
- }
1938
-
1939
- function openRecentGamesTab() {
1940
- const id = "recent-games";
1941
- const title = "Recent Games";
1942
-
1943
- if (tabs.has(id)) { activateTab(id); renderRecentPane(); return; }
1944
-
1945
- const button = createTabButton(id, title, true);
1946
- tabBar.insertBefore(button, tabBarAnchor());
1947
-
1948
- const panel = document.createElement("div");
1949
- panel.className = "panel game-pane static-pane";
1950
- panel.id = `panel-${id}`;
1951
-
1952
- const wrapper = document.createElement("div");
1953
- wrapper.className = "recent-pane";
1954
- wrapper.innerHTML = `
1955
- <div class="recent-inner">
1956
- <h2 class="recent-header">Recently Played</h2>
1957
- <p class="recent-subheader" id="recent-count"></p>
1958
- <div class="recent-list" id="recent-list"></div>
1959
- </div>
1960
- `;
1961
-
1962
- panel.appendChild(wrapper);
1963
- contentArea.appendChild(panel);
1964
-
1965
- const tab = { id, title, button, panel };
1966
- tabs.set(id, tab);
1967
- tabOrder.push(id);
1968
- activateTab(id);
1969
-
1970
- renderRecentPane();
1971
- }
1972
-
1973
2854
  function openRunnerTab() {
1974
2855
  const id = "runner";
1975
2856
  const title = "HTML Runner";
@@ -2004,9 +2885,14 @@
2004
2885
  &lt;/body&gt;
2005
2886
  &lt;/html&gt;</textarea>
2006
2887
  <div class="runner-controls">
2007
- <button id="runner-run-btn">Open in New Tab</button>
2888
+ <button id="runner-run-btn">Open in New Subtab</button>
2889
+ <button id="runner-newtab-btn">Open in New Tab</button>
2008
2890
  <button id="runner-clear-btn">Clear</button>
2009
2891
  </div>
2892
+ <label class="runner-check" for="runner-cloak">
2893
+ <input type="checkbox" id="runner-cloak" checked>
2894
+ <span>Keep Cloaking</span>
2895
+ </label>
2010
2896
  </div>
2011
2897
  </div>
2012
2898
  `;
@@ -2021,12 +2907,18 @@
2021
2907
 
2022
2908
  const code = panel.querySelector("#runner-code");
2023
2909
  const runBtn = panel.querySelector("#runner-run-btn");
2910
+ const newTabBtn = panel.querySelector("#runner-newtab-btn");
2911
+ const cloakBox = panel.querySelector("#runner-cloak");
2024
2912
  const clearBtn = panel.querySelector("#runner-clear-btn");
2025
2913
 
2026
2914
  runBtn.onclick = () => {
2027
2915
  openHtmlPreviewTab(code.value);
2028
2916
  };
2029
2917
 
2918
+ newTabBtn.onclick = () => {
2919
+ openHtmlInNewWindow(code.value, cloakBox.checked);
2920
+ };
2921
+
2030
2922
  clearBtn.onclick = () => {
2031
2923
  code.value = "";
2032
2924
  };
@@ -2052,6 +2944,7 @@
2052
2944
  <textarea class="runner-textarea" placeholder="Paste your bookmarklet javascript: here..."></textarea>
2053
2945
  <div class="runner-controls">
2054
2946
  <button class="run-btn">Run Bookmarklet</button>
2947
+ <button class="clear-btn">Clear</button>
2055
2948
  </div>
2056
2949
  </div>
2057
2950
  </div>
@@ -2059,6 +2952,11 @@
2059
2952
 
2060
2953
  const textarea = tab.panel.querySelector(".runner-textarea");
2061
2954
  const runBtn = tab.panel.querySelector(".run-btn");
2955
+ const clearBtn = tab.panel.querySelector(".clear-btn");
2956
+
2957
+ clearBtn.addEventListener("click", () => {
2958
+ textarea.value = "";
2959
+ });
2062
2960
 
2063
2961
  runBtn.addEventListener("click", () => {
2064
2962
  let code = textarea.value.trim();
@@ -2134,13 +3032,44 @@
2134
3032
  const container = document.getElementById("sections-container");
2135
3033
  const firstRealSection = container.firstElementChild;
2136
3034
  container.insertBefore(section, firstRealSection);
3035
+
3036
+ filterGames();
3037
+ }
3038
+
3039
+ function renderRecentSection() {
3040
+ const container = document.getElementById("sections-container");
3041
+ if (!container) return;
3042
+
3043
+ const existing = document.getElementById("section-recent");
3044
+ if (existing) existing.remove();
3045
+
3046
+ const recentSidebarBtn = document.getElementById("sidebar-recent");
3047
+ if (recentSidebarBtn) recentSidebarBtn.style.opacity = recent.length > 0 ? "1" : "0.3";
3048
+
3049
+ if (recent.length === 0) return;
3050
+
3051
+ const section = document.createElement("div");
3052
+ section.className = "letter-section"; section.id = "section-recent";
3053
+
3054
+ const header = document.createElement("div");
3055
+ header.className = "letter-header"; header.textContent = "Recently Played"; section.appendChild(header);
3056
+
3057
+ const buttonsContainer = document.createElement("div");
3058
+ buttonsContainer.className = "buttons-container";
3059
+ recent.forEach(file => buttonsContainer.appendChild(makeGameRow(file)));
3060
+ section.appendChild(buttonsContainer);
3061
+
3062
+ const fav = document.getElementById("section-fav");
3063
+ container.insertBefore(section, fav ? fav.nextElementSibling : container.firstElementChild);
3064
+
3065
+ filterGames();
2137
3066
  }
2138
3067
 
2139
3068
  function generateSidebar(allChars, filesByChar) {
2140
3069
  const sidebar = document.getElementById("sidebar"); sidebar.innerHTML = "";
2141
3070
 
2142
3071
  const searchBtn = document.createElement("button");
2143
- searchBtn.className = "sidebar-btn"; searchBtn.textContent = "Search";
3072
+ searchBtn.className = "sidebar-btn"; searchBtn.textContent = "search";
2144
3073
  searchBtn.onclick = () => document.getElementById("gameSearch").scrollIntoView({ behavior: "smooth", block: "end" });
2145
3074
  sidebar.appendChild(searchBtn);
2146
3075
 
@@ -2153,6 +3082,15 @@
2153
3082
  };
2154
3083
  sidebar.appendChild(favBtn);
2155
3084
 
3085
+ const recentBtn = document.createElement("button");
3086
+ recentBtn.className = "sidebar-btn"; recentBtn.id = "sidebar-recent"; recentBtn.textContent = "recent";
3087
+ recentBtn.style.opacity = recent.length > 0 ? "1" : "0.3";
3088
+ recentBtn.onclick = () => {
3089
+ const section = document.getElementById("section-recent");
3090
+ if (section) section.scrollIntoView({ behavior: "smooth", block: "start" });
3091
+ };
3092
+ sidebar.appendChild(recentBtn);
3093
+
2156
3094
  allChars.forEach((char) => {
2157
3095
  if (char === "Search") return;
2158
3096
  const btn = document.createElement("button");
@@ -2200,11 +3138,12 @@
2200
3138
  section.appendChild(buttonsContainer); container.appendChild(section);
2201
3139
  });
2202
3140
 
2203
- generateSidebar(sidebarChars, filesByChar); renderFavoritesSection();
3141
+ generateSidebar(sidebarChars, filesByChar); renderFavoritesSection(); renderRecentSection();
2204
3142
  }
2205
3143
 
2206
3144
  function filterGames() {
2207
3145
  const input = document.getElementById("gameSearch");
3146
+ if (!input) return;
2208
3147
  const filter = input.value.toLowerCase();
2209
3148
  const sections = document.querySelectorAll(".letter-section");
2210
3149
 
@@ -2226,7 +3165,6 @@
2226
3165
  }
2227
3166
 
2228
3167
  document.getElementById("open-runner-btn").addEventListener("click", openRunnerTab);
2229
- document.getElementById("open-recent-btn").addEventListener("click", openRecentGamesTab);
2230
3168
 
2231
3169
  async function fetchAndInjectProxy(targetUrl, tab, title, id) {
2232
3170
  const logArea = tab.panel.querySelector(`#logs-${id}`);
@@ -2361,6 +3299,13 @@
2361
3299
 
2362
3300
  document.getElementById("open-form-btn").addEventListener("click", openFormTab);
2363
3301
 
3302
+ // Ask the browser to show its native "Leave site?" prompt on close/reload.
3303
+ window.addEventListener("beforeunload", (e) => {
3304
+ e.preventDefault();
3305
+ e.returnValue = "";
3306
+ return "";
3307
+ });
3308
+
2364
3309
  function initWhenReady() {
2365
3310
  if (typeof files === "undefined") {
2366
3311
  setTimeout(initWhenReady, 50); return;