@okjavis/nodebb-theme-javis 2.3.0 → 2.4.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.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Modern, premium NodeBB theme for JAVIS Community - Extends Harmony with custom styling",
5
5
  "main": "theme.js",
6
6
  "scripts": {
@@ -0,0 +1,716 @@
1
+ // ===========================================================
2
+ // COMPOSER MODAL – JAVIS Design System
3
+ // Transforms the default NodeBB composer into a centered modal
4
+ // ===========================================================
5
+
6
+ // ===========================================================
7
+ // BACKDROP
8
+ // ===========================================================
9
+ html.composing::before {
10
+ content: '';
11
+ position: fixed;
12
+ top: 0;
13
+ left: 0;
14
+ right: 0;
15
+ bottom: 0;
16
+ background: rgba(0, 0, 0, 0.5);
17
+ backdrop-filter: blur(4px);
18
+ -webkit-backdrop-filter: blur(4px);
19
+ z-index: 1040;
20
+ }
21
+
22
+ // ===========================================================
23
+ // MODAL CONTAINER
24
+ // ===========================================================
25
+ html.composing .composer {
26
+ position: fixed !important;
27
+ top: 50% !important;
28
+ left: 50% !important;
29
+ right: auto !important;
30
+ bottom: auto !important;
31
+ transform: translate(-50%, -50%) !important;
32
+ width: 720px !important;
33
+ max-width: calc(100vw - 32px) !important;
34
+ height: 600px !important;
35
+ max-height: 85vh !important;
36
+ background: $jv-surface !important;
37
+ border-radius: $jv-radius-lg !important;
38
+ border: 1px solid $jv-border-subtle !important;
39
+ box-shadow: $jv-shadow-xl !important;
40
+ z-index: 1050 !important;
41
+ overflow: visible !important; // Allow absolute children to overflow
42
+ margin: 0 !important;
43
+ }
44
+
45
+ html.composing body {
46
+ overflow: hidden !important;
47
+ }
48
+
49
+ // ===========================================================
50
+ // COMPOSER CONTAINER
51
+ // ===========================================================
52
+ html.composing .composer .composer-container {
53
+ display: flex !important;
54
+ flex-direction: column !important;
55
+ height: 100% !important;
56
+ padding: 0 !important;
57
+ gap: 0 !important;
58
+ overflow: hidden !important;
59
+ border-radius: $jv-radius-lg !important;
60
+ }
61
+
62
+ html.composing .composer .composer-container > .p-2 {
63
+ display: flex !important;
64
+ flex-direction: column !important;
65
+ height: 100% !important;
66
+ padding: 0 !important;
67
+ gap: 0 !important;
68
+ }
69
+
70
+ // ===========================================================
71
+ // HIDE MOBILE NAVBAR
72
+ // ===========================================================
73
+ html.composing .composer .mobile-navbar {
74
+ display: none !important;
75
+ }
76
+
77
+ // ===========================================================
78
+ // HEADER
79
+ // ===========================================================
80
+ html.composing .composer .title-container {
81
+ display: flex !important;
82
+ align-items: center !important;
83
+ gap: $jv-space-3 !important;
84
+ padding: $jv-space-4 $jv-space-5 !important;
85
+ border-bottom: 1px solid $jv-border-subtle !important;
86
+ background: $jv-surface !important;
87
+ flex-shrink: 0 !important;
88
+ min-height: 56px !important;
89
+ border-radius: $jv-radius-lg $jv-radius-lg 0 0 !important;
90
+ }
91
+
92
+ // Category selector
93
+ html.composing .composer .category-list-container {
94
+ flex-shrink: 0 !important;
95
+ display: block !important;
96
+
97
+ [component="category-selector"] {
98
+ .btn {
99
+ background: $jv-bg !important;
100
+ border: 1px solid $jv-border-subtle !important;
101
+ border-radius: $jv-radius-sm !important;
102
+ padding: $jv-space-2 $jv-space-3 !important;
103
+ font-size: $jv-font-size-sm !important;
104
+ color: $jv-text-main !important;
105
+
106
+ &:hover {
107
+ border-color: $jv-border-strong !important;
108
+ background: $jv-hover-bg !important;
109
+ }
110
+ }
111
+
112
+ .dropdown-menu {
113
+ min-width: 280px !important;
114
+ max-height: 320px !important;
115
+ overflow-y: auto !important;
116
+ padding: $jv-space-2 !important;
117
+
118
+ // Fix category list spacing
119
+ .category-dropdown-menu {
120
+ margin: 0 !important;
121
+ padding: 0 !important;
122
+ }
123
+
124
+ // Category items inside dropdown
125
+ li.category {
126
+ margin: 0 !important;
127
+ padding: 0 !important;
128
+
129
+ a.dropdown-item {
130
+ display: flex !important;
131
+ align-items: center !important;
132
+ gap: $jv-space-2 !important;
133
+ padding: $jv-space-2 $jv-space-3 !important;
134
+ margin: 2px 0 !important;
135
+ font-size: $jv-font-size-sm !important;
136
+ color: $jv-text-main !important;
137
+ border-radius: $jv-radius-xs !important;
138
+ white-space: nowrap !important;
139
+ overflow: hidden !important;
140
+ text-overflow: ellipsis !important;
141
+
142
+ &:hover {
143
+ background: $jv-hover-bg !important;
144
+ }
145
+
146
+ // Category icon
147
+ .category-item {
148
+ display: inline-flex !important;
149
+ align-items: center !important;
150
+ gap: $jv-space-2 !important;
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ // Title input
159
+ html.composing .composer [data-component="composer/title"] {
160
+ flex: 1 !important;
161
+ min-width: 0 !important;
162
+
163
+ .title {
164
+ width: 100% !important;
165
+ font-size: $jv-font-size-lg !important;
166
+ font-weight: 600 !important;
167
+ border: none !important;
168
+ background: transparent !important;
169
+ padding: $jv-space-2 0 !important;
170
+ color: $jv-text-main !important;
171
+ box-shadow: none !important;
172
+
173
+ &::placeholder {
174
+ color: $jv-text-soft !important;
175
+ font-weight: 400 !important;
176
+ }
177
+
178
+ &:focus {
179
+ outline: none !important;
180
+ box-shadow: none !important;
181
+ }
182
+ }
183
+ }
184
+
185
+ // ===========================================================
186
+ // ACTION BAR - X close button in header, Submit at bottom
187
+ // ===========================================================
188
+ html.composing .composer .action-bar {
189
+ flex-shrink: 0 !important;
190
+ display: flex !important;
191
+ align-items: center !important;
192
+ gap: $jv-space-2 !important;
193
+
194
+ // HIDE minimize button
195
+ .composer-minimize {
196
+ display: none !important;
197
+ }
198
+
199
+ // Style discard as X close button - stays in header
200
+ .composer-discard {
201
+ display: flex !important;
202
+ align-items: center !important;
203
+ justify-content: center !important;
204
+ width: 36px !important;
205
+ height: 36px !important;
206
+ padding: 0 !important;
207
+ margin: 0 !important;
208
+ border-radius: $jv-radius-sm !important;
209
+ background: transparent !important;
210
+ border: none !important;
211
+ color: $jv-text-muted !important;
212
+ cursor: pointer !important;
213
+
214
+ &:hover {
215
+ background: $jv-hover-bg !important;
216
+ color: $jv-text-main !important;
217
+ }
218
+
219
+ // Hide text
220
+ span {
221
+ display: none !important;
222
+ }
223
+
224
+ // Style icon as X
225
+ i.fa-trash {
226
+ font-size: 18px !important;
227
+ margin: 0 !important;
228
+
229
+ &::before {
230
+ content: '\f00d' !important; // fa-times
231
+ }
232
+ }
233
+ }
234
+
235
+ }
236
+
237
+ // ===========================================================
238
+ // SUBMIT BUTTON - Positioned at bottom right of modal (footer)
239
+ // Vertically centered with the footer (56px height, button ~40px)
240
+ // ===========================================================
241
+ html.composing .composer .action-bar [component="composer/submit/container"] {
242
+ position: absolute !important;
243
+ bottom: 8px !important; // Centered: (56px footer - 40px button) / 2 = 8px
244
+ right: $jv-space-5 !important;
245
+ top: auto !important;
246
+ left: auto !important;
247
+ z-index: 1060 !important;
248
+ display: flex !important;
249
+ align-items: center !important;
250
+
251
+ .btn-group {
252
+ display: flex !important;
253
+ }
254
+
255
+ .composer-submit {
256
+ background: $jv-primary !important;
257
+ border-color: $jv-primary !important;
258
+ color: white !important;
259
+ font-weight: 600 !important;
260
+ padding: $jv-space-2 $jv-space-5 !important;
261
+ border-radius: $jv-radius-sm !important;
262
+ font-size: $jv-font-size-sm !important;
263
+ display: inline-flex !important;
264
+ align-items: center !important;
265
+ gap: $jv-space-1 !important;
266
+ height: 40px !important;
267
+
268
+ &:hover {
269
+ background: $jv-primary-hover !important;
270
+ border-color: $jv-primary-hover !important;
271
+ }
272
+
273
+ i {
274
+ margin-right: $jv-space-1 !important;
275
+ }
276
+
277
+ span {
278
+ display: inline !important;
279
+ }
280
+ }
281
+
282
+ // HIDE schedule/options dropdown - not needed
283
+ [component="composer/submit/options/container"],
284
+ .dropdown-toggle {
285
+ display: none !important;
286
+ }
287
+ }
288
+
289
+ // ===========================================================
290
+ // FORMATTING BAR
291
+ // ===========================================================
292
+ html.composing .composer .formatting-bar {
293
+ display: flex !important;
294
+ align-items: center !important;
295
+ justify-content: space-between !important;
296
+ padding: $jv-space-2 $jv-space-4 !important;
297
+ background: $jv-bg !important;
298
+ border-bottom: 1px solid $jv-border-subtle !important;
299
+ margin: 0 !important;
300
+ flex-shrink: 0 !important;
301
+ gap: $jv-space-3 !important;
302
+ }
303
+
304
+ html.composing .composer .formatting-group {
305
+ display: flex !important;
306
+ align-items: center !important;
307
+ gap: 2px !important;
308
+ flex-wrap: nowrap !important;
309
+ overflow-x: auto !important;
310
+
311
+ li {
312
+ flex-shrink: 0 !important;
313
+ list-style: none !important;
314
+
315
+ .btn {
316
+ color: $jv-text-muted !important;
317
+ padding: $jv-space-2 !important;
318
+ border-radius: $jv-radius-sm !important;
319
+ min-width: 32px !important;
320
+ height: 32px !important;
321
+ display: flex !important;
322
+ align-items: center !important;
323
+ justify-content: center !important;
324
+ background: transparent !important;
325
+ border: none !important;
326
+
327
+ &:hover {
328
+ background: $jv-hover-bg !important;
329
+ color: $jv-text-main !important;
330
+ }
331
+ }
332
+ }
333
+
334
+ .spacer {
335
+ width: 1px !important;
336
+ height: 20px !important;
337
+ background: $jv-border-subtle !important;
338
+ margin: 0 $jv-space-1 !important;
339
+ }
340
+ }
341
+
342
+ // Preview/Help buttons
343
+ html.composing .composer .formatting-bar > .d-flex {
344
+ flex-shrink: 0 !important;
345
+ gap: $jv-space-2 !important;
346
+ align-items: center !important;
347
+
348
+ .draft-icon {
349
+ color: $jv-success !important;
350
+ }
351
+
352
+ [data-action="preview"],
353
+ [data-action="help"] {
354
+ color: $jv-text-muted !important;
355
+ font-size: $jv-font-size-sm !important;
356
+ padding: $jv-space-2 $jv-space-3 !important;
357
+ border-radius: $jv-radius-sm !important;
358
+ background: transparent !important;
359
+ border: none !important;
360
+
361
+ &:hover {
362
+ background: $jv-hover-bg !important;
363
+ color: $jv-text-main !important;
364
+ }
365
+ }
366
+ }
367
+
368
+ // ===========================================================
369
+ // WRITE/PREVIEW CONTAINER
370
+ // ===========================================================
371
+ html.composing .composer .write-preview-container {
372
+ flex: 1 !important;
373
+ display: flex !important;
374
+ flex-direction: row !important;
375
+ min-height: 0 !important;
376
+ overflow: hidden !important;
377
+ gap: 0 !important;
378
+ }
379
+
380
+ // Write container - OVERRIDE w-50
381
+ html.composing .composer .write-container {
382
+ flex: 1 !important;
383
+ display: flex !important;
384
+ flex-direction: column !important;
385
+ width: 100% !important;
386
+ min-width: 0 !important;
387
+ position: relative !important;
388
+
389
+ &.w-50 {
390
+ width: 100% !important;
391
+ }
392
+
393
+ .write {
394
+ flex: 1 !important;
395
+ width: 100% !important;
396
+ height: 100% !important;
397
+ min-height: 150px !important;
398
+ padding: $jv-space-4 !important;
399
+ border: none !important;
400
+ border-radius: 0 !important;
401
+ resize: none !important;
402
+ font-size: $jv-font-size-base !important;
403
+ line-height: $jv-line-height-relaxed !important;
404
+ color: $jv-text-main !important;
405
+ background: $jv-surface !important;
406
+
407
+ &::placeholder {
408
+ color: $jv-text-soft !important;
409
+ }
410
+
411
+ &:focus {
412
+ outline: none !important;
413
+ box-shadow: none !important;
414
+ }
415
+ }
416
+ }
417
+
418
+ // Preview container - toggles via d-none/d-flex classes from NodeBB JS
419
+ html.composing .composer .preview-container {
420
+ width: 100% !important;
421
+ overflow-y: auto !important;
422
+ padding: $jv-space-4 !important;
423
+ background: $jv-surface !important;
424
+ flex: 1 !important;
425
+ flex-direction: column !important;
426
+
427
+ &.w-50 {
428
+ width: 100% !important;
429
+ }
430
+
431
+ // Allow NodeBB's d-none to work (hide by default unless d-flex is added)
432
+ &.d-none {
433
+ display: none !important;
434
+ }
435
+
436
+ &:not(.d-none) {
437
+ display: flex !important;
438
+ }
439
+
440
+ .preview {
441
+ width: 100% !important;
442
+ }
443
+ }
444
+
445
+ // Write container visibility - controlled by NodeBB JS
446
+ html.composing .composer .write-container.d-none {
447
+ display: none !important;
448
+ }
449
+
450
+ html.composing .composer .write-container:not(.d-none) {
451
+ display: flex !important;
452
+ }
453
+
454
+ // ===========================================================
455
+ // FOOTER - Tags row
456
+ // ===========================================================
457
+ html.composing .composer .tag-row {
458
+ display: flex !important;
459
+ align-items: center !important;
460
+ padding: 8px 20px !important;
461
+ border-top: 1px solid $jv-border-subtle !important;
462
+ background: $jv-surface !important;
463
+ flex-shrink: 0 !important;
464
+ min-height: 56px !important;
465
+ gap: $jv-space-4 !important;
466
+ border-radius: 0 0 $jv-radius-lg $jv-radius-lg !important;
467
+ }
468
+
469
+ html.composing .composer .tags-container {
470
+ display: flex !important;
471
+ align-items: center !important;
472
+ gap: $jv-space-2 !important;
473
+ flex: 1 !important;
474
+ min-width: 0 !important;
475
+ max-width: calc(100% - 140px) !important; // Leave space for submit button
476
+ background: $jv-bg !important;
477
+ border: 1px solid $jv-border-subtle !important;
478
+ border-radius: $jv-radius-sm !important;
479
+ padding: $jv-space-1 $jv-space-3 !important;
480
+ min-height: 40px !important;
481
+ cursor: text !important;
482
+ transition: border-color 0.15s ease !important;
483
+
484
+ &:hover {
485
+ border-color: $jv-border-strong !important;
486
+ }
487
+
488
+ &:focus-within {
489
+ border-color: $jv-primary !important;
490
+ box-shadow: 0 0 0 2px rgba($jv-primary, 0.1) !important;
491
+ }
492
+
493
+ [component="composer/tag/dropdown"] {
494
+ flex-shrink: 0 !important;
495
+
496
+ .btn {
497
+ color: $jv-text-muted !important;
498
+ font-size: $jv-font-size-sm !important;
499
+ padding: $jv-space-1 $jv-space-2 !important;
500
+ border-radius: $jv-radius-xs !important;
501
+ background: transparent !important;
502
+ border: none !important;
503
+
504
+ i {
505
+ color: $jv-primary !important;
506
+ }
507
+
508
+ &:hover {
509
+ background: $jv-hover-bg !important;
510
+ }
511
+ }
512
+ }
513
+
514
+ // Wrapper that appears when tags are present
515
+ .bootstrap-tagsinput {
516
+ flex: 1 !important;
517
+ display: flex !important;
518
+ flex-wrap: wrap !important;
519
+ align-items: center !important;
520
+ gap: $jv-space-2 !important;
521
+ background: transparent !important;
522
+ border: none !important;
523
+ padding: 0 !important;
524
+ box-shadow: none !important;
525
+ min-height: auto !important;
526
+
527
+ // Individual tags - beautiful pills
528
+ .tag {
529
+ display: inline-flex !important;
530
+ align-items: center !important;
531
+ gap: 4px !important;
532
+ background: $jv-primary-soft !important;
533
+ color: $jv-primary !important;
534
+ border-radius: $jv-radius-pill !important;
535
+ padding: 4px 10px !important;
536
+ font-size: $jv-font-size-xs !important;
537
+ font-weight: 500 !important;
538
+ border: none !important;
539
+ line-height: 1.4 !important;
540
+
541
+ // Remove button inside tag
542
+ [data-role="remove"] {
543
+ margin-left: 2px !important;
544
+ cursor: pointer !important;
545
+ opacity: 0.7 !important;
546
+ font-size: 12px !important;
547
+
548
+ &:hover {
549
+ opacity: 1 !important;
550
+ }
551
+ }
552
+ }
553
+
554
+ // The input inside bootstrap-tagsinput
555
+ input {
556
+ flex: 1 !important;
557
+ min-width: 80px !important;
558
+ border: none !important;
559
+ background: transparent !important;
560
+ font-size: $jv-font-size-sm !important;
561
+ color: $jv-text-main !important;
562
+ padding: $jv-space-1 0 !important;
563
+ outline: none !important;
564
+
565
+ &::placeholder {
566
+ color: $jv-text-soft !important;
567
+ }
568
+ }
569
+ }
570
+
571
+ // Plain input when no bootstrap-tagsinput
572
+ > input.tags {
573
+ flex: 1 !important;
574
+ border: none !important;
575
+ background: transparent !important;
576
+ font-size: $jv-font-size-sm !important;
577
+ color: $jv-text-main !important;
578
+ padding: $jv-space-1 0 !important;
579
+
580
+ &::placeholder {
581
+ color: $jv-text-soft !important;
582
+ }
583
+
584
+ &:focus {
585
+ outline: none !important;
586
+ }
587
+ }
588
+ }
589
+
590
+ // ===========================================================
591
+ // IMAGE DROP ZONE
592
+ // ===========================================================
593
+ html.composing .composer .imagedrop {
594
+ position: absolute !important;
595
+ inset: 0 !important;
596
+ background: rgba($jv-primary, 0.1) !important;
597
+ border: 2px dashed $jv-primary !important;
598
+ border-radius: $jv-radius-md !important;
599
+ display: none !important;
600
+ align-items: center !important;
601
+ justify-content: center !important;
602
+ z-index: 100 !important;
603
+
604
+ > div {
605
+ background: $jv-surface !important;
606
+ padding: $jv-space-4 $jv-space-8 !important;
607
+ border-radius: $jv-radius-sm !important;
608
+ font-weight: 500 !important;
609
+ color: $jv-primary !important;
610
+ }
611
+ }
612
+
613
+ // ===========================================================
614
+ // RESIZER - HIDE
615
+ // ===========================================================
616
+ html.composing .composer .resizer {
617
+ display: none !important;
618
+ }
619
+
620
+ // ===========================================================
621
+ // DROPDOWN MENUS
622
+ // ===========================================================
623
+ html.composing .composer .dropdown-menu {
624
+ border: 1px solid $jv-border-subtle !important;
625
+ border-radius: $jv-radius-sm !important;
626
+ box-shadow: $jv-shadow-lg !important;
627
+ background: $jv-surface !important;
628
+ padding: $jv-space-2 !important;
629
+
630
+ .dropdown-item {
631
+ font-size: $jv-font-size-sm !important;
632
+ padding: $jv-space-2 $jv-space-3 !important;
633
+ border-radius: $jv-radius-xs !important;
634
+
635
+ &:hover {
636
+ background: $jv-hover-bg !important;
637
+ }
638
+ }
639
+ }
640
+
641
+ // ===========================================================
642
+ // FILE FORM - HIDE
643
+ // ===========================================================
644
+ html.composing .composer #fileForm {
645
+ display: none !important;
646
+ }
647
+
648
+ // ===========================================================
649
+ // QUICK SEARCH
650
+ // ===========================================================
651
+ html.composing .composer .quick-search-container {
652
+ position: absolute !important;
653
+ top: 100% !important;
654
+ left: 0 !important;
655
+ right: 0 !important;
656
+ background: $jv-surface !important;
657
+ border: 1px solid $jv-border-subtle !important;
658
+ border-radius: $jv-radius-sm !important;
659
+ box-shadow: $jv-shadow-lg !important;
660
+ z-index: 50 !important;
661
+ }
662
+
663
+ // ===========================================================
664
+ // REPLY MODE
665
+ // ===========================================================
666
+ html.composing .composer.reply {
667
+ height: 450px !important;
668
+
669
+ .tags-container {
670
+ display: none !important;
671
+ }
672
+ }
673
+
674
+ // ===========================================================
675
+ // MOBILE RESPONSIVE
676
+ // ===========================================================
677
+ @media (max-width: 767px) {
678
+ html.composing .composer {
679
+ top: 0 !important;
680
+ left: 0 !important;
681
+ transform: none !important;
682
+ width: 100vw !important;
683
+ max-width: 100vw !important;
684
+ height: 100vh !important;
685
+ max-height: 100vh !important;
686
+ border-radius: 0 !important;
687
+ border: none !important;
688
+ }
689
+
690
+ html.composing .composer .composer-container {
691
+ height: 100vh !important;
692
+ border-radius: 0 !important;
693
+ }
694
+
695
+ html.composing .composer .title-container {
696
+ padding: $jv-space-3 $jv-space-4 !important;
697
+ padding-top: max($jv-space-3, env(safe-area-inset-top)) !important;
698
+ border-radius: 0 !important;
699
+ }
700
+
701
+ html.composing .composer .formatting-bar {
702
+ padding: $jv-space-2 $jv-space-3 !important;
703
+ overflow-x: auto !important;
704
+
705
+ [data-action="preview"] span,
706
+ [data-action="help"] span {
707
+ display: none !important;
708
+ }
709
+ }
710
+
711
+ html.composing .composer .tag-row {
712
+ padding: $jv-space-3 $jv-space-4 !important;
713
+ padding-bottom: max($jv-space-3, env(safe-area-inset-bottom)) !important;
714
+ border-radius: 0 !important;
715
+ }
716
+ }
package/scss/_feed.scss CHANGED
@@ -4,22 +4,193 @@
4
4
  // ===========================================================
5
5
 
6
6
  // ===========================================================
7
- // HIDE NEW TOPIC BUTTON AND ALL CATEGORIES ROW
7
+ // HIDE CATEGORIES WIDGET ON FEED PAGE
8
+ // The categories list widget is not relevant on the feed page
8
9
  // ===========================================================
9
- // Hide New Topic button
10
- // #new_topic,
11
- // .btn[href*="/compose"],
12
- // button[data-action="newTopic"],
13
- // a[href*="/compose"] {
14
- // display: none !important;
15
- // }
10
+ .feed {
11
+ // Hide categories list directly - simple and reliable
12
+ ul.categories-list {
13
+ display: none !important;
14
+ }
15
+
16
+ // Also hide any widget title that precedes a categories list
17
+ // This targets the "All Categories" header
18
+ [data-widget-area="left"],
19
+ [data-widget-area="right"] {
20
+ // Hide the entire widget container that has categories
21
+ // Using adjacent sibling and general sibling selectors
22
+ .categories-list,
23
+ [component="categories/category"] {
24
+ display: none !important;
25
+ }
26
+ }
27
+ }
28
+
29
+ // ===========================================================
30
+ // LINKEDIN-STYLE COMPOSER PROMPT CARD
31
+ // ===========================================================
32
+ .composer-prompt-card {
33
+ background: $jv-surface;
34
+ border: 1px solid $jv-border-subtle;
35
+ border-radius: $jv-radius-lg;
36
+ padding: $jv-space-4;
37
+ margin-bottom: $jv-space-4;
38
+ box-shadow: $jv-shadow-sm;
39
+ transition: box-shadow $jv-transition-fast, border-color $jv-transition-fast;
40
+
41
+ &:hover {
42
+ box-shadow: $jv-shadow-md;
43
+ border-color: $jv-border-strong;
44
+ }
45
+
46
+ // Guest variant
47
+ &.composer-prompt-guest {
48
+ .composer-prompt-input {
49
+ cursor: pointer;
50
+ }
51
+ }
52
+ }
53
+
54
+ .composer-prompt-inner {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: $jv-space-3;
58
+ }
59
+
60
+ .composer-prompt-avatar {
61
+ flex-shrink: 0;
62
+
63
+ .avatar {
64
+ width: 48px !important;
65
+ height: 48px !important;
66
+ border-radius: 50% !important;
67
+ object-fit: cover;
68
+ border: 2px solid rgba(0, 0, 0, 0.05);
69
+ }
70
+
71
+ .avatar-placeholder {
72
+ width: 48px;
73
+ height: 48px;
74
+ border-radius: 50%;
75
+ background: linear-gradient(135deg, $jv-primary-soft 0%, rgba(0, 81, 255, 0.2) 100%);
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ color: $jv-primary;
80
+ font-size: 18px;
81
+ }
82
+ }
83
+
84
+ .composer-prompt-input {
85
+ flex: 1;
86
+ display: flex;
87
+ align-items: center;
88
+ padding: $jv-space-3 $jv-space-4;
89
+ background: $jv-bg;
90
+ border: 1px solid $jv-border-subtle;
91
+ border-radius: $jv-radius-pill;
92
+ cursor: pointer;
93
+ transition: all $jv-transition-fast;
94
+ text-decoration: none;
95
+ min-height: 48px;
96
+
97
+ &:hover {
98
+ background: rgba(0, 0, 0, 0.04);
99
+ border-color: $jv-border-strong;
100
+ }
101
+
102
+ &:focus {
103
+ outline: none;
104
+ border-color: $jv-primary;
105
+ box-shadow: $jv-focus-ring;
106
+ }
107
+
108
+ // Reset button styles
109
+ &.composer-prompt-input {
110
+ text-align: left;
111
+ font-family: inherit;
112
+ font-size: inherit;
113
+ }
114
+ }
115
+
116
+ .composer-prompt-placeholder {
117
+ color: $jv-text-muted;
118
+ font-size: $jv-font-size-base;
119
+ font-weight: 500;
120
+ }
121
+
122
+ .composer-prompt-actions {
123
+ display: flex;
124
+ gap: $jv-space-1;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ .composer-prompt-action {
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ width: 40px;
133
+ height: 40px;
134
+ border: none;
135
+ background: transparent;
136
+ border-radius: 50%;
137
+ color: $jv-text-muted;
138
+ cursor: pointer;
139
+ transition: all $jv-transition-fast;
140
+
141
+ &:hover {
142
+ background: $jv-hover-bg;
143
+ color: $jv-primary;
144
+ }
145
+
146
+ &:focus {
147
+ outline: none;
148
+ box-shadow: $jv-focus-ring;
149
+ }
150
+
151
+ i {
152
+ font-size: 18px;
153
+ }
154
+ }
16
155
 
17
- // // Hide All categories row
18
- // .category-selector-container,
19
- // [component="category/dropdown"],
20
- // .d-flex.justify-content-between.py-2.mb-2 {
21
- // display: none !important;
22
- // }
156
+ // Feed options row (settings dropdown only)
157
+ .feed-options-row {
158
+ margin-bottom: $jv-space-2 !important;
159
+ padding: $jv-space-2 0 !important;
160
+ }
161
+
162
+ // Mobile responsive
163
+ @media (max-width: 576px) {
164
+ .composer-prompt-card {
165
+ padding: $jv-space-3;
166
+ border-radius: $jv-radius-md;
167
+ }
168
+
169
+ .composer-prompt-avatar {
170
+ .avatar,
171
+ .avatar-placeholder {
172
+ width: 40px !important;
173
+ height: 40px !important;
174
+ }
175
+
176
+ .avatar-placeholder {
177
+ font-size: 16px;
178
+ }
179
+ }
180
+
181
+ .composer-prompt-input {
182
+ padding: $jv-space-2 $jv-space-3;
183
+ min-height: 40px;
184
+ }
185
+
186
+ .composer-prompt-placeholder {
187
+ font-size: $jv-font-size-sm;
188
+ }
189
+
190
+ .composer-prompt-actions {
191
+ display: none; // Hide action buttons on mobile for cleaner UI
192
+ }
193
+ }
23
194
 
24
195
  // ===========================================================
25
196
  // FEED CONTAINER
@@ -131,22 +302,18 @@
131
302
  .overflow-hidden {
132
303
  border-radius: $jv-radius-md $jv-radius-md 0 0 !important; // Match card radius
133
304
  max-height: 280px !important; // Reduced from 350px - more balanced ratio
305
+ background: rgba(0, 0, 0, 0.02); // Subtle background for letterboxing
134
306
 
135
307
  img {
136
308
  width: 100%;
137
309
  height: 280px; // Fixed height for consistency
138
- object-fit: cover;
310
+ object-fit: contain; // Prevent stretching, maintain aspect ratio
139
311
  }
140
312
  }
141
313
 
142
- // Additional thumbnails
314
+ // Hide additional thumbnails - only show main image in feed
143
315
  .position-absolute {
144
- padding: $jv-space-4 !important;
145
-
146
- img {
147
- border: 2px solid $jv-surface;
148
- box-shadow: $jv-shadow-soft;
149
- }
316
+ display: none !important;
150
317
  }
151
318
  }
152
319
  }
@@ -314,6 +481,95 @@
314
481
  border-color: $jv-primary !important;
315
482
  }
316
483
  }
484
+
485
+ // ===========================================================
486
+ // FEED IMAGE CAROUSEL (matching topic page design)
487
+ // ===========================================================
488
+ .post-image-carousel {
489
+ border-radius: $jv-radius-md;
490
+ overflow: hidden;
491
+ margin: $jv-space-4 0;
492
+ background: rgba(0, 0, 0, 0.03);
493
+
494
+ .carousel-inner {
495
+ border-radius: $jv-radius-md;
496
+ }
497
+
498
+ .carousel-item {
499
+ // Fixed height container to prevent jumping
500
+ height: 400px;
501
+
502
+ img {
503
+ width: 100%;
504
+ height: 100%;
505
+ object-fit: contain;
506
+ background: rgba(0, 0, 0, 0.02);
507
+ }
508
+ }
509
+
510
+ // Navigation arrows - matching topic page
511
+ .carousel-control-prev,
512
+ .carousel-control-next {
513
+ width: 48px;
514
+ height: 48px;
515
+ top: 50%;
516
+ transform: translateY(-50%);
517
+ background: rgba(0, 0, 0, 0.5);
518
+ border-radius: 50%;
519
+ opacity: 0;
520
+ transition: opacity $jv-transition-fast;
521
+
522
+ &:hover {
523
+ background: rgba(0, 0, 0, 0.7);
524
+ }
525
+
526
+ .carousel-control-prev-icon,
527
+ .carousel-control-next-icon {
528
+ width: 20px;
529
+ height: 20px;
530
+ }
531
+ }
532
+
533
+ .carousel-control-prev {
534
+ left: $jv-space-3;
535
+ }
536
+
537
+ .carousel-control-next {
538
+ right: $jv-space-3;
539
+ }
540
+
541
+ &:hover {
542
+ .carousel-control-prev,
543
+ .carousel-control-next {
544
+ opacity: 1;
545
+ }
546
+ }
547
+
548
+ // Indicator dots - matching topic page
549
+ .carousel-indicators {
550
+ margin-bottom: $jv-space-3;
551
+ gap: $jv-space-2;
552
+
553
+ button {
554
+ width: 8px;
555
+ height: 8px;
556
+ border-radius: 50%;
557
+ background: rgba(255, 255, 255, 0.5);
558
+ border: none;
559
+ opacity: 1;
560
+ transition: background-color $jv-transition-fast, transform $jv-transition-fast;
561
+
562
+ &.active {
563
+ background: #fff;
564
+ transform: scale(1.2);
565
+ }
566
+
567
+ &:hover {
568
+ background: rgba(255, 255, 255, 0.8);
569
+ }
570
+ }
571
+ }
572
+ }
317
573
  }
318
574
 
319
575
  // ===========================================================
@@ -504,7 +504,7 @@ div[data-widget-area="right"],
504
504
 
505
505
  // Sticky positioning - stays visible when scrolling
506
506
  position: sticky;
507
- top: 48px; // Adjust based on your header height
507
+ top: 72px; // Adjust based on your header height
508
508
  align-self: flex-start;
509
509
  max-height: calc(100vh - 100px); // Account for header + padding
510
510
  overflow-y: auto; // Allow scrolling if content is too tall
package/scss/_topic.scss CHANGED
@@ -598,9 +598,7 @@ body.template-topic {
598
598
  }
599
599
 
600
600
  [component="topic/reply/container"] {
601
- display: flex;
602
- flex-direction: column;
603
- gap: $jv-space-2;
601
+ width: 100%;
604
602
 
605
603
  .btn-primary {
606
604
  flex: 1;
@@ -609,7 +607,11 @@ body.template-topic {
609
607
  padding: $jv-space-3 $jv-space-4;
610
608
  }
611
609
 
612
- .dropdown-toggle { display: none; }
610
+ // Hide the "Reply as topic" dropdown toggle and menu
611
+ .dropdown-toggle,
612
+ .dropdown-menu {
613
+ display: none !important;
614
+ }
613
615
  }
614
616
 
615
617
  // Sidebar action buttons - full-width, left-aligned (extends base .btn-ghost)
@@ -27,12 +27,16 @@
27
27
  // Initialize post hover actions (show actions only on directly hovered post)
28
28
  initPostHoverActions();
29
29
 
30
+ // Initialize click handler for composer prompt card (rendered server-side in feed.tpl)
31
+ initFeedComposerPromptHandler();
32
+
30
33
  // Re-initialize carousels when new posts are loaded (infinite scroll, etc.)
31
34
  // Also handle post edits by clearing the processed flag
32
35
  $(window).on('action:posts.loaded action:topic.loaded action:ajaxify.end', function() {
33
36
  initPostImageCarousels();
34
37
  initParentPostNavigation();
35
38
  initPostHoverActions();
39
+ initFeedComposerPromptHandler();
36
40
  });
37
41
 
38
42
  // Handle post edits - need to re-process the edited post
@@ -370,4 +374,42 @@
370
374
  });
371
375
  }
372
376
 
377
+ /**
378
+ * Initialize click handler for composer prompt card on feed page
379
+ * The card HTML is rendered server-side in feed.tpl for instant display (no flicker)
380
+ * This function only sets up the click handlers
381
+ */
382
+ function initFeedComposerPromptHandler() {
383
+ // Only run on feed page with composer card present
384
+ var $card = $('.composer-prompt-card');
385
+ if (!$card.length) {
386
+ return;
387
+ }
388
+
389
+ // Skip if already initialized
390
+ if ($card.data('handler-initialized')) {
391
+ return;
392
+ }
393
+ $card.data('handler-initialized', true);
394
+
395
+ var isLoggedIn = typeof app !== 'undefined' && app.user && app.user.uid;
396
+
397
+ // Bind click handler to open composer
398
+ $card.on('click', '.composer-prompt-input, .composer-prompt-action', function(e) {
399
+ e.preventDefault();
400
+
401
+ if (!isLoggedIn) {
402
+ // Redirect to login
403
+ window.location.href = config.relative_path + '/login';
404
+ return;
405
+ }
406
+
407
+ // Trigger the original New Topic button click
408
+ var $newTopicBtn = $('#new_topic');
409
+ if ($newTopicBtn.length) {
410
+ $newTopicBtn.trigger('click');
411
+ }
412
+ });
413
+ }
414
+
373
415
  })();
@@ -0,0 +1,163 @@
1
+ <div data-widget-area="header">
2
+ {{{each widgets.header}}}
3
+ {{widgets.header.html}}
4
+ {{{end}}}
5
+ </div>
6
+ <style>.feed .post-body .content > p:last-child { margin-bottom: 0px; }</style>
7
+ <div class="feed">
8
+ <div class="row">
9
+ <div data-widget-area="left" class="col-lg-3 col-sm-12 {{{ if !widgets.left.length }}}hidden{{{ end }}}">
10
+ {{{each widgets.left}}}
11
+ {{widgets.left.html}}
12
+ {{{end}}}
13
+ </div>
14
+ {{{ if ((widgets.left.length && widgets.right.length) || (!widgets.left.length && !widgets.right.length))}}}
15
+ <div class="col-lg-6 col-sm-12 mx-auto">
16
+ {{{ end }}}
17
+ {{{ if (widgets.left.length && !widgets.right.length) }}}
18
+ <div class="col-lg-6 col-sm-12 me-auto">
19
+ {{{ end }}}
20
+ {{{ if (!widgets.left.length && widgets.right.length) }}}
21
+ <div class="col-lg-6 col-sm-12 ms-auto">
22
+ {{{ end }}}
23
+
24
+ <!-- JAVIS: LinkedIn-style Composer Prompt Card -->
25
+ <div class="composer-prompt-card{{{ if !loggedIn }}} composer-prompt-guest{{{ end }}}">
26
+ <div class="composer-prompt-inner">
27
+ <div class="composer-prompt-avatar">
28
+ {{{ if loggedIn }}}
29
+ {buildAvatar(loggedInUser, "48px", true, "avatar avatar-rounded")}
30
+ {{{ else }}}
31
+ <div class="avatar avatar-rounded avatar-placeholder"><i class="fa fa-user"></i></div>
32
+ {{{ end }}}
33
+ </div>
34
+ <button class="composer-prompt-input" type="button" id="composer-prompt-btn">
35
+ <span class="composer-prompt-placeholder">{{{ if loggedIn }}}Start a discussion...{{{ else }}}Sign in to start a discussion...{{{ end }}}</span>
36
+ </button>
37
+ {{{ if loggedIn }}}
38
+ <div class="composer-prompt-actions">
39
+ <button class="composer-prompt-action" type="button" title="Add image"><i class="fa fa-image"></i></button>
40
+ <button class="composer-prompt-action" type="button" title="Add link"><i class="fa fa-link"></i></button>
41
+ </div>
42
+ {{{ end }}}
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Hidden controls row - New Topic button kept for JS functionality -->
47
+ <div class="d-flex justify-content-between py-2 mb-2 gap-1 feed-controls-row">
48
+ {{{ if canPost }}}
49
+ <button id="new_topic" class="btn btn-primary btn-sm d-none">[[category:new-topic-button]]</button>
50
+ {{{ end }}}
51
+ {{{ if (!loggedIn && !canPost) }}}
52
+ <a href="{config.relative_path}/login" class="btn btn-primary btn-sm d-none">[[category:guest-login-post]]</a>
53
+ {{{ end }}}
54
+
55
+ <div class="d-flex justify-content-end gap-1 ms-auto">
56
+ <!-- IMPORT partials/category/filter-dropdown-right.tpl -->
57
+
58
+ <div id="options-dropdown" class="btn-group dropdown dropdown-right bottom-sheet">
59
+ <button type="button" class="btn btn-ghost btn-sm d-flex align-items-center gap-2 ff-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
60
+ <i class="fa fa-fw fa-gear text-primary"></i>
61
+ </button>
62
+ <ul class="dropdown-menu p-1 text-sm" role="menu">
63
+ <li class="py-1 px-3">
64
+ <div class="form-check form-switch d-flex px-0 align-items-center justify-content-between gap-3">
65
+ <label class="form-check-label text-nowrap" for="showAllPosts">[[feed:show-all-posts]]</label>
66
+ <input class="form-check-input float-none m-0 pointer" type="checkbox" role="switch" id="showAllPosts" {{{ if showAllPosts }}}checked{{{ end }}}>
67
+ </div>
68
+ </li>
69
+ {{{ if loggedIn }}}
70
+ <li class="py-1 px-3">
71
+ <div class="form-check form-switch d-flex px-0 align-items-center justify-content-between gap-3">
72
+ <label class="form-check-label text-nowrap" for="showFollowedUsers">[[feed:followed-users-only]]</label>
73
+ <input class="form-check-input float-none m-0 pointer" type="checkbox" role="switch" id="showFollowedUsers" {{{ if showFollowed }}}checked{{{ end }}}>
74
+ </div>
75
+ </li>
76
+ {{{ end }}}
77
+ </ul>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ {{{ if !posts.length }}}
83
+ <div class="alert alert-warning text-center">[[feed:no-posts-found]] {{{ if !following.length }}}[[feed:are-you-following-anyone]] {{{ end }}}</div>
84
+ {{{ end }}}
85
+
86
+ <ul component="posts" class="list-unstyled" data-nextstart="{nextStart}">
87
+ {{{ each posts }}}
88
+ <li component="post" class="shadow-sm mb-3 rounded-2 border posts-list-item {{{ if ./deleted }}} deleted{{{ else }}}{{{ if ./topic.deleted }}} deleted{{{ end }}}{{{ end }}}{{{ if ./topic.scheduled }}} scheduled{{{ end }}}" data-pid="{./pid}" data-uid="{./uid}">
89
+
90
+ {{{ if (showThumbs && ./topic.thumbs.length)}}}
91
+ <div class="p-1 position-relative">
92
+ <div class="overflow-hidden rounded-1" style="max-height: 300px;">
93
+ <a href="{config.relative_path}/topic/{./topic.slug}">
94
+ <img class="w-100" src="{./topic.thumbs.0.url}">
95
+ </a>
96
+ </div>
97
+
98
+ <div class="position-absolute end-0 bottom-0 p-3 d-flex gap-2 align-items-center pe-none">
99
+ {{{ each ./topic.thumbs }}}
100
+ {{{ if (@index != 0) }}}
101
+ <img class="rounded-1" style="max-height: 64px; object-fit: contain;" src="{./url}">
102
+ {{{ end }}}
103
+ {{{ end }}}
104
+ </div>
105
+ </div>
106
+ {{{ end }}}
107
+
108
+ <div class="d-flex gap-2 p-3">
109
+ <div class="d-none d-lg-block">
110
+ <a class="lh-1 text-decoration-none" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(./user, "40px", true, "not-responsive")}</a>
111
+ </div>
112
+ <div class="post-body d-flex flex-column gap-2 flex-grow-1 hover-parent" style="min-width: 0px;">
113
+ {{{ if ./isMainPost }}}
114
+ <a class="lh-1 topic-title fw-semibold fs-5 text-reset text-break d-block" href="{config.relative_path}/topic/{./topic.slug}">
115
+ {./topic.title}
116
+ </a>
117
+ {{{ end }}}
118
+
119
+ <div class="d-flex gap-1 post-info text-sm align-items-center">
120
+ <div class="post-author d-flex align-items-center gap-1">
121
+ <a class="d-inline d-lg-none lh-1 text-decoration-none" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(./user, "16px", true, "not-responsive")}</a>
122
+ <a class="lh-normal fw-semibold text-nowrap" href="{config.relative_path}/user/{./user.userslug}">{./user.displayname}</a>
123
+ </div>
124
+ {{{ if !./isMainPost}}}{./repliedString}{{{ else }}}<span class="timeago text-muted lh-normal" title="{./timestampISO}"></span>{{{ end}}}
125
+ </div>
126
+
127
+ <div component="post/content" class="content text-sm text-break position-relative truncate-post-content">
128
+ <a href="{config.relative_path}/post/{./pid}" class="stretched-link"></a>
129
+ {./content}
130
+ </div>
131
+ <div class="position-relative hover-visible">
132
+ <button component="show/more" class="btn btn-light btn-sm rounded-pill position-absolute start-50 translate-middle-x bottom-0 z-1 hidden ff-secondary">[[feed:see-more]]</button>
133
+ </div>
134
+ <hr class="my-2"/>
135
+ <div class="d-flex justify-content-between">
136
+ <a href="{config.relative_path}/post/{{{ if ./topic.teaserPid }}}{./topic.teaserPid}{{{ else }}}{./pid}{{{ end }}}" class="btn btn-link btn-sm text-body {{{ if !./isMainPost }}}invisible{{{ end }}}"><i class="fa-fw fa-regular fa-message text-muted"></i> {humanReadableNumber(./topic.postcount)}</a>
137
+
138
+ <a href="#" data-pid="{./pid}" data-action="bookmark" data-bookmarked="{./bookmarked}" data-bookmarks="{./bookmarks}" class="btn btn-link btn-sm text-body"><i class="fa-fw fa-bookmark {{{ if ./bookmarked }}}fa text-primary{{{ else }}}fa-regular text-muted{{{ end }}}"></i> <span component="bookmark-count">{humanReadableNumber(./bookmarks)}</span></a>
139
+
140
+ <a href="#" data-pid="{./pid}" data-action="upvote" data-upvoted="{./upvoted}" data-upvotes="{./upvotes}" class="btn btn-link btn-sm text-body"><i class="fa-fw fa-heart {{{ if ./upvoted }}}fa text-danger{{{ else }}}fa-regular text-muted{{{ end }}}"></i> <span component="upvote-count">{humanReadableNumber(./upvotes)}</span></a>
141
+
142
+ <a href="#" data-pid="{./pid}" data-is-main="{./isMainPost}" data-tid="{./tid}" data-action="reply" class="btn btn-link btn-sm text-body"><i class="fa-fw fa fa-reply text-muted"></i> [[topic:reply]]</a>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </li>
147
+ {{{ end }}}
148
+ </ul>
149
+ </div>
150
+
151
+ <div data-widget-area="right" class="col-lg-3 col-sm-12 {{{ if !widgets.right.length }}}hidden{{{ end }}}">
152
+ {{{each widgets.right}}}
153
+ {{widgets.right.html}}
154
+ {{{end}}}
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <div data-widget-area="footer">
160
+ {{{each widgets.footer}}}
161
+ {{widgets.footer.html}}
162
+ {{{end}}}
163
+ </div>
package/theme.scss CHANGED
@@ -21,3 +21,4 @@
21
21
  @import "./scss/categories";
22
22
  @import "./scss/feed";
23
23
  @import "./scss/topic";
24
+ @import "./scss/composer";