@okjavis/nodebb-theme-javis 2.1.0 → 2.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okjavis/nodebb-theme-javis",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Modern, premium NodeBB theme for JAVIS Community - Extends Harmony with custom styling",
5
5
  "main": "theme.js",
6
6
  "scripts": {
package/scss/_topic.scss CHANGED
@@ -33,15 +33,15 @@ body.template-topic {
33
33
  margin-bottom: 0;
34
34
  display: flex;
35
35
  align-items: flex-start;
36
- gap: $jv-space-4;
36
+ gap: $jv-space-3;
37
37
 
38
38
  h1,
39
39
  [component="topic/title"],
40
40
  .topic-title {
41
- font-size: 32px;
42
- font-weight: 800;
43
- line-height: 1.2;
44
- letter-spacing: -0.03em;
41
+ font-size: $jv-font-size-xl;
42
+ font-weight: 700;
43
+ line-height: $jv-line-height-tight;
44
+ letter-spacing: -0.02em;
45
45
  color: $jv-text-main;
46
46
  margin-bottom: 0;
47
47
  flex: 1;
@@ -49,10 +49,10 @@ body.template-topic {
49
49
  }
50
50
 
51
51
  h1.fs-3 {
52
- font-size: 32px !important;
53
- font-weight: 800 !important;
54
- line-height: 1.2 !important;
55
- letter-spacing: -0.03em !important;
52
+ font-size: $jv-font-size-xl !important;
53
+ font-weight: 700 !important;
54
+ line-height: $jv-line-height-tight !important;
55
+ letter-spacing: -0.02em !important;
56
56
  }
57
57
 
58
58
  // ===========================================================
@@ -111,23 +111,77 @@ body.template-topic {
111
111
  width: 100%;
112
112
  }
113
113
 
114
+ // Remove timeline start/end dots from posts list
115
+ .posts.timeline::before,
116
+ .posts.timeline::after {
117
+ display: none !important;
118
+ }
119
+
114
120
  [component="post"] {
115
- background: $jv-surface;
116
- border-radius: $jv-radius-md;
117
- margin-bottom: $jv-space-4;
118
- border: 1px solid $jv-border-subtle;
121
+ background: transparent;
122
+ margin-bottom: $jv-space-3;
123
+ border: none;
119
124
  box-shadow: none;
120
125
 
126
+ // Remove timeline vertical line from Harmony theme
127
+ border-left: none !important;
128
+
129
+ // Remove timeline end dot
130
+ &:last-child::after {
131
+ display: none !important;
132
+ }
133
+
134
+ // Remove all timeline pseudo-elements (dots at start/end)
135
+ &::before,
136
+ &::after {
137
+ display: none !important;
138
+ }
139
+
140
+ // Hide user status indicator (green/grey dot below avatar)
141
+ [component="user/status"] {
142
+ display: none !important;
143
+ }
144
+
145
+ // Card styling on the parent container that includes avatar
146
+ .post-container-parent {
147
+ background: $jv-surface;
148
+ border-radius: $jv-radius-md;
149
+ border: 1px solid $jv-border-subtle;
150
+ padding: $jv-space-3;
151
+ gap: $jv-space-2 !important; // Tighter gap
152
+
153
+ // Reduce avatar size from 48px to 36px
154
+ > .bg-body {
155
+ // Align avatar container with the post header (username/timestamp line)
156
+ align-self: flex-start;
157
+ margin-top: 2px; // Fine-tune to vertically center with text
158
+
159
+ [component="user/picture"],
160
+ .avatar {
161
+ width: 36px !important;
162
+ height: 36px !important;
163
+ font-size: $jv-font-size-sm !important;
164
+ line-height: 36px !important;
165
+ }
166
+ }
167
+ }
168
+
169
+ // Align the post header row with avatar
170
+ .post-header {
171
+ min-height: 36px; // Match avatar height
172
+ align-items: center !important;
173
+ margin-bottom: 0 !important;
174
+ }
175
+
121
176
  .post-container {
122
- padding: $jv-space-6;
177
+ padding: 0;
123
178
  padding-top: 0 !important;
124
179
  border: none;
125
180
  background: transparent;
126
181
  }
127
182
 
128
- &.selected .post-container {
183
+ &.selected .post-container-parent {
129
184
  background: $jv-selected-bg;
130
- border-radius: $jv-radius-md;
131
185
  }
132
186
  }
133
187
  }
@@ -138,13 +192,13 @@ body.template-topic {
138
192
  .post-header {
139
193
  display: flex;
140
194
  align-items: center;
141
- gap: $jv-space-3;
142
- margin-bottom: $jv-space-4;
143
- font-size: $jv-font-size-sm;
195
+ gap: $jv-space-2;
196
+ margin-bottom: $jv-space-3;
197
+ font-size: $jv-font-size-xs;
144
198
 
145
199
  .avatar {
146
- width: 40px;
147
- height: 40px;
200
+ width: 32px;
201
+ height: 32px;
148
202
  border-radius: 50%;
149
203
  object-fit: cover;
150
204
  flex-shrink: 0;
@@ -153,12 +207,13 @@ body.template-topic {
153
207
  .user-info {
154
208
  display: flex;
155
209
  flex-direction: column;
156
- gap: 2px;
210
+ gap: $jv-space-1;
157
211
  }
158
212
 
159
213
  .username,
160
214
  [component="post/header/username"] {
161
215
  font-weight: 600;
216
+ font-size: $jv-font-size-sm;
162
217
  color: $jv-text-main;
163
218
  text-decoration: none;
164
219
 
@@ -195,20 +250,20 @@ body.template-topic {
195
250
  color: $jv-text-main;
196
251
 
197
252
  p {
198
- margin-bottom: $jv-space-4;
253
+ margin-bottom: $jv-space-3;
199
254
  &:last-child { margin-bottom: 0; }
200
255
  }
201
256
 
202
257
  pre, code {
203
258
  background: rgba(0, 0, 0, 0.04);
204
259
  border-radius: $jv-radius-sm;
205
- font-size: $jv-font-size-sm;
260
+ font-size: $jv-font-size-xs;
206
261
  }
207
262
 
208
- code { padding: 2px 6px; }
263
+ code { padding: 2px 5px; }
209
264
 
210
265
  pre {
211
- padding: $jv-space-4;
266
+ padding: $jv-space-3;
212
267
  overflow-x: auto;
213
268
  code {
214
269
  padding: 0;
@@ -229,6 +284,95 @@ body.template-topic {
229
284
  border-radius: $jv-radius-sm;
230
285
  }
231
286
 
287
+ // ===========================================================
288
+ // POST IMAGE CAROUSEL
289
+ // ===========================================================
290
+ .post-image-carousel {
291
+ border-radius: $jv-radius-md;
292
+ overflow: hidden;
293
+ margin: $jv-space-4 0;
294
+ background: rgba(0, 0, 0, 0.03);
295
+
296
+ .carousel-inner {
297
+ border-radius: $jv-radius-md;
298
+ }
299
+
300
+ .carousel-item {
301
+ // Fixed height container to prevent jumping
302
+ height: 400px;
303
+
304
+ img {
305
+ width: 100%;
306
+ height: 100%;
307
+ object-fit: contain;
308
+ background: rgba(0, 0, 0, 0.02);
309
+ }
310
+ }
311
+
312
+ // Navigation arrows
313
+ .carousel-control-prev,
314
+ .carousel-control-next {
315
+ width: 48px;
316
+ height: 48px;
317
+ top: 50%;
318
+ transform: translateY(-50%);
319
+ background: rgba(0, 0, 0, 0.5);
320
+ border-radius: 50%;
321
+ opacity: 0;
322
+ transition: opacity $jv-transition-fast;
323
+
324
+ &:hover {
325
+ background: rgba(0, 0, 0, 0.7);
326
+ }
327
+
328
+ .carousel-control-prev-icon,
329
+ .carousel-control-next-icon {
330
+ width: 20px;
331
+ height: 20px;
332
+ }
333
+ }
334
+
335
+ .carousel-control-prev {
336
+ left: $jv-space-3;
337
+ }
338
+
339
+ .carousel-control-next {
340
+ right: $jv-space-3;
341
+ }
342
+
343
+ &:hover {
344
+ .carousel-control-prev,
345
+ .carousel-control-next {
346
+ opacity: 1;
347
+ }
348
+ }
349
+
350
+ // Indicator dots
351
+ .carousel-indicators {
352
+ margin-bottom: $jv-space-3;
353
+ gap: $jv-space-2;
354
+
355
+ button {
356
+ width: 8px;
357
+ height: 8px;
358
+ border-radius: 50%;
359
+ background: rgba(255, 255, 255, 0.5);
360
+ border: none;
361
+ opacity: 1;
362
+ transition: background-color $jv-transition-fast, transform $jv-transition-fast;
363
+
364
+ &.active {
365
+ background: #fff;
366
+ transform: scale(1.2);
367
+ }
368
+
369
+ &:hover {
370
+ background: rgba(255, 255, 255, 0.8);
371
+ }
372
+ }
373
+ }
374
+ }
375
+
232
376
  a {
233
377
  color: $jv-primary;
234
378
  &:hover { text-decoration: underline; }
@@ -593,42 +737,283 @@ body.template-topic {
593
737
  }
594
738
 
595
739
  // ===========================================================
596
- // NESTED REPLIES
740
+ // PARENT POST CONTEXT (shown when viewing a reply)
597
741
  // ===========================================================
598
- [component="post/replies/container"] {
599
- margin-top: $jv-space-4;
600
- padding-left: $jv-space-6;
601
- border-left: 2px solid $jv-border-subtle;
742
+ [component="post/parent"] {
743
+ background: $jv-surface !important;
744
+ border: 1px solid $jv-border-subtle !important;
745
+ border-left: 3px solid $jv-primary !important;
746
+ border-radius: $jv-radius-sm !important;
747
+ padding: $jv-space-3 !important;
748
+ margin-bottom: $jv-space-3 !important;
749
+ transition: background-color $jv-transition-fast, border-color $jv-transition-fast;
602
750
 
603
- [component="post"] {
604
- margin-bottom: $jv-space-3;
605
- padding: $jv-space-4;
606
- background: rgba(0, 0, 0, 0.02);
607
- border-radius: $jv-radius-sm;
751
+ &:hover {
752
+ background: $jv-hover-bg !important;
753
+ border-color: $jv-border-strong !important;
754
+ border-left-color: $jv-primary-hover !important;
755
+ }
756
+
757
+ // Header row with avatar and metadata
758
+ > .d-flex:first-child {
759
+ gap: $jv-space-2 !important;
760
+ margin-bottom: $jv-space-2;
761
+
762
+ // Small avatar
763
+ .avatar,
764
+ [component="user/picture"] {
765
+ width: 20px !important;
766
+ height: 20px !important;
767
+ border-radius: 50%;
768
+ flex-shrink: 0;
769
+ }
770
+
771
+ // Username and time
772
+ .text-nowrap,
773
+ .fw-bold {
774
+ font-size: $jv-font-size-xs !important;
775
+ font-weight: 600;
776
+ color: $jv-text-main;
777
+ }
608
778
 
609
- .post-container { padding: 0; }
779
+ .text-muted {
780
+ font-size: $jv-font-size-xs !important;
781
+ color: $jv-text-soft !important;
782
+ }
783
+ }
784
+
785
+ // Parent content preview
786
+ [component="post/parent/content"] {
787
+ font-size: $jv-font-size-sm !important;
788
+ color: $jv-text-muted !important;
789
+ line-height: $jv-line-height-base;
790
+ display: -webkit-box;
791
+ -webkit-line-clamp: 2;
792
+ -webkit-box-orient: vertical;
793
+ overflow: hidden;
794
+
795
+ p {
796
+ margin: 0;
797
+ }
610
798
  }
611
799
  }
612
800
 
613
801
  // ===========================================================
614
- // REPLY COUNT ACCORDION
802
+ // REPLY COUNT BUTTON (Accordion trigger)
615
803
  // ===========================================================
616
804
  [component="post/reply-count"] {
617
- color: $jv-text-muted;
618
- font-size: $jv-font-size-sm;
619
- padding: $jv-space-2 $jv-space-3;
620
- border-radius: $jv-radius-sm;
805
+ display: inline-flex !important;
806
+ align-items: center;
807
+ gap: $jv-space-2;
808
+ background: $jv-surface !important;
809
+ border: 1px solid $jv-border-subtle !important;
810
+ border-radius: $jv-radius-pill !important;
811
+ padding: $jv-space-1 $jv-space-3 !important;
812
+ font-size: $jv-font-size-xs !important;
813
+ color: $jv-text-muted !important;
621
814
  cursor: pointer;
622
- transition: background-color $jv-transition-fast, color $jv-transition-fast;
815
+ transition: all $jv-transition-fast;
623
816
 
624
817
  &:hover {
625
- background: $jv-hover-bg;
626
- color: $jv-text-main;
818
+ background: $jv-hover-bg !important;
819
+ border-color: $jv-border-strong !important;
820
+ color: $jv-text-main !important;
627
821
  }
628
822
 
629
- .replies-count {
630
- color: $jv-text-main;
823
+ // Avatar stack
824
+ [component="post/reply-count/avatars"] {
825
+ display: flex;
826
+ align-items: center;
827
+
828
+ span {
829
+ margin-left: -4px;
830
+
831
+ &:first-child {
832
+ margin-left: 0;
833
+ }
834
+ }
835
+
836
+ .avatar,
837
+ img {
838
+ width: 18px !important;
839
+ height: 18px !important;
840
+ border-radius: 50%;
841
+ border: 2px solid $jv-surface;
842
+ }
843
+ }
844
+
845
+ // Reply count text
846
+ .replies-count,
847
+ [component="post/reply-count/text"] {
631
848
  font-weight: 600;
849
+ color: $jv-text-main;
850
+ }
851
+
852
+ // Last reply time
853
+ .replies-last {
854
+ color: $jv-text-soft;
855
+ font-size: $jv-font-size-xs;
856
+ }
857
+
858
+ // Chevron icon
859
+ .fa-chevron-down {
860
+ font-size: 10px;
861
+ color: $jv-text-soft;
862
+ transition: transform $jv-transition-fast;
863
+ }
864
+
865
+ // Expanded state
866
+ &[aria-expanded="true"] {
867
+ .fa-chevron-down {
868
+ transform: rotate(180deg);
869
+ }
870
+ }
871
+ }
872
+
873
+ // ===========================================================
874
+ // NESTED REPLIES CONTAINER (Threaded style)
875
+ // ===========================================================
876
+ [component="post/replies/container"] {
877
+ margin-top: $jv-space-3 !important;
878
+ margin-bottom: 0 !important;
879
+ margin-left: $jv-space-6;
880
+ padding: 0 !important;
881
+ background: transparent !important;
882
+ border: none !important;
883
+ border-radius: 0 !important;
884
+ position: relative;
885
+
886
+ // Thread line
887
+ &::before {
888
+ content: "";
889
+ position: absolute;
890
+ left: -$jv-space-4;
891
+ top: 0;
892
+ bottom: $jv-space-4;
893
+ width: 2px;
894
+ background: $jv-border-subtle;
895
+ border-radius: 1px;
896
+ }
897
+
898
+ // Replies list
899
+ [component="post/replies"],
900
+ ul.list-unstyled {
901
+ margin: 0;
902
+ padding: 0;
903
+ display: flex;
904
+ flex-direction: column;
905
+ gap: $jv-space-3;
906
+ }
907
+
908
+ // Individual reply posts
909
+ [component="post"] {
910
+ margin: 0 !important;
911
+ padding: 0 !important;
912
+ background: transparent !important;
913
+ position: relative;
914
+
915
+ // Thread connector dot
916
+ &::before {
917
+ content: "";
918
+ position: absolute;
919
+ left: -$jv-space-4 - 3px;
920
+ top: $jv-space-4;
921
+ width: 8px;
922
+ height: 8px;
923
+ background: $jv-surface;
924
+ border: 2px solid $jv-border-strong;
925
+ border-radius: 50%;
926
+ z-index: 1;
927
+ }
928
+
929
+ // Reply card
930
+ .post-container-parent {
931
+ background: $jv-surface !important;
932
+ border: 1px solid $jv-border-subtle !important;
933
+ border-radius: $jv-radius-sm !important;
934
+ padding: $jv-space-3 !important;
935
+ gap: $jv-space-2 !important;
936
+ transition: border-color $jv-transition-fast, box-shadow $jv-transition-fast;
937
+
938
+ &:hover {
939
+ border-color: $jv-border-strong !important;
940
+ box-shadow: $jv-shadow-sm;
941
+ }
942
+
943
+ // Smaller avatar for replies
944
+ > .bg-body,
945
+ > div:first-child {
946
+ .avatar,
947
+ [component="user/picture"] {
948
+ width: 28px !important;
949
+ height: 28px !important;
950
+ }
951
+ }
952
+ }
953
+
954
+ // Post content area
955
+ .post-container {
956
+ padding: 0 !important;
957
+ background: transparent !important;
958
+ border: none !important;
959
+ }
960
+
961
+ // Reply header
962
+ .post-header {
963
+ font-size: $jv-font-size-xs !important;
964
+ min-height: auto !important;
965
+ margin-bottom: $jv-space-1 !important;
966
+
967
+ .fw-bold,
968
+ a[data-username] {
969
+ font-size: $jv-font-size-xs !important;
970
+ font-weight: 600;
971
+ }
972
+
973
+ .text-muted {
974
+ font-size: $jv-font-size-xs !important;
975
+ }
976
+
977
+ // Hide post index in replies
978
+ .post-index {
979
+ display: none !important;
980
+ }
981
+ }
982
+
983
+ // Reply content
984
+ [component="post/content"] {
985
+ font-size: $jv-font-size-sm !important;
986
+ line-height: $jv-line-height-base;
987
+ }
988
+
989
+ // Reply footer/actions
990
+ [component="post/footer"],
991
+ .post-footer {
992
+ margin-top: $jv-space-2 !important;
993
+ padding-top: $jv-space-2 !important;
994
+ padding-bottom: 0 !important;
995
+ border-top: 1px solid $jv-border-subtle !important;
996
+ border-bottom: none !important;
997
+ }
998
+
999
+ [component="post/actions"] {
1000
+ gap: $jv-space-1 !important;
1001
+
1002
+ .btn,
1003
+ .btn-ghost {
1004
+ padding: $jv-space-1 $jv-space-2 !important;
1005
+ font-size: $jv-font-size-xs !important;
1006
+
1007
+ i {
1008
+ font-size: 14px !important;
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ // Last reply - no bottom spacing on thread line
1014
+ &:last-child::before {
1015
+ display: block !important;
1016
+ }
632
1017
  }
633
1018
  }
634
1019
  }
@@ -666,7 +1051,7 @@ body.template-topic {
666
1051
  body.template-topic {
667
1052
  .topic-header h1,
668
1053
  .topic-header [component="topic/title"] {
669
- font-size: 22px;
1054
+ font-size: $jv-font-size-xl;
670
1055
  }
671
1056
 
672
1057
  .quick-reply,
@@ -674,8 +1059,35 @@ body.template-topic {
674
1059
  padding: $jv-space-4;
675
1060
  }
676
1061
 
1062
+ // Mobile adjustments for threaded replies
677
1063
  [component="post/replies/container"] {
678
- padding-left: $jv-space-4;
1064
+ margin-left: $jv-space-4;
1065
+
1066
+ &::before {
1067
+ left: -$jv-space-3;
1068
+ }
1069
+
1070
+ [component="post"] {
1071
+ &::before {
1072
+ left: -$jv-space-3 - 3px;
1073
+ }
1074
+ }
1075
+ }
1076
+
1077
+ // Mobile adjustments for parent context
1078
+ [component="post/parent"] {
1079
+ padding: $jv-space-2 !important;
1080
+
1081
+ [component="post/parent/content"] {
1082
+ -webkit-line-clamp: 1;
1083
+ }
1084
+ }
1085
+
1086
+ // Mobile reply count button
1087
+ [component="post/reply-count"] {
1088
+ .replies-last {
1089
+ display: none !important;
1090
+ }
679
1091
  }
680
1092
  }
681
1093
  }
@@ -6,14 +6,186 @@
6
6
  (function() {
7
7
  'use strict';
8
8
 
9
+ var carouselCounter = 0;
10
+
9
11
  // Theme initialization
10
12
  $(document).ready(function() {
11
13
  console.log('JAVIS Community Theme initialized');
12
14
 
13
15
  // Ensure sidebar toggle works (reinitialize if Harmony's handler isn't loaded)
14
16
  initSidebarToggle();
17
+
18
+ // Initialize image carousels for posts with multiple images
19
+ initPostImageCarousels();
20
+
21
+ // Fix bookmark alert - only show when bookmark is meaningfully ahead
22
+ fixBookmarkAlert();
23
+
24
+ // Re-initialize carousels when new posts are loaded (infinite scroll, etc.)
25
+ // Also handle post edits by clearing the processed flag
26
+ $(window).on('action:posts.loaded action:topic.loaded action:ajaxify.end', function() {
27
+ initPostImageCarousels();
28
+ });
29
+
30
+ // Handle post edits - need to re-process the edited post
31
+ $(window).on('action:posts.edited', function(ev, data) {
32
+ if (data && data.post && data.post.pid) {
33
+ // Find the edited post and remove the processed flag so it gets re-scanned
34
+ var $post = $('[data-pid="' + data.post.pid + '"]');
35
+ var $content = $post.find('[component="post/content"]');
36
+ $content.removeAttr('data-carousel-processed');
37
+ // Remove any existing carousel in this post
38
+ $content.find('.post-image-carousel').remove();
39
+ // Re-initialize
40
+ initPostImageCarousels();
41
+ }
42
+ });
15
43
  });
16
44
 
45
+ /**
46
+ * Convert multiple images in post content to Bootstrap carousels
47
+ */
48
+ function initPostImageCarousels() {
49
+ // Find all post content areas that haven't been processed
50
+ $('[component="post/content"]:not([data-carousel-processed])').each(function() {
51
+ var $content = $(this);
52
+ $content.attr('data-carousel-processed', 'true');
53
+
54
+ // Find all images in post content - including those in links (lightbox) and paragraphs
55
+ var $images = $content.find('img').filter(function() {
56
+ var $img = $(this);
57
+
58
+ // Exclude images already in a carousel
59
+ if ($img.closest('.carousel, .post-image-carousel').length) {
60
+ return false;
61
+ }
62
+
63
+ // Exclude emojis and small icons by class
64
+ if ($img.hasClass('emoji') || $img.hasClass('emoji-img') || $img.hasClass('icon') || $img.hasClass('not-responsive')) {
65
+ return false;
66
+ }
67
+
68
+ // Check the image source to determine if it's a content image
69
+ var src = $img.attr('src') || '';
70
+
71
+ // Include images from uploads folder or with image extensions
72
+ var isContentImage = src.indexOf('/assets/uploads/') !== -1 ||
73
+ src.indexOf('/files/') !== -1 ||
74
+ src.match(/\.(jpg|jpeg|png|gif|webp)(\?|$)/i);
75
+
76
+ if (!isContentImage) {
77
+ return false;
78
+ }
79
+
80
+ // Exclude tiny images by checking width/height attributes if present
81
+ var width = $img.attr('width');
82
+ var height = $img.attr('height');
83
+ if ((width && parseInt(width, 10) < 50) || (height && parseInt(height, 10) < 50)) {
84
+ return false;
85
+ }
86
+
87
+ return true;
88
+ });
89
+
90
+ // Only create carousel if 2+ images
91
+ if ($images.length >= 2) {
92
+ createCarousel($content, $images);
93
+ console.log('JAVIS: Found ' + $images.length + ' images, creating carousel');
94
+ } else if ($images.length > 0) {
95
+ console.log('JAVIS: Found ' + $images.length + ' image(s), not enough for carousel');
96
+ }
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Create a Bootstrap 5 carousel from a set of images
102
+ */
103
+ function createCarousel($content, $images) {
104
+ var carouselId = 'post-carousel-' + (++carouselCounter);
105
+
106
+ // Build carousel HTML
107
+ var carouselHtml = '<div id="' + carouselId + '" class="carousel slide post-image-carousel" data-bs-ride="false">';
108
+
109
+ // Indicators (dots)
110
+ carouselHtml += '<div class="carousel-indicators">';
111
+ $images.each(function(index) {
112
+ var activeClass = index === 0 ? 'active' : '';
113
+ var ariaCurrent = index === 0 ? 'aria-current="true"' : '';
114
+ carouselHtml += '<button type="button" data-bs-target="#' + carouselId + '" data-bs-slide-to="' + index + '" class="' + activeClass + '" ' + ariaCurrent + ' aria-label="Slide ' + (index + 1) + '"></button>';
115
+ });
116
+ carouselHtml += '</div>';
117
+
118
+ // Carousel inner (slides)
119
+ carouselHtml += '<div class="carousel-inner">';
120
+ $images.each(function(index) {
121
+ var $img = $(this);
122
+ var src = $img.attr('src');
123
+ var alt = $img.attr('alt') || 'Image ' + (index + 1);
124
+ var activeClass = index === 0 ? 'active' : '';
125
+ carouselHtml += '<div class="carousel-item ' + activeClass + '">';
126
+ carouselHtml += '<img src="' + src + '" class="d-block w-100" alt="' + alt + '" loading="lazy">';
127
+ carouselHtml += '</div>';
128
+ });
129
+ carouselHtml += '</div>';
130
+
131
+ // Navigation arrows
132
+ carouselHtml += '<button class="carousel-control-prev" type="button" data-bs-target="#' + carouselId + '" data-bs-slide="prev">';
133
+ carouselHtml += '<span class="carousel-control-prev-icon" aria-hidden="true"></span>';
134
+ carouselHtml += '<span class="visually-hidden">Previous</span>';
135
+ carouselHtml += '</button>';
136
+ carouselHtml += '<button class="carousel-control-next" type="button" data-bs-target="#' + carouselId + '" data-bs-slide="next">';
137
+ carouselHtml += '<span class="carousel-control-next-icon" aria-hidden="true"></span>';
138
+ carouselHtml += '<span class="visually-hidden">Next</span>';
139
+ carouselHtml += '</button>';
140
+
141
+ carouselHtml += '</div>';
142
+
143
+ // Find the topmost element to insert carousel before
144
+ // Images can be in: <p><img></p>, <p><a><img></a></p>, <a><img></a>, or just <img>
145
+ var $firstImg = $images.first();
146
+ var $insertBefore = $firstImg;
147
+
148
+ // Walk up to find the direct child of $content
149
+ while ($insertBefore.parent().length && !$insertBefore.parent().is($content)) {
150
+ $insertBefore = $insertBefore.parent();
151
+ }
152
+
153
+ // Insert carousel before the first image's container
154
+ $insertBefore.before(carouselHtml);
155
+
156
+ // Collect elements to remove (images and their empty containers)
157
+ var elementsToRemove = [];
158
+ $images.each(function() {
159
+ var $img = $(this);
160
+ var $element = $img;
161
+
162
+ // Walk up to find the direct child of $content
163
+ while ($element.parent().length && !$element.parent().is($content)) {
164
+ $element = $element.parent();
165
+ }
166
+
167
+ // Mark for removal if not already marked
168
+ if (elementsToRemove.indexOf($element[0]) === -1) {
169
+ elementsToRemove.push($element[0]);
170
+ }
171
+ });
172
+
173
+ // Remove the original image containers
174
+ $(elementsToRemove).remove();
175
+
176
+ // Initialize Bootstrap carousel
177
+ var carouselEl = document.getElementById(carouselId);
178
+ if (carouselEl && typeof bootstrap !== 'undefined') {
179
+ new bootstrap.Carousel(carouselEl, {
180
+ interval: false, // Don't auto-slide
181
+ touch: true,
182
+ wrap: true
183
+ });
184
+ }
185
+
186
+ console.log('JAVIS: Created carousel with ' + $images.length + ' images');
187
+ }
188
+
17
189
  function initSidebarToggle() {
18
190
  // Check if the toggle element exists
19
191
  var toggleEl = $('[component="sidebar/toggle"]');
@@ -51,4 +223,47 @@
51
223
  console.log('JAVIS: Sidebar toggle initialized');
52
224
  }
53
225
 
226
+ /**
227
+ * Fix the bookmark alert bug - NodeBB shows "Click here to return to last read post"
228
+ * even when the bookmark position isn't meaningfully ahead of the current position.
229
+ *
230
+ * The bug: NodeBB checks if bookmark exists and postcount > threshold, but doesn't
231
+ * check if the bookmark is actually ahead of where the user currently is.
232
+ *
233
+ * This fix removes the bookmark alert if:
234
+ * 1. The bookmark is at position 1 or 2 (meaningless to "return" to the start)
235
+ * 2. The bookmark is at or behind the current post index
236
+ */
237
+ function fixBookmarkAlert() {
238
+ $(window).on('action:topic.loaded', function() {
239
+ // Small delay to let NodeBB's handleBookmark run first
240
+ setTimeout(function() {
241
+ if (typeof ajaxify === 'undefined' || !ajaxify.data || !ajaxify.data.template || !ajaxify.data.template.topic) {
242
+ return;
243
+ }
244
+
245
+ require(['storage', 'alerts'], function(storage, alerts) {
246
+ var tid = ajaxify.data.tid;
247
+ var bookmark = ajaxify.data.bookmark || storage.getItem('topic:' + tid + ':bookmark');
248
+ var postIndex = ajaxify.data.postIndex || 1;
249
+ var bookmarkInt = parseInt(bookmark, 10) || 0;
250
+ var postIndexInt = parseInt(postIndex, 10) || 1;
251
+
252
+ // Remove bookmark alert if:
253
+ // 1. No meaningful bookmark (position 1 or 2 - essentially the start)
254
+ // 2. Bookmark is at or behind current position (nothing to "return" to)
255
+ // 3. Bookmark is only 1-2 posts ahead (not worth showing notification)
256
+ var shouldRemoveAlert = bookmarkInt <= 2 ||
257
+ bookmarkInt <= postIndexInt ||
258
+ (bookmarkInt - postIndexInt) <= 2;
259
+
260
+ if (shouldRemoveAlert) {
261
+ alerts.remove('bookmark');
262
+ console.log('JAVIS: Removed unnecessary bookmark alert (bookmark: ' + bookmarkInt + ', current: ' + postIndexInt + ')');
263
+ }
264
+ });
265
+ }, 100);
266
+ });
267
+ }
268
+
54
269
  })();