@okjavis/nodebb-theme-javis 2.1.0 → 2.3.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.3.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,75 @@ 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 (extends base .post-header)
170
+ .post-header {
171
+ min-height: 36px; // Match avatar height for alignment
172
+ }
173
+
121
174
  .post-container {
122
- padding: $jv-space-6;
175
+ padding: 0;
123
176
  padding-top: 0 !important;
124
177
  border: none;
125
178
  background: transparent;
126
179
  }
127
180
 
128
- &.selected .post-container {
181
+ &.selected .post-container-parent {
129
182
  background: $jv-selected-bg;
130
- border-radius: $jv-radius-md;
131
183
  }
132
184
  }
133
185
  }
@@ -137,14 +189,14 @@ body.template-topic {
137
189
  // ===========================================================
138
190
  .post-header {
139
191
  display: flex;
140
- align-items: center;
141
- gap: $jv-space-3;
142
- margin-bottom: $jv-space-4;
143
- font-size: $jv-font-size-sm;
192
+ align-items: center !important;
193
+ gap: $jv-space-2;
194
+ margin-bottom: 0 !important;
195
+ font-size: $jv-font-size-xs;
144
196
 
145
197
  .avatar {
146
- width: 40px;
147
- height: 40px;
198
+ width: 32px;
199
+ height: 32px;
148
200
  border-radius: 50%;
149
201
  object-fit: cover;
150
202
  flex-shrink: 0;
@@ -153,12 +205,13 @@ body.template-topic {
153
205
  .user-info {
154
206
  display: flex;
155
207
  flex-direction: column;
156
- gap: 2px;
208
+ gap: $jv-space-1;
157
209
  }
158
210
 
159
211
  .username,
160
212
  [component="post/header/username"] {
161
213
  font-weight: 600;
214
+ font-size: $jv-font-size-sm;
162
215
  color: $jv-text-main;
163
216
  text-decoration: none;
164
217
 
@@ -195,20 +248,20 @@ body.template-topic {
195
248
  color: $jv-text-main;
196
249
 
197
250
  p {
198
- margin-bottom: $jv-space-4;
251
+ margin-bottom: $jv-space-3;
199
252
  &:last-child { margin-bottom: 0; }
200
253
  }
201
254
 
202
255
  pre, code {
203
256
  background: rgba(0, 0, 0, 0.04);
204
257
  border-radius: $jv-radius-sm;
205
- font-size: $jv-font-size-sm;
258
+ font-size: $jv-font-size-xs;
206
259
  }
207
260
 
208
- code { padding: 2px 6px; }
261
+ code { padding: 2px 5px; }
209
262
 
210
263
  pre {
211
- padding: $jv-space-4;
264
+ padding: $jv-space-3;
212
265
  overflow-x: auto;
213
266
  code {
214
267
  padding: 0;
@@ -229,6 +282,95 @@ body.template-topic {
229
282
  border-radius: $jv-radius-sm;
230
283
  }
231
284
 
285
+ // ===========================================================
286
+ // POST IMAGE CAROUSEL
287
+ // ===========================================================
288
+ .post-image-carousel {
289
+ border-radius: $jv-radius-md;
290
+ overflow: hidden;
291
+ margin: $jv-space-4 0;
292
+ background: rgba(0, 0, 0, 0.03);
293
+
294
+ .carousel-inner {
295
+ border-radius: $jv-radius-md;
296
+ }
297
+
298
+ .carousel-item {
299
+ // Fixed height container to prevent jumping
300
+ height: 400px;
301
+
302
+ img {
303
+ width: 100%;
304
+ height: 100%;
305
+ object-fit: contain;
306
+ background: rgba(0, 0, 0, 0.02);
307
+ }
308
+ }
309
+
310
+ // Navigation arrows
311
+ .carousel-control-prev,
312
+ .carousel-control-next {
313
+ width: 48px;
314
+ height: 48px;
315
+ top: 50%;
316
+ transform: translateY(-50%);
317
+ background: rgba(0, 0, 0, 0.5);
318
+ border-radius: 50%;
319
+ opacity: 0;
320
+ transition: opacity $jv-transition-fast;
321
+
322
+ &:hover {
323
+ background: rgba(0, 0, 0, 0.7);
324
+ }
325
+
326
+ .carousel-control-prev-icon,
327
+ .carousel-control-next-icon {
328
+ width: 20px;
329
+ height: 20px;
330
+ }
331
+ }
332
+
333
+ .carousel-control-prev {
334
+ left: $jv-space-3;
335
+ }
336
+
337
+ .carousel-control-next {
338
+ right: $jv-space-3;
339
+ }
340
+
341
+ &:hover {
342
+ .carousel-control-prev,
343
+ .carousel-control-next {
344
+ opacity: 1;
345
+ }
346
+ }
347
+
348
+ // Indicator dots
349
+ .carousel-indicators {
350
+ margin-bottom: $jv-space-3;
351
+ gap: $jv-space-2;
352
+
353
+ button {
354
+ width: 8px;
355
+ height: 8px;
356
+ border-radius: 50%;
357
+ background: rgba(255, 255, 255, 0.5);
358
+ border: none;
359
+ opacity: 1;
360
+ transition: background-color $jv-transition-fast, transform $jv-transition-fast;
361
+
362
+ &.active {
363
+ background: #fff;
364
+ transform: scale(1.2);
365
+ }
366
+
367
+ &:hover {
368
+ background: rgba(255, 255, 255, 0.8);
369
+ }
370
+ }
371
+ }
372
+ }
373
+
232
374
  a {
233
375
  color: $jv-primary;
234
376
  &:hover { text-decoration: underline; }
@@ -300,10 +442,32 @@ body.template-topic {
300
442
  }
301
443
  }
302
444
 
303
- // Always visible post actions (override Harmony's hover behavior)
304
- .topic .posts [component="post"] [component="post/actions"] {
445
+ // Post actions - show on hover (Reddit/HN style)
446
+ // Uses JS-managed .post-hovered class to prevent bubble to parent posts
447
+ .topic .posts [component="post"] {
448
+ [component="post/actions"] {
449
+ opacity: 0;
450
+ transition: opacity $jv-transition-fast;
451
+ }
452
+
453
+ // Show actions only on the directly hovered post (JS adds .post-hovered class)
454
+ > .post-container-parent.post-hovered [component="post/actions"] {
455
+ opacity: 1;
456
+ }
457
+
458
+ // Always show if user has interacted (voted, bookmarked)
459
+ [component="post/actions"] {
460
+ .upvoted,
461
+ .downvoted,
462
+ .bookmarked {
463
+ opacity: 1;
464
+ }
465
+ }
466
+ }
467
+
468
+ // Keep vote count always visible when there are votes
469
+ .topic .posts [component="post/vote-count"]:not(:empty) {
305
470
  opacity: 1 !important;
306
- visibility: visible !important;
307
471
  }
308
472
 
309
473
  // ===========================================================
@@ -448,9 +612,21 @@ body.template-topic {
448
612
  .dropdown-toggle { display: none; }
449
613
  }
450
614
 
451
- // Consistent ghost button styling for all sidebar action buttons
615
+ // Sidebar action buttons - full-width, left-aligned (extends base .btn-ghost)
452
616
  .btn-ghost,
453
617
  .bottom-sheet > .btn,
618
+ .thread-tools > .dropdown-toggle {
619
+ text-align: left;
620
+ width: 100%;
621
+ justify-content: flex-start;
622
+
623
+ i {
624
+ width: 16px; // Fixed width for icon alignment
625
+ }
626
+ }
627
+
628
+ // Non-ghost buttons need base ghost styling too
629
+ .bottom-sheet > .btn,
454
630
  .thread-tools > .dropdown-toggle {
455
631
  background: transparent !important;
456
632
  border: 1px solid transparent !important;
@@ -459,15 +635,10 @@ body.template-topic {
459
635
  border-radius: $jv-radius-sm;
460
636
  font-size: $jv-font-size-sm;
461
637
  font-weight: 500;
462
- text-align: left;
463
- width: 100%;
464
- justify-content: flex-start;
465
638
  transition: background-color $jv-transition-fast, color $jv-transition-fast;
466
-
467
- i {
468
- width: 16px;
469
- color: $jv-primary;
470
- }
639
+ display: inline-flex;
640
+ align-items: center;
641
+ gap: $jv-space-2;
471
642
 
472
643
  &:hover {
473
644
  background: $jv-hover-bg !important;
@@ -593,42 +764,310 @@ body.template-topic {
593
764
  }
594
765
 
595
766
  // ===========================================================
596
- // NESTED REPLIES
767
+ // PARENT POST CONTEXT (shown when viewing a reply)
768
+ // Compact inline style - visually distinct from main posts
597
769
  // ===========================================================
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;
770
+ [component="post/parent"] {
771
+ background: transparent !important;
772
+ border: none !important;
773
+ border-left: 2px solid $jv-border-strong !important;
774
+ border-radius: 0 !important;
775
+ padding: $jv-space-1 0 $jv-space-1 $jv-space-3 !important;
776
+ margin-bottom: $jv-space-2 !important;
777
+ margin-left: $jv-space-1 !important;
778
+ transition: border-color $jv-transition-fast;
779
+ cursor: pointer;
780
+ display: flex !important;
781
+ align-items: center !important;
782
+ gap: $jv-space-2 !important;
783
+ flex-direction: row !important;
602
784
 
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;
785
+ &:hover {
786
+ border-left-color: $jv-primary !important;
787
+
788
+ .fw-semibold {
789
+ color: $jv-primary !important;
790
+ }
791
+ }
792
+
793
+ // Avatar
794
+ .avatar,
795
+ [component="user/picture"] {
796
+ width: 16px !important;
797
+ height: 16px !important;
798
+ border-radius: 50%;
799
+ flex-shrink: 0;
800
+ }
801
+
802
+ // Username
803
+ .fw-semibold {
804
+ font-size: $jv-font-size-xs !important;
805
+ font-weight: 600;
806
+ color: $jv-text-muted;
807
+ transition: color $jv-transition-fast;
808
+ }
809
+
810
+ // Parent content preview - inline, truncated
811
+ [component="post/parent/content"] {
812
+ font-size: $jv-font-size-xs !important;
813
+ color: $jv-text-soft !important;
814
+ line-height: 1.4;
815
+ white-space: nowrap;
816
+ overflow: hidden;
817
+ text-overflow: ellipsis;
818
+ flex: 1;
819
+ min-width: 0;
820
+
821
+ p {
822
+ margin: 0;
823
+ display: inline;
824
+ }
825
+
826
+ // Hide images in preview
827
+ img {
828
+ display: none;
829
+ }
830
+ }
608
831
 
609
- .post-container { padding: 0; }
832
+ // Hide the nested flex container structure, flatten it
833
+ > .d-flex {
834
+ display: contents !important;
610
835
  }
611
836
  }
612
837
 
613
838
  // ===========================================================
614
- // REPLY COUNT ACCORDION
839
+ // POST HIGHLIGHT ANIMATION (when navigating to a post)
840
+ // ===========================================================
841
+ .post-highlight-flash {
842
+ animation: postHighlightFlash 1.5s ease-out;
843
+ }
844
+
845
+ @keyframes postHighlightFlash {
846
+ 0% {
847
+ background-color: rgba($jv-primary, 0.15);
848
+ box-shadow: 0 0 0 4px rgba($jv-primary, 0.1);
849
+ }
850
+ 100% {
851
+ background-color: $jv-surface;
852
+ box-shadow: none;
853
+ }
854
+ }
855
+
856
+ // ===========================================================
857
+ // REPLY COUNT BUTTON (Reddit/Discord style - no border, text link)
615
858
  // ===========================================================
616
859
  [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;
860
+ display: inline-flex;
861
+ align-items: center;
862
+ gap: $jv-space-2;
863
+ background: transparent !important;
864
+ border: none !important;
865
+ border-radius: $jv-radius-sm !important;
866
+ padding: $jv-space-1 $jv-space-2 !important;
867
+ font-size: $jv-font-size-xs !important;
868
+ color: $jv-text-muted !important;
621
869
  cursor: pointer;
622
- transition: background-color $jv-transition-fast, color $jv-transition-fast;
870
+ transition: all $jv-transition-fast;
871
+ text-decoration: none !important;
872
+
873
+ // Respect the hidden class when there are no replies
874
+ &.hidden {
875
+ display: none !important;
876
+ }
623
877
 
624
878
  &:hover {
625
- background: $jv-hover-bg;
626
- color: $jv-text-main;
879
+ background: $jv-hover-bg !important;
880
+ color: $jv-primary !important;
881
+
882
+ [component="post/reply-count/text"],
883
+ .replies-count {
884
+ color: $jv-primary !important;
885
+ }
627
886
  }
628
887
 
629
- .replies-count {
630
- color: $jv-text-main;
888
+ // Hide avatars - cleaner look like Reddit
889
+ [component="post/reply-count/avatars"] {
890
+ display: none !important;
891
+ }
892
+
893
+ // Reply count text - simple and clean
894
+ .replies-count,
895
+ [component="post/reply-count/text"] {
631
896
  font-weight: 600;
897
+ font-size: $jv-font-size-xs;
898
+ color: $jv-text-muted;
899
+ transition: color $jv-transition-fast;
900
+ }
901
+
902
+ // Hide last reply time - too verbose
903
+ .replies-last {
904
+ display: none !important;
905
+ }
906
+
907
+ // Chevron icon
908
+ .fa-chevron-down {
909
+ font-size: 10px;
910
+ color: $jv-text-soft;
911
+ transition: transform $jv-transition-fast;
912
+ }
913
+
914
+ // Expanded state
915
+ &[aria-expanded="true"] {
916
+ color: $jv-primary !important;
917
+
918
+ .fa-chevron-down {
919
+ transform: rotate(180deg);
920
+ }
921
+
922
+ [component="post/reply-count/text"],
923
+ .replies-count {
924
+ color: $jv-primary !important;
925
+ }
926
+ }
927
+ }
928
+
929
+ // ===========================================================
930
+ // NESTED REPLIES CONTAINER (Clean threaded style)
931
+ // Inspired by Reddit/GitHub - compact, minimal
932
+ // ===========================================================
933
+ [component="post/replies/container"] {
934
+ margin-top: $jv-space-3 !important;
935
+ margin-bottom: 0 !important;
936
+ margin-left: $jv-space-4;
937
+ padding: 0 !important;
938
+ background: transparent !important;
939
+ border: none !important;
940
+ border-radius: 0 !important;
941
+ position: relative;
942
+
943
+ // Simple thread line
944
+ &::before {
945
+ content: "";
946
+ position: absolute;
947
+ left: -$jv-space-2;
948
+ top: 0;
949
+ bottom: 0;
950
+ width: 2px;
951
+ background: $jv-border-subtle;
952
+ border-radius: 1px;
953
+ transition: background $jv-transition-fast;
954
+ }
955
+
956
+ // Hover effect on thread line
957
+ &:hover::before {
958
+ background: $jv-border-strong;
959
+ }
960
+
961
+ // Replies list - compact spacing
962
+ [component="post/replies"],
963
+ ul.list-unstyled {
964
+ margin: 0;
965
+ padding: 0;
966
+ display: flex;
967
+ flex-direction: column;
968
+ gap: $jv-space-2;
969
+ }
970
+
971
+ // Individual reply posts - clean, compact
972
+ [component="post"] {
973
+ margin: 0 !important;
974
+ padding: 0 !important;
975
+ background: transparent !important;
976
+ position: relative;
977
+
978
+ // No connector dots
979
+ &::before {
980
+ display: none !important;
981
+ }
982
+
983
+ // Reply container - compact padding
984
+ .post-container-parent {
985
+ background: transparent !important;
986
+ border: none !important;
987
+ border-radius: $jv-radius-sm !important;
988
+ padding: $jv-space-2 $jv-space-3 !important;
989
+ margin-left: 0;
990
+ margin-right: 0;
991
+ gap: $jv-space-2 !important;
992
+ transition: background $jv-transition-fast;
993
+
994
+ &:hover {
995
+ background: $jv-hover-bg !important;
996
+ }
997
+
998
+ // Smaller avatar for replies
999
+ > .bg-body,
1000
+ > div:first-child {
1001
+ .avatar,
1002
+ [component="user/picture"] {
1003
+ width: 20px !important;
1004
+ height: 20px !important;
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ // Post content area
1010
+ .post-container {
1011
+ padding: 0 !important;
1012
+ background: transparent !important;
1013
+ border: none !important;
1014
+ }
1015
+
1016
+ // Reply header - inline, compact
1017
+ .post-header {
1018
+ font-size: $jv-font-size-xs !important;
1019
+ min-height: auto !important;
1020
+ margin-bottom: 0 !important;
1021
+ display: flex;
1022
+ align-items: center;
1023
+ gap: $jv-space-2;
1024
+
1025
+ .fw-bold,
1026
+ a[data-username] {
1027
+ font-size: $jv-font-size-xs !important;
1028
+ font-weight: 600;
1029
+ color: $jv-text-main;
1030
+ }
1031
+
1032
+ .text-muted {
1033
+ font-size: $jv-font-size-xs !important;
1034
+ }
1035
+
1036
+ // Hide post index in replies
1037
+ .post-index {
1038
+ display: none !important;
1039
+ }
1040
+ }
1041
+
1042
+ // Reply content - tighter
1043
+ [component="post/content"] {
1044
+ font-size: $jv-font-size-sm !important;
1045
+ line-height: $jv-line-height-base;
1046
+ color: $jv-text-main;
1047
+ margin-top: $jv-space-1;
1048
+ }
1049
+
1050
+ // Reply footer - override main post styles (no border, tighter spacing)
1051
+ [component="post/footer"],
1052
+ .post-footer {
1053
+ margin-top: $jv-space-1 !important;
1054
+ border-top: none !important;
1055
+ }
1056
+
1057
+ // Smaller action buttons for compact reply layout (inherits opacity/hover behavior from main rule)
1058
+ [component="post/actions"] {
1059
+ gap: $jv-space-1 !important;
1060
+
1061
+ .btn,
1062
+ .btn-ghost {
1063
+ padding: $jv-space-1 $jv-space-2 !important;
1064
+ font-size: $jv-font-size-xs !important;
1065
+
1066
+ i {
1067
+ font-size: 12px !important;
1068
+ }
1069
+ }
1070
+ }
632
1071
  }
633
1072
  }
634
1073
  }
@@ -666,7 +1105,7 @@ body.template-topic {
666
1105
  body.template-topic {
667
1106
  .topic-header h1,
668
1107
  .topic-header [component="topic/title"] {
669
- font-size: 22px;
1108
+ font-size: $jv-font-size-xl;
670
1109
  }
671
1110
 
672
1111
  .quick-reply,
@@ -674,8 +1113,25 @@ body.template-topic {
674
1113
  padding: $jv-space-4;
675
1114
  }
676
1115
 
1116
+ // Mobile adjustments for threaded replies
677
1117
  [component="post/replies/container"] {
678
- padding-left: $jv-space-4;
1118
+ margin-left: $jv-space-3;
1119
+
1120
+ &::before {
1121
+ left: -$jv-space-2;
1122
+ }
1123
+
1124
+ [component="post"] .post-container-parent {
1125
+ padding: $jv-space-2 $jv-space-3 !important;
1126
+ margin-left: 0;
1127
+ margin-right: 0;
1128
+ }
1129
+ }
1130
+
1131
+ // Mobile adjustments for parent context
1132
+ [component="post/parent"] {
1133
+ padding: $jv-space-1 0 $jv-space-1 $jv-space-2 !important;
1134
+ margin-left: 0 !important;
679
1135
  }
680
1136
  }
681
1137
  }
@@ -6,14 +6,194 @@
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
+ // Initialize parent post click navigation with smooth scroll
25
+ initParentPostNavigation();
26
+
27
+ // Initialize post hover actions (show actions only on directly hovered post)
28
+ initPostHoverActions();
29
+
30
+ // Re-initialize carousels when new posts are loaded (infinite scroll, etc.)
31
+ // Also handle post edits by clearing the processed flag
32
+ $(window).on('action:posts.loaded action:topic.loaded action:ajaxify.end', function() {
33
+ initPostImageCarousels();
34
+ initParentPostNavigation();
35
+ initPostHoverActions();
36
+ });
37
+
38
+ // Handle post edits - need to re-process the edited post
39
+ $(window).on('action:posts.edited', function(ev, data) {
40
+ if (data && data.post && data.post.pid) {
41
+ // Find the edited post and remove the processed flag so it gets re-scanned
42
+ var $post = $('[data-pid="' + data.post.pid + '"]');
43
+ var $content = $post.find('[component="post/content"]');
44
+ $content.removeAttr('data-carousel-processed');
45
+ // Remove any existing carousel in this post
46
+ $content.find('.post-image-carousel').remove();
47
+ // Re-initialize
48
+ initPostImageCarousels();
49
+ }
50
+ });
15
51
  });
16
52
 
53
+ /**
54
+ * Convert multiple images in post content to Bootstrap carousels
55
+ */
56
+ function initPostImageCarousels() {
57
+ // Find all post content areas that haven't been processed
58
+ $('[component="post/content"]:not([data-carousel-processed])').each(function() {
59
+ var $content = $(this);
60
+ $content.attr('data-carousel-processed', 'true');
61
+
62
+ // Find all images in post content - including those in links (lightbox) and paragraphs
63
+ var $images = $content.find('img').filter(function() {
64
+ var $img = $(this);
65
+
66
+ // Exclude images already in a carousel
67
+ if ($img.closest('.carousel, .post-image-carousel').length) {
68
+ return false;
69
+ }
70
+
71
+ // Exclude emojis and small icons by class
72
+ if ($img.hasClass('emoji') || $img.hasClass('emoji-img') || $img.hasClass('icon') || $img.hasClass('not-responsive')) {
73
+ return false;
74
+ }
75
+
76
+ // Check the image source to determine if it's a content image
77
+ var src = $img.attr('src') || '';
78
+
79
+ // Include images from uploads folder or with image extensions
80
+ var isContentImage = src.indexOf('/assets/uploads/') !== -1 ||
81
+ src.indexOf('/files/') !== -1 ||
82
+ src.match(/\.(jpg|jpeg|png|gif|webp)(\?|$)/i);
83
+
84
+ if (!isContentImage) {
85
+ return false;
86
+ }
87
+
88
+ // Exclude tiny images by checking width/height attributes if present
89
+ var width = $img.attr('width');
90
+ var height = $img.attr('height');
91
+ if ((width && parseInt(width, 10) < 50) || (height && parseInt(height, 10) < 50)) {
92
+ return false;
93
+ }
94
+
95
+ return true;
96
+ });
97
+
98
+ // Only create carousel if 2+ images
99
+ if ($images.length >= 2) {
100
+ createCarousel($content, $images);
101
+ console.log('JAVIS: Found ' + $images.length + ' images, creating carousel');
102
+ } else if ($images.length > 0) {
103
+ console.log('JAVIS: Found ' + $images.length + ' image(s), not enough for carousel');
104
+ }
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Create a Bootstrap 5 carousel from a set of images
110
+ */
111
+ function createCarousel($content, $images) {
112
+ var carouselId = 'post-carousel-' + (++carouselCounter);
113
+
114
+ // Build carousel HTML
115
+ var carouselHtml = '<div id="' + carouselId + '" class="carousel slide post-image-carousel" data-bs-ride="false">';
116
+
117
+ // Indicators (dots)
118
+ carouselHtml += '<div class="carousel-indicators">';
119
+ $images.each(function(index) {
120
+ var activeClass = index === 0 ? 'active' : '';
121
+ var ariaCurrent = index === 0 ? 'aria-current="true"' : '';
122
+ carouselHtml += '<button type="button" data-bs-target="#' + carouselId + '" data-bs-slide-to="' + index + '" class="' + activeClass + '" ' + ariaCurrent + ' aria-label="Slide ' + (index + 1) + '"></button>';
123
+ });
124
+ carouselHtml += '</div>';
125
+
126
+ // Carousel inner (slides)
127
+ carouselHtml += '<div class="carousel-inner">';
128
+ $images.each(function(index) {
129
+ var $img = $(this);
130
+ var src = $img.attr('src');
131
+ var alt = $img.attr('alt') || 'Image ' + (index + 1);
132
+ var activeClass = index === 0 ? 'active' : '';
133
+ carouselHtml += '<div class="carousel-item ' + activeClass + '">';
134
+ carouselHtml += '<img src="' + src + '" class="d-block w-100" alt="' + alt + '" loading="lazy">';
135
+ carouselHtml += '</div>';
136
+ });
137
+ carouselHtml += '</div>';
138
+
139
+ // Navigation arrows
140
+ carouselHtml += '<button class="carousel-control-prev" type="button" data-bs-target="#' + carouselId + '" data-bs-slide="prev">';
141
+ carouselHtml += '<span class="carousel-control-prev-icon" aria-hidden="true"></span>';
142
+ carouselHtml += '<span class="visually-hidden">Previous</span>';
143
+ carouselHtml += '</button>';
144
+ carouselHtml += '<button class="carousel-control-next" type="button" data-bs-target="#' + carouselId + '" data-bs-slide="next">';
145
+ carouselHtml += '<span class="carousel-control-next-icon" aria-hidden="true"></span>';
146
+ carouselHtml += '<span class="visually-hidden">Next</span>';
147
+ carouselHtml += '</button>';
148
+
149
+ carouselHtml += '</div>';
150
+
151
+ // Find the topmost element to insert carousel before
152
+ // Images can be in: <p><img></p>, <p><a><img></a></p>, <a><img></a>, or just <img>
153
+ var $firstImg = $images.first();
154
+ var $insertBefore = $firstImg;
155
+
156
+ // Walk up to find the direct child of $content
157
+ while ($insertBefore.parent().length && !$insertBefore.parent().is($content)) {
158
+ $insertBefore = $insertBefore.parent();
159
+ }
160
+
161
+ // Insert carousel before the first image's container
162
+ $insertBefore.before(carouselHtml);
163
+
164
+ // Collect elements to remove (images and their empty containers)
165
+ var elementsToRemove = [];
166
+ $images.each(function() {
167
+ var $img = $(this);
168
+ var $element = $img;
169
+
170
+ // Walk up to find the direct child of $content
171
+ while ($element.parent().length && !$element.parent().is($content)) {
172
+ $element = $element.parent();
173
+ }
174
+
175
+ // Mark for removal if not already marked
176
+ if (elementsToRemove.indexOf($element[0]) === -1) {
177
+ elementsToRemove.push($element[0]);
178
+ }
179
+ });
180
+
181
+ // Remove the original image containers
182
+ $(elementsToRemove).remove();
183
+
184
+ // Initialize Bootstrap carousel
185
+ var carouselEl = document.getElementById(carouselId);
186
+ if (carouselEl && typeof bootstrap !== 'undefined') {
187
+ new bootstrap.Carousel(carouselEl, {
188
+ interval: false, // Don't auto-slide
189
+ touch: true,
190
+ wrap: true
191
+ });
192
+ }
193
+
194
+ console.log('JAVIS: Created carousel with ' + $images.length + ' images');
195
+ }
196
+
17
197
  function initSidebarToggle() {
18
198
  // Check if the toggle element exists
19
199
  var toggleEl = $('[component="sidebar/toggle"]');
@@ -51,4 +231,143 @@
51
231
  console.log('JAVIS: Sidebar toggle initialized');
52
232
  }
53
233
 
234
+ /**
235
+ * Fix the bookmark alert bug - NodeBB shows "Click here to return to last read post"
236
+ * even when the bookmark position isn't meaningfully ahead of the current position.
237
+ *
238
+ * The bug: NodeBB checks if bookmark exists and postcount > threshold, but doesn't
239
+ * check if the bookmark is actually ahead of where the user currently is.
240
+ *
241
+ * This fix removes the bookmark alert if:
242
+ * 1. The bookmark is at position 1 or 2 (meaningless to "return" to the start)
243
+ * 2. The bookmark is at or behind the current post index
244
+ */
245
+ function fixBookmarkAlert() {
246
+ $(window).on('action:topic.loaded', function() {
247
+ // Small delay to let NodeBB's handleBookmark run first
248
+ setTimeout(function() {
249
+ if (typeof ajaxify === 'undefined' || !ajaxify.data || !ajaxify.data.template || !ajaxify.data.template.topic) {
250
+ return;
251
+ }
252
+
253
+ require(['storage', 'alerts'], function(storage, alerts) {
254
+ var tid = ajaxify.data.tid;
255
+ var bookmark = ajaxify.data.bookmark || storage.getItem('topic:' + tid + ':bookmark');
256
+ var postIndex = ajaxify.data.postIndex || 1;
257
+ var bookmarkInt = parseInt(bookmark, 10) || 0;
258
+ var postIndexInt = parseInt(postIndex, 10) || 1;
259
+
260
+ // Remove bookmark alert if:
261
+ // 1. No meaningful bookmark (position 1 or 2 - essentially the start)
262
+ // 2. Bookmark is at or behind current position (nothing to "return" to)
263
+ // 3. Bookmark is only 1-2 posts ahead (not worth showing notification)
264
+ var shouldRemoveAlert = bookmarkInt <= 2 ||
265
+ bookmarkInt <= postIndexInt ||
266
+ (bookmarkInt - postIndexInt) <= 2;
267
+
268
+ if (shouldRemoveAlert) {
269
+ alerts.remove('bookmark');
270
+ console.log('JAVIS: Removed unnecessary bookmark alert (bookmark: ' + bookmarkInt + ', current: ' + postIndexInt + ')');
271
+ }
272
+ });
273
+ }, 100);
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Initialize parent post click navigation with smooth scroll
279
+ * Clicking the "Replying to" component scrolls to the parent post
280
+ */
281
+ function initParentPostNavigation() {
282
+ // Use event delegation on the topic container
283
+ $('[component="topic"]').off('click.javis-parent').on('click.javis-parent', '[component="post/parent"]', function(e) {
284
+ // Don't navigate if clicking on a link (username, etc.)
285
+ if ($(e.target).closest('a').length) {
286
+ return;
287
+ }
288
+
289
+ e.preventDefault();
290
+ e.stopPropagation();
291
+
292
+ var parentPid = $(this).attr('data-parent-pid');
293
+ if (!parentPid) {
294
+ return;
295
+ }
296
+
297
+ // Find the parent post element
298
+ var $parentPost = $('[component="topic"] > [component="post"][data-pid="' + parentPid + '"]');
299
+
300
+ if ($parentPost.length) {
301
+ // Post is on the current page - smooth scroll to it
302
+ smoothScrollToPost($parentPost);
303
+ } else {
304
+ // Post is on a different page - navigate via URL
305
+ window.location.href = config.relative_path + '/post/' + parentPid;
306
+ }
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Smooth scroll to a post element with highlight effect
312
+ */
313
+ function smoothScrollToPost($postElement) {
314
+ if (!$postElement.length) {
315
+ return;
316
+ }
317
+
318
+ // Calculate scroll position (with offset for header)
319
+ var headerHeight = $('header').outerHeight() || 60;
320
+ var scrollTop = $postElement.offset().top - headerHeight - 20;
321
+
322
+ // Smooth scroll
323
+ $('html, body').animate({
324
+ scrollTop: scrollTop
325
+ }, 400, 'swing', function() {
326
+ // Add highlight effect after scroll completes
327
+ highlightPost($postElement);
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Briefly highlight a post to draw attention
333
+ */
334
+ function highlightPost($postElement) {
335
+ var $container = $postElement.find('.post-container-parent');
336
+ if (!$container.length) {
337
+ $container = $postElement;
338
+ }
339
+
340
+ // Add highlight class
341
+ $container.addClass('post-highlight-flash');
342
+
343
+ // Remove after animation
344
+ setTimeout(function() {
345
+ $container.removeClass('post-highlight-flash');
346
+ }, 1500);
347
+ }
348
+
349
+ /**
350
+ * Initialize post hover actions
351
+ * Adds 'post-hovered' class only to the directly hovered post container
352
+ * This prevents CSS hover from bubbling up to parent posts
353
+ */
354
+ function initPostHoverActions() {
355
+ // Use event delegation for all post containers
356
+ $(document).off('mouseenter.javis-hover mouseleave.javis-hover', '.post-container-parent');
357
+
358
+ $(document).on('mouseenter.javis-hover', '.post-container-parent', function(e) {
359
+ // Stop propagation to prevent parent containers from getting the event
360
+ e.stopPropagation();
361
+ // Remove class from all other containers first
362
+ $('.post-container-parent.post-hovered').removeClass('post-hovered');
363
+ // Add class only to this specific container
364
+ $(this).addClass('post-hovered');
365
+ });
366
+
367
+ $(document).on('mouseleave.javis-hover', '.post-container-parent', function(e) {
368
+ e.stopPropagation();
369
+ $(this).removeClass('post-hovered');
370
+ });
371
+ }
372
+
54
373
  })();
@@ -0,0 +1,9 @@
1
+ <div component="post/parent" data-parent-pid="{./parent.pid}" data-uid="{./parent.uid}" class="btn btn-ghost btn-sm d-flex gap-2 text-start flex-row mb-2 post-parent-clickable" style="font-size: 13px;">
2
+ <div class="d-flex gap-2 text-nowrap">
3
+ <div class="d-flex flex-nowrap gap-1 align-items-center">
4
+ <a href="{config.relative_path}/user/{./parent.user.userslug}" class="text-decoration-none lh-1" onclick="event.stopPropagation();">{buildAvatar(./parent.user, "16px", true, "not-responsive align-middle")}</a>
5
+ <a class="fw-semibold text-truncate" style="max-width: 150px;" href="{config.relative_path}/user/{./parent.user.userslug}" onclick="event.stopPropagation();">{./parent.user.displayname}</a>
6
+ </div>
7
+ </div>
8
+ <div component="post/parent/content" class="text-muted line-clamp-1 text-break w-100">{./parent.content}</div>
9
+ </div>