@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 +1 -1
- package/scss/_topic.scss +461 -49
- package/static/lib/theme.js +215 -0
package/package.json
CHANGED
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-
|
|
36
|
+
gap: $jv-space-3;
|
|
37
37
|
|
|
38
38
|
h1,
|
|
39
39
|
[component="topic/title"],
|
|
40
40
|
.topic-title {
|
|
41
|
-
font-size:
|
|
42
|
-
font-weight:
|
|
43
|
-
line-height:
|
|
44
|
-
letter-spacing: -0.
|
|
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:
|
|
53
|
-
font-weight:
|
|
54
|
-
line-height:
|
|
55
|
-
letter-spacing: -0.
|
|
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:
|
|
116
|
-
|
|
117
|
-
|
|
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:
|
|
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-
|
|
142
|
-
margin-bottom: $jv-space-
|
|
143
|
-
font-size: $jv-font-size-
|
|
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:
|
|
147
|
-
height:
|
|
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:
|
|
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-
|
|
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-
|
|
260
|
+
font-size: $jv-font-size-xs;
|
|
206
261
|
}
|
|
207
262
|
|
|
208
|
-
code { padding: 2px
|
|
263
|
+
code { padding: 2px 5px; }
|
|
209
264
|
|
|
210
265
|
pre {
|
|
211
|
-
padding: $jv-space-
|
|
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
|
-
//
|
|
740
|
+
// PARENT POST CONTEXT (shown when viewing a reply)
|
|
597
741
|
// ===========================================================
|
|
598
|
-
[component="post/
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
border-left:
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
.
|
|
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
|
|
802
|
+
// REPLY COUNT BUTTON (Accordion trigger)
|
|
615
803
|
// ===========================================================
|
|
616
804
|
[component="post/reply-count"] {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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:
|
|
815
|
+
transition: all $jv-transition-fast;
|
|
623
816
|
|
|
624
817
|
&:hover {
|
|
625
|
-
background: $jv-hover-bg;
|
|
626
|
-
color: $jv-
|
|
818
|
+
background: $jv-hover-bg !important;
|
|
819
|
+
border-color: $jv-border-strong !important;
|
|
820
|
+
color: $jv-text-main !important;
|
|
627
821
|
}
|
|
628
822
|
|
|
629
|
-
|
|
630
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|
package/static/lib/theme.js
CHANGED
|
@@ -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
|
})();
|