@okjavis/nodebb-theme-javis 2.5.2 → 3.0.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.5.2",
3
+ "version": "3.0.0",
4
4
  "description": "Modern, premium NodeBB theme for JAVIS Community - Extends Harmony with custom styling",
5
5
  "main": "theme.js",
6
6
  "scripts": {
@@ -34,6 +34,7 @@
34
34
  "scss/",
35
35
  "static/",
36
36
  "templates/",
37
+ "public/",
37
38
  "theme.js",
38
39
  "theme.scss",
39
40
  "theme.json",
package/plugin.json CHANGED
@@ -9,8 +9,7 @@
9
9
  { "hook": "static:app.load", "method": "init" },
10
10
  { "hook": "filter:widgets.getAreas", "method": "defineWidgetAreas" },
11
11
  { "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
12
- { "hook": "filter:config.get", "method": "getThemeConfig" },
13
- { "hook": "filter:settings.get", "method": "getAdminSettings" }
12
+ { "hook": "filter:config.get", "method": "getThemeConfig" }
14
13
  ],
15
14
  "staticDirs": {
16
15
  "static": "./static"
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ define('admin/plugins/javis', ['settings'], function (Settings) {
4
+ var ACP = {};
5
+
6
+ ACP.init = function () {
7
+ // Load settings from 'harmony' key for compatibility with parent theme
8
+ Settings.load('harmony', $('.javis-settings'));
9
+
10
+ $('#save').on('click', function () {
11
+ // Save to 'harmony' key for compatibility
12
+ Settings.save('harmony', $('.javis-settings'));
13
+ });
14
+ };
15
+
16
+ return ACP;
17
+ });
package/scss/_cards.scss CHANGED
@@ -43,10 +43,10 @@
43
43
  flex-direction: column;
44
44
  align-items: center;
45
45
  justify-content: flex-start;
46
- padding: $jv-space-6 $jv-space-4; // 24dp vertical, 16dp horizontal - Material grid
46
+ padding: $jv-space-4 $jv-space-3; // Tighter padding
47
47
  background: rgba(0, 0, 0, 0.02);
48
- min-width: 44px;
49
- gap: $jv-space-1; // 4dp - Material grid
48
+ min-width: 40px;
49
+ gap: 2px; // Minimal gap
50
50
  }
51
51
 
52
52
  .vote-btn {
@@ -100,10 +100,10 @@
100
100
  // ===========================================================
101
101
  .topic-content {
102
102
  flex: 1;
103
- padding: $jv-space-3; // 12dp - Material grid
103
+ padding: $jv-space-3;
104
104
  display: flex;
105
105
  flex-direction: column;
106
- gap: $jv-space-3; // 12dp - Material grid
106
+ gap: $jv-space-2; // Tighter gap between elements
107
107
  min-width: 0; // Allow text truncation
108
108
  }
109
109
 
@@ -147,10 +147,10 @@
147
147
  // Topic Title
148
148
  .topic-card .topic-title {
149
149
  margin: 0;
150
- font-size: 20px; // Larger for prominence - consistent with feed
151
- font-weight: 700; // Bolder for better hierarchy
152
- line-height: 1.4; // Better readability
153
- letter-spacing: -0.02em; // Tighter for modern feel
150
+ font-size: $jv-font-size-lg; // 18px - clear hierarchy
151
+ font-weight: 600; // Semi-bold
152
+ line-height: $jv-line-height-tight;
153
+ letter-spacing: -0.02em;
154
154
 
155
155
  a {
156
156
  color: $jv-text-main;
@@ -260,7 +260,7 @@
260
260
  color: $jv-text-muted;
261
261
  line-height: $jv-line-height-base;
262
262
  display: -webkit-box;
263
- -webkit-line-clamp: 3;
263
+ -webkit-line-clamp: 2; // Reduced from 3 to 2 lines
264
264
  -webkit-box-orient: vertical;
265
265
  overflow: hidden;
266
266
 
@@ -270,6 +270,54 @@
270
270
  }
271
271
  }
272
272
 
273
+ // ===========================================================
274
+ // MEDIA ROW (Image + Text side by side)
275
+ // ===========================================================
276
+ .topic-media-row {
277
+ display: flex;
278
+ gap: $jv-space-4;
279
+ align-items: flex-start;
280
+
281
+ .topic-media-text {
282
+ flex: 1;
283
+ min-width: 0; // Allow text truncation
284
+
285
+ .topic-teaser {
286
+ -webkit-line-clamp: 3; // Allow more lines when beside image
287
+ }
288
+ }
289
+
290
+ .topic-thumbnail {
291
+ flex-shrink: 0;
292
+ width: 140px;
293
+ height: 100px;
294
+
295
+ img {
296
+ width: 100%;
297
+ height: 100%;
298
+ object-fit: cover;
299
+ }
300
+ }
301
+ }
302
+
303
+ @media (max-width: 576px) {
304
+ .topic-media-row {
305
+ flex-direction: column-reverse;
306
+ gap: $jv-space-3;
307
+
308
+ .topic-thumbnail {
309
+ width: 100%;
310
+ height: auto;
311
+ max-height: 180px;
312
+
313
+ img {
314
+ max-height: 180px;
315
+ width: 100%;
316
+ }
317
+ }
318
+ }
319
+ }
320
+
273
321
  // ===========================================================
274
322
  // ACTION BAR (Bottom - Reddit style)
275
323
  // ===========================================================
@@ -298,21 +346,21 @@ li[component="post"] hr,
298
346
  .action-btn {
299
347
  display: inline-flex;
300
348
  align-items: center;
301
- gap: $jv-space-2; // 8dp grid
302
- padding: $jv-space-2 $jv-space-3; // 8dp vertical, 12dp horizontal
303
- font-size: 14px;
304
- font-weight: 600;
305
- color: $jv-text-muted;
349
+ gap: $jv-space-1; // Tighter gap
350
+ padding: $jv-space-1 $jv-space-2; // Smaller padding
351
+ font-size: $jv-font-size-xs; // Smaller font
352
+ font-weight: 500; // Lighter weight
353
+ color: $jv-text-soft; // More subtle color
306
354
  background: transparent;
307
355
  border: none;
308
- border-radius: $jv-radius-sm; // 8px shadcn standard
356
+ border-radius: $jv-radius-xs;
309
357
  cursor: pointer;
310
358
  text-decoration: none;
311
- transition: background-color $jv-transition-fast, color $jv-transition-fast; // Modern transitions
359
+ transition: background-color $jv-transition-fast, color $jv-transition-fast;
312
360
 
313
361
  &:hover {
314
- background: $jv-hover-bg; // Consistent hover state
315
- color: $jv-text-main;
362
+ background: $jv-hover-bg;
363
+ color: $jv-text-muted;
316
364
  }
317
365
 
318
366
  // Focus state for accessibility
@@ -322,12 +370,11 @@ li[component="post"] hr,
322
370
  }
323
371
 
324
372
  i {
325
- font-size: 18px; // Larger icons
373
+ font-size: 14px; // Smaller icons
326
374
  }
327
375
 
328
- // Make numbers bold
329
376
  span {
330
- font-weight: 700;
377
+ font-weight: 500; // Normal weight, not bold
331
378
  }
332
379
  }
333
380
 
@@ -1,58 +1,93 @@
1
1
  // ===========================================================
2
- // CATEGORIES – Enterprise Clean Style (Linear-inspired)
2
+ // CATEGORIES – Reddit/Community Style Cards
3
+ // Matches topic-card design for visual consistency
3
4
  // ===========================================================
4
5
 
5
6
  // Category Header
6
7
  .feed h2 {
7
- font-size: 16px;
8
+ font-size: $jv-font-size-base;
8
9
  font-weight: 600;
9
- margin: 0 0 $jv-space-6 0; // 24dp - Material grid
10
+ margin: 0 0 $jv-space-6 0;
10
11
  padding: 0;
11
12
  color: $jv-text-main;
12
13
  letter-spacing: -0.2px;
13
14
  }
14
15
 
15
- // Wrapper list spacing
16
+ // Wrapper list spacing - cards with gaps
16
17
  ul.categories-list.list-unstyled {
17
18
  margin: 0;
18
19
  padding: 0;
19
20
  display: flex;
20
21
  flex-direction: column;
21
- gap: 0;
22
+ gap: $jv-space-3;
23
+
24
+ // First category gets subtle featured treatment
25
+ > li[component="categories/category"]:first-child {
26
+ background: linear-gradient(135deg, rgba($jv-primary, 0.04) 0%, rgba($jv-primary, 0.01) 100%);
27
+ border-color: rgba($jv-primary, 0.2);
28
+
29
+ &:hover {
30
+ border-color: rgba($jv-primary, 0.35);
31
+ }
32
+ }
22
33
  }
23
34
 
24
35
  // ===========================================================
25
- // PARENT CATEGORY BLOCK
36
+ // PARENT CATEGORY CARD (Consolidated selector)
26
37
  // ===========================================================
27
38
 
28
39
  li[component="categories/category"] {
29
- padding: $jv-space-4 0; // 16dp grid
30
- border-bottom: 1px solid #f1f1f1;
31
- transition: background $jv-transition-fast; // Modern transition
32
-
33
- &:last-child {
34
- border-bottom: none;
35
- }
40
+ position: relative;
41
+ background: $jv-surface;
42
+ border-radius: $jv-radius-sm;
43
+ border: 1px solid $jv-border-subtle;
44
+ box-shadow: $jv-shadow-sm;
45
+ padding: $jv-space-5;
46
+ transition: box-shadow $jv-transition-base, border-color $jv-transition-fast, transform $jv-transition-fast;
36
47
 
37
48
  &:hover {
38
- background: $jv-hover-bg; // Consistent hover state
49
+ box-shadow: $jv-shadow-md;
50
+ border-color: $jv-border-strong;
51
+ transform: translateY(-1px);
52
+
53
+ .subcategory-pill {
54
+ background: $jv-surface;
55
+ }
39
56
  }
40
57
 
41
- // Focus state for accessibility
42
58
  &:focus-within {
43
59
  outline: none;
44
- background: $jv-hover-bg;
60
+ box-shadow: $jv-shadow-md, $jv-focus-ring;
61
+ }
62
+
63
+ // Unread state
64
+ &.unread {
65
+ border-left: 3px solid $jv-primary;
66
+
67
+ .title a {
68
+ font-weight: 700;
69
+ }
70
+ }
71
+
72
+ // New posts indicator
73
+ &.new-posts {
74
+ .activity-badge.active {
75
+ animation: pulse 2s ease-in-out infinite;
76
+ }
45
77
  }
46
78
 
47
- // Parent title
79
+ // ------------------------------------
80
+ // Title
81
+ // ------------------------------------
48
82
  .title {
49
- font-size: $jv-font-size-base;
83
+ font-size: $jv-font-size-lg;
50
84
  font-weight: 600;
51
85
  color: $jv-text-main;
52
- margin-bottom: $jv-space-1; // 4dp - Material grid
86
+ margin-bottom: $jv-space-1; // Tighter spacing to description
53
87
  display: flex;
54
88
  align-items: center;
55
- gap: $jv-space-2; // 8dp - Material grid
89
+ gap: $jv-space-3;
90
+ letter-spacing: -0.02em;
56
91
 
57
92
  a {
58
93
  text-decoration: none;
@@ -69,51 +104,173 @@ li[component="categories/category"] {
69
104
  }
70
105
  }
71
106
 
72
- // Category description
107
+ // ------------------------------------
108
+ // Description
109
+ // ------------------------------------
73
110
  .description {
74
111
  font-size: $jv-font-size-sm;
75
112
  color: $jv-text-muted;
76
- line-height: 1.45;
77
- margin-bottom: $jv-space-2; // 8dp - Material grid
78
- max-width: 92%;
113
+ line-height: $jv-line-height-base;
114
+ margin-bottom: $jv-space-3;
115
+ max-width: 100%;
116
+ }
117
+
118
+ // ------------------------------------
119
+ // Category Icon
120
+ // ------------------------------------
121
+ .category-icon,
122
+ [component="category/icon"] {
123
+ width: 48px !important;
124
+ height: 48px !important;
125
+ border-radius: $jv-radius-sm !important;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ font-size: $jv-font-size-lg !important;
130
+ flex-shrink: 0;
131
+ box-shadow: $jv-shadow-xs;
132
+ }
133
+
134
+ // ------------------------------------
135
+ // Stats - Minimal inline design
136
+ // ------------------------------------
137
+ .stats-minimal {
138
+ .stat-item {
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: center;
142
+ gap: 2px;
143
+ }
144
+
145
+ .stat-number {
146
+ font-size: $jv-font-size-lg;
147
+ font-weight: 600;
148
+ color: $jv-text-main;
149
+ line-height: 1;
150
+ }
151
+
152
+ .stat-label {
153
+ font-size: $jv-font-size-xs;
154
+ color: $jv-text-soft;
155
+ text-transform: uppercase;
156
+ letter-spacing: 0.3px;
157
+ font-weight: 500;
158
+ }
159
+ }
160
+
161
+ // ------------------------------------
162
+ // Last Post Teaser
163
+ // ------------------------------------
164
+ .teaser {
165
+ // Reset inherited spacing from NodeBB
166
+ padding-top: 0 !important;
167
+ margin-top: 0 !important;
168
+ border-top: none !important;
169
+
170
+ .teaser-enhanced {
171
+ border-left: 3px solid var(--teaser-color, $jv-primary);
172
+ height: auto !important;
173
+ border-radius: 0 $jv-radius-sm $jv-radius-sm 0;
174
+ background: $jv-bg;
175
+ padding: $jv-space-3;
176
+ transition: background $jv-transition-fast;
177
+
178
+ &:hover {
179
+ background: darken($jv-bg, 2%);
180
+ }
181
+
182
+ .teaser-header {
183
+ .avatar-tooltip img,
184
+ [component="user/picture"] {
185
+ width: 24px !important;
186
+ height: 24px !important;
187
+ border-radius: $jv-radius-pill;
188
+ }
189
+
190
+ .teaser-meta {
191
+ display: flex;
192
+ flex-direction: column;
193
+ gap: 0;
194
+ line-height: 1.2;
195
+
196
+ .teaser-user {
197
+ font-size: $jv-font-size-xs;
198
+ font-weight: 600;
199
+ color: $jv-text-main;
200
+ }
201
+
202
+ .teaser-time {
203
+ font-size: $jv-font-size-xs;
204
+ color: $jv-text-soft;
205
+ text-decoration: none;
206
+ }
207
+ }
208
+ }
209
+
210
+ .teaser-content-wrapper {
211
+ display: flex;
212
+ flex-direction: column;
213
+ gap: $jv-space-1;
214
+ }
215
+
216
+ .teaser-content {
217
+ font-size: $jv-font-size-sm;
218
+ color: $jv-text-muted;
219
+ line-height: $jv-line-height-base;
220
+ display: -webkit-box;
221
+ -webkit-line-clamp: 2;
222
+ -webkit-box-orient: vertical;
223
+ overflow: hidden;
224
+ }
225
+
226
+ .teaser-read-more {
227
+ font-size: $jv-font-size-xs;
228
+ font-weight: 500;
229
+ color: $jv-primary;
230
+ text-decoration: none;
231
+ transition: color $jv-transition-fast;
232
+
233
+ &:hover {
234
+ color: darken($jv-primary, 10%);
235
+ text-decoration: underline;
236
+ }
237
+ }
238
+ }
79
239
  }
80
240
  }
81
241
 
82
242
  // ===========================================================
83
- // CHILD CATEGORY ITEMS – Linear-style tiny dot list
243
+ // CHILD CATEGORY ITEMS – Subcategory pills
84
244
  // ===========================================================
85
245
 
86
- li[component="categories/category"] .category-children {
246
+ .category-children {
87
247
  display: flex;
88
248
  flex-direction: column;
89
- gap: $jv-space-2; // 8dp - Material grid
90
- margin-top: $jv-space-1; // 4dp - Material grid
249
+ gap: $jv-space-2;
250
+ margin-top: $jv-space-1;
91
251
  }
92
252
 
93
253
  .category-children-item {
94
254
  small > .d-flex {
95
255
  display: flex;
96
256
  align-items: center;
97
- gap: $jv-space-2; // 8dp - Material grid
98
- font-size: 14px;
99
- color: #333;
257
+ gap: $jv-space-2;
258
+ font-size: $jv-font-size-sm;
259
+ color: $jv-text-main;
100
260
  }
101
261
 
102
- // Dot (neutral by default)
103
262
  i {
104
263
  font-size: 6px;
105
- color: #999;
264
+ color: $jv-text-soft;
106
265
  margin-top: 1px;
107
266
  }
108
267
 
109
- // Child link
110
268
  a {
111
269
  text-decoration: none;
112
- font-size: 14px;
113
- color: #333;
114
- transition: color $jv-transition-fast; // Modern transition
270
+ font-size: $jv-font-size-sm;
271
+ color: $jv-text-main;
272
+ transition: color $jv-transition-fast;
115
273
 
116
- // Focus state for accessibility
117
274
  &:focus-visible {
118
275
  outline: none;
119
276
  color: $jv-primary;
@@ -123,7 +280,6 @@ li[component="categories/category"] .category-children {
123
280
  }
124
281
  }
125
282
 
126
- // Hover state: blue dot + blue text
127
283
  &:hover {
128
284
  i {
129
285
  color: $jv-primary;
@@ -134,3 +290,232 @@ li[component="categories/category"] .category-children {
134
290
  }
135
291
  }
136
292
  }
293
+
294
+ // ===========================================================
295
+ // ACTIVITY BADGES
296
+ // ===========================================================
297
+
298
+ .category-activity-row {
299
+ margin-top: $jv-space-1;
300
+ }
301
+
302
+ .activity-badge {
303
+ display: inline-flex;
304
+ align-items: center;
305
+ gap: $jv-space-1;
306
+ padding: $jv-space-1 $jv-space-2;
307
+ font-size: $jv-font-size-xs;
308
+ font-weight: 500;
309
+ color: $jv-text-soft;
310
+ background: $jv-bg;
311
+ border-radius: $jv-radius-pill;
312
+ transition: all $jv-transition-fast;
313
+ cursor: default;
314
+
315
+ i {
316
+ font-size: $jv-font-size-xs;
317
+ }
318
+
319
+ &:hover {
320
+ transform: scale(1.02);
321
+ }
322
+
323
+ // Removed .active green styling - now uses default muted style for time badge
324
+
325
+ &.hot {
326
+ background: rgba($jv-warning, 0.12);
327
+ color: $jv-warning;
328
+
329
+ i {
330
+ color: $jv-warning;
331
+ }
332
+ }
333
+
334
+ &.new {
335
+ background: $jv-primary-soft;
336
+ color: $jv-primary;
337
+
338
+ i {
339
+ color: $jv-primary;
340
+ }
341
+ }
342
+ }
343
+
344
+ // ===========================================================
345
+ // CATEGORY ICON ENHANCED (with glow effect)
346
+ // ===========================================================
347
+
348
+ .category-icon-enhanced {
349
+ position: relative;
350
+
351
+ &::after {
352
+ content: '';
353
+ position: absolute;
354
+ inset: -2px;
355
+ border-radius: inherit;
356
+ background: inherit;
357
+ opacity: 0.2;
358
+ z-index: -1;
359
+ filter: blur(4px);
360
+ }
361
+ }
362
+
363
+ // ===========================================================
364
+ // EMPTY STATE
365
+ // ===========================================================
366
+
367
+ .empty-state {
368
+ color: $jv-text-soft !important;
369
+ font-style: italic;
370
+ }
371
+
372
+ .empty-category-state {
373
+ display: flex;
374
+ align-items: center;
375
+ height: 100%;
376
+
377
+ .empty-icon {
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ width: 28px;
382
+ height: 28px;
383
+ border-radius: $jv-radius-pill;
384
+ background: $jv-bg;
385
+ color: $jv-text-soft;
386
+ font-size: $jv-font-size-sm;
387
+ }
388
+
389
+ .empty-text {
390
+ font-size: $jv-font-size-xs;
391
+ color: $jv-text-soft;
392
+ font-weight: 500;
393
+ }
394
+ }
395
+
396
+ // ===========================================================
397
+ // SUBCATEGORY PILLS
398
+ // ===========================================================
399
+
400
+ .subcategory-pills {
401
+ margin-top: $jv-space-2;
402
+ }
403
+
404
+ .subcategory-pill {
405
+ display: inline-flex;
406
+ align-items: center;
407
+ gap: $jv-space-2;
408
+ padding: $jv-space-1 $jv-space-3 $jv-space-1 $jv-space-1;
409
+ background: $jv-bg;
410
+ border: 1px solid transparent;
411
+ border-radius: $jv-radius-pill;
412
+ text-decoration: none;
413
+ transition: all $jv-transition-fast;
414
+
415
+ .pill-icon {
416
+ display: flex;
417
+ align-items: center;
418
+ justify-content: center;
419
+ width: 22px;
420
+ height: 22px;
421
+ border-radius: $jv-radius-pill;
422
+ color: white;
423
+ font-size: $jv-font-size-xs;
424
+ flex-shrink: 0;
425
+
426
+ i {
427
+ line-height: 1;
428
+ }
429
+ }
430
+
431
+ .pill-name {
432
+ font-size: $jv-font-size-xs;
433
+ font-weight: 500;
434
+ color: $jv-text-main;
435
+ white-space: nowrap;
436
+ }
437
+
438
+ &:hover {
439
+ background: $jv-surface;
440
+ border-color: var(--pill-color, $jv-border-subtle);
441
+ box-shadow: $jv-shadow-sm;
442
+ transform: translateY(-1px);
443
+
444
+ .pill-name {
445
+ color: var(--pill-color, $jv-primary);
446
+ }
447
+ }
448
+
449
+ &:focus-visible {
450
+ outline: none;
451
+ box-shadow: $jv-focus-ring;
452
+ }
453
+ }
454
+
455
+ // ===========================================================
456
+ // ANIMATIONS
457
+ // ===========================================================
458
+
459
+ @keyframes pulse {
460
+ 0%, 100% {
461
+ opacity: 1;
462
+ }
463
+ 50% {
464
+ opacity: 0.6;
465
+ }
466
+ }
467
+
468
+ // ===========================================================
469
+ // RESPONSIVE ADJUSTMENTS
470
+ // ===========================================================
471
+
472
+ @media (max-width: 991px) {
473
+ li[component="categories/category"] {
474
+ padding: $jv-space-4;
475
+
476
+ .teaser {
477
+ margin-top: $jv-space-3;
478
+ }
479
+ }
480
+ }
481
+
482
+ @media (max-width: 576px) {
483
+ ul.categories-list.list-unstyled {
484
+ gap: $jv-space-2;
485
+ }
486
+
487
+ li[component="categories/category"] {
488
+ padding: $jv-space-4;
489
+ border-radius: $jv-radius-sm;
490
+
491
+ .title {
492
+ font-size: $jv-font-size-base;
493
+ }
494
+
495
+ .description {
496
+ font-size: $jv-font-size-xs;
497
+ }
498
+
499
+ .category-activity-row {
500
+ flex-wrap: wrap;
501
+ }
502
+ }
503
+
504
+ .subcategory-pills {
505
+ gap: $jv-space-1 !important;
506
+ }
507
+
508
+ .subcategory-pill {
509
+ padding: $jv-space-1 $jv-space-2 $jv-space-1 $jv-space-1;
510
+
511
+ .pill-icon {
512
+ width: 18px;
513
+ height: 18px;
514
+ font-size: $jv-font-size-xs;
515
+ }
516
+
517
+ .pill-name {
518
+ font-size: $jv-font-size-xs;
519
+ }
520
+ }
521
+ }
package/scss/_topic.scss CHANGED
@@ -63,7 +63,7 @@ body.template-topic {
63
63
  display: flex;
64
64
  flex-wrap: wrap;
65
65
  gap: $jv-space-2;
66
- margin-bottom: $jv-space-4;
66
+ margin-bottom: 0;
67
67
 
68
68
  .badge,
69
69
  .tag {
@@ -0,0 +1,95 @@
1
+ <!-- IMPORT partials/breadcrumbs-json-ld.tpl -->
2
+ {{{ if config.theme.enableBreadcrumbs }}}
3
+ <!-- IMPORT partials/breadcrumbs.tpl -->
4
+ {{{ end }}}
5
+
6
+ <div class="category-header d-flex flex-column gap-2">
7
+ <div class="d-flex gap-2 align-items-center mb-1 {{{ if config.theme.centerHeaderElements }}}justify-content-center{{{ end }}}">
8
+ {buildCategoryIcon(@value, "40px", "rounded-1 flex-shrink-0")}
9
+ <h1 class="tracking-tight fs-2 fw-semibold mb-0">{./name}</h1>
10
+ </div>
11
+ {{{ if ./descriptionParsed }}}
12
+ <div class="description text-secondary text-sm w-100 {{{ if config.theme.centerHeaderElements }}}text-center{{{ end }}} line-clamp-4 clamp-fade-4">
13
+ {./descriptionParsed}
14
+ </div>
15
+ {{{ end }}}
16
+ <!-- ActivityPub handle - hidden by default, can show via tooltip -->
17
+ {{{ if ./handleFull }}}
18
+ <div class="activitypub-handle d-none">
19
+ <p class="text-secondary text-sm fst-italic mb-0">
20
+ [[category:handle.description, {handleFull}]]
21
+ <a href="#" class="link-secondary" data-action="copy" data-clipboard-text="{handleFull}"><i class="fa fa-fw fa-copy" aria-hidden="true"></i></a>
22
+ </p>
23
+ </div>
24
+ {{{ end }}}
25
+ <div class="d-flex flex-wrap gap-2 {{{ if config.theme.centerHeaderElements }}}justify-content-center{{{ end }}}">
26
+ <span class="badge text-body border border-gray-300 stats text-xs">
27
+ <span title="{totalTopicCount}" class="fw-bold">{humanReadableNumber(totalTopicCount)}</span>
28
+ <span class="text-lowercase fw-normal">[[global:topics]]</span>
29
+ </span>
30
+ <span class="badge text-body border border-gray-300 stats text-xs">
31
+ <span title="{totalPostCount}" class="fw-bold">{humanReadableNumber(totalPostCount)}</span>
32
+ <span class="text-lowercase fw-normal">[[global:posts]]</span>
33
+ </span>
34
+ {{{ if !isNumber(cid) }}}
35
+ <a href="{./url}" class="badge text-body border border-gray-300 text-xs" data-ajaxify="false">
36
+ <span class="fw-normal">View Original</span>
37
+ <i class="fa fa-external-link"></i>
38
+ </a>
39
+ {{{ end }}}
40
+ </div>
41
+ </div>
42
+
43
+ {{{ if widgets.header.length }}}
44
+ <div data-widget-area="header">
45
+ {{{ each widgets.header }}}
46
+ {{widgets.header.html}}
47
+ {{{ end }}}
48
+ </div>
49
+ {{{ end }}}
50
+
51
+
52
+ <div class="row flex-fill mt-3">
53
+ <div class="category d-flex flex-column {{{if widgets.sidebar.length }}}col-lg-9 col-sm-12{{{ else }}}col-lg-12{{{ end }}}">
54
+ <!-- IMPORT partials/category/subcategory.tpl -->
55
+ {{{ if (topics.length || privileges.topics:create) }}}
56
+ <!-- IMPORT partials/topic-list-bar.tpl -->
57
+ {{{ end }}}
58
+
59
+ {{{ if (./inbox && (./hasFollowers == false)) }}}
60
+ <div class="alert alert-warning mb-4" id="category-no-followers" data-bs-toggle="dropdown" data-bs-target='[component="topic/watch"] button' aria-hidden="true">
61
+ <i class="fa fa-triangle-exclamation pe-2"></i>
62
+ [[category:no-followers]]
63
+ <a href="#" class="stretched-link"></a>
64
+ </div>
65
+ {{{ end }}}
66
+
67
+ {{{ if (!topics.length && privileges.topics:create) }}}
68
+ <div class="alert alert-info" id="category-no-topics">
69
+ [[category:no-topics]]
70
+ </div>
71
+ {{{ end }}}
72
+
73
+ <!-- IMPORT partials/topics_list.tpl -->
74
+
75
+ {{{ if config.usePagination }}}
76
+ <!-- IMPORT partials/paginator.tpl -->
77
+ {{{ end }}}
78
+ </div>
79
+ <div data-widget-area="sidebar" class="col-lg-3 col-sm-12 {{{ if !widgets.sidebar.length }}}hidden{{{ end }}}">
80
+ {{{ each widgets.sidebar }}}
81
+ {{widgets.sidebar.html}}
82
+ {{{ end }}}
83
+ </div>
84
+ </div>
85
+ <div data-widget-area="footer">
86
+ {{{each widgets.footer}}}
87
+ {{widgets.footer.html}}
88
+ {{{end}}}
89
+ </div>
90
+
91
+ {{{ if !config.usePagination }}}
92
+ <noscript>
93
+ <!-- IMPORT partials/paginator.tpl -->
94
+ </noscript>
95
+ {{{ end }}}
@@ -0,0 +1,67 @@
1
+ <li component="categories/category" data-cid="{./cid}" class="category-card w-100 py-3 py-lg-4 gap-lg-0 gap-2 d-flex flex-column flex-lg-row align-items-start category-{./cid} {./unread-class}">
2
+ <meta itemprop="name" content="{./name}">
3
+
4
+ <div class="d-flex col-lg-7 gap-2 gap-lg-3">
5
+ <div class="flex-shrink-0">
6
+ {buildCategoryIcon(@value, "48px", "rounded-1 category-icon-enhanced")}
7
+ </div>
8
+ <div class="flex-grow-1 d-flex flex-wrap gap-1 me-0 me-lg-2">
9
+ <h2 class="title text-break fs-5 fw-semibold m-0 tracking-tight w-100">
10
+ <!-- IMPORT partials/categories/link.tpl -->
11
+ </h2>
12
+ {{{ if ./descriptionParsed }}}
13
+ <div class="description text-muted text-sm w-100">
14
+ {./descriptionParsed}
15
+ </div>
16
+ {{{ end }}}
17
+
18
+ <!-- Activity Badge Row -->
19
+ <div class="category-activity-row d-flex align-items-center gap-2 w-100 mt-1">
20
+ {{{ each ./posts }}}
21
+ {{{ if @first }}}
22
+ <span class="activity-badge" title="Last activity">
23
+ <i class="fa fa-clock-o"></i>
24
+ <span class="timeago" title="{./timestampISO}"></span>
25
+ </span>
26
+ {{{ end }}}
27
+ {{{ end }}}
28
+ </div>
29
+
30
+ {{{ if !config.hideSubCategories }}}
31
+ {{{ if ./children.length }}}
32
+ <div class="subcategory-pills d-flex flex-wrap gap-2 mt-2 w-100">
33
+ {{{ each ./children }}}
34
+ {{{ if !./isSection }}}
35
+ <a href="{{{ if ./link }}}{./link}{{{ else }}}{config.relative_path}/category/{./slug}{{{ end }}}" class="subcategory-pill" style="--pill-color: {./bgColor};">
36
+ <span class="pill-icon" style="background-color: {./bgColor};">
37
+ <i class="{./icon}"></i>
38
+ </span>
39
+ <span class="pill-name">{./name}</span>
40
+ </a>
41
+ {{{ end }}}
42
+ {{{ end }}}
43
+ </div>
44
+ {{{ end }}}
45
+ {{{ end }}}
46
+ </div>
47
+ </div>
48
+ {{{ if !./link }}}
49
+ <div class="d-flex col-lg-5 col-12 align-content-stretch">
50
+ <div class="meta stats-minimal d-none d-lg-flex col-4 gap-3 pe-3 text-muted align-items-center justify-content-end">
51
+ <div class="stat-item text-center">
52
+ <span class="stat-number" title="{./totalTopicCount}">{humanReadableNumber(./totalTopicCount, 0)}</span>
53
+ <span class="stat-label">topics</span>
54
+ </div>
55
+ <div class="stat-item text-center">
56
+ <span class="stat-number" title="{./totalPostCount}">{humanReadableNumber(./totalPostCount, 0)}</span>
57
+ <span class="stat-label">posts</span>
58
+ </div>
59
+ </div>
60
+ {{{ if !config.hideCategoryLastPost }}}
61
+ <div component="topic/teaser" class="teaser col-lg-8 col-12 {{{ if !config.theme.mobileTopicTeasers }}}d-none d-lg-block{{{ end }}}">
62
+ <!-- IMPORT partials/categories/lastpost.tpl -->
63
+ </div>
64
+ {{{ end }}}
65
+ </div>
66
+ {{{ end }}}
67
+ </li>
@@ -0,0 +1,30 @@
1
+ <div class="lastpost teaser-enhanced lh-sm h-100" style="--teaser-color: {./bgColor};">
2
+ {{{ each ./posts }}}
3
+ {{{ if @first }}}
4
+ <div component="category/posts" class="d-flex flex-column h-100 gap-2">
5
+ <div class="teaser-header d-flex align-items-center gap-2">
6
+ <a class="text-decoration-none avatar-tooltip" title="{./user.displayname}" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(posts.user, "24px", true)}</a>
7
+ <div class="teaser-meta">
8
+ <span class="teaser-user">{./user.displayname}</span>
9
+ <a class="permalink timeago teaser-time" href="{config.relative_path}/topic/{./topic.slug}{{{ if ./index }}}/{./index}{{{ end }}}" title="{./timestampISO}" aria-label="[[global:lastpost]]"></a>
10
+ </div>
11
+ </div>
12
+ <div class="teaser-content-wrapper position-relative flex-fill">
13
+ <div class="teaser-content text-break line-clamp-2">
14
+ {./content}
15
+ </div>
16
+ <a class="teaser-read-more" href="{config.relative_path}/topic/{./topic.slug}{{{ if ./index }}}/{./index}{{{ end }}}">Read more →</a>
17
+ </div>
18
+ </div>
19
+ {{{ end }}}
20
+ {{{ end }}}
21
+
22
+ {{{ if !./posts.length }}}
23
+ <div component="category/posts" class="ps-2 empty-category-state">
24
+ <div class="d-flex flex-column align-items-start gap-1">
25
+ <span class="empty-icon"><i class="fa fa-inbox"></i></span>
26
+ <span class="empty-text">Be the first to post</span>
27
+ </div>
28
+ </div>
29
+ {{{ end }}}
30
+ </div>
@@ -0,0 +1,5 @@
1
+ {{{ if ./isSection }}}
2
+ {./name}
3
+ {{{ else }}}
4
+ <a class="text-reset" href="{{{ if ./link }}}{./link}{{{ else }}}{config.relative_path}/category/{./slug}{{{ end }}}" itemprop="url">{../name}</a>
5
+ {{{ end }}}
@@ -69,28 +69,37 @@
69
69
  </div>
70
70
  {{{ end }}}
71
71
 
72
- <!-- Thumbnail Preview (if exists) -->
72
+ <!-- Content with optional thumbnail (side by side when image exists) -->
73
73
  {{{ if ./thumbs.length }}}
74
- <a class="topic-thumbnail" href="{config.relative_path}/topic/{./slug}{{{ if ./bookmark }}}/{./bookmark}{{{ end }}}">
75
- <img src="{./thumbs.0.url}" alt="" loading="lazy" />
76
- {{{ if ./thumbs.1 }}}
77
- <span class="thumb-count">+{subtract(./thumbs.length, 1)}</span>
78
- {{{ end }}}
79
- </a>
80
- {{{ end }}}
81
-
82
- <!-- Teaser/Preview Content -->
74
+ <div class="topic-media-row">
75
+ <div class="topic-media-text">
76
+ {{{ if ./teaser.content }}}
77
+ <div class="topic-teaser">
78
+ {./teaser.content}
79
+ </div>
80
+ {{{ end }}}
81
+ </div>
82
+ <a class="topic-thumbnail" href="{config.relative_path}/topic/{./slug}{{{ if ./bookmark }}}/{./bookmark}{{{ end }}}">
83
+ <img src="{./thumbs.0.url}" alt="" loading="lazy" />
84
+ {{{ if ./thumbs.1 }}}
85
+ <span class="thumb-count">+{subtract(./thumbs.length, 1)}</span>
86
+ {{{ end }}}
87
+ </a>
88
+ </div>
89
+ {{{ else }}}
90
+ <!-- Teaser/Preview Content (no image) -->
83
91
  {{{ if ./teaser.content }}}
84
92
  <div class="topic-teaser">
85
93
  {./teaser.content}
86
94
  </div>
87
95
  {{{ end }}}
96
+ {{{ end }}}
88
97
 
89
98
  <!-- Action Bar -->
90
99
  <div class="topic-actions">
91
100
  <a href="{config.relative_path}/topic/{./slug}" class="action-btn">
92
101
  <i class="fa-regular fa-comment"></i>
93
- <span>{humanReadableNumber(./postcount, 0)} Comments</span>
102
+ <span>{humanReadableNumber(./postcount, 0)} {{{ if (./postcount == 1) }}}Comment{{{ else }}}Comments{{{ end }}}</span>
94
103
  </a>
95
104
  <button class="action-btn share-btn" data-url="{config.relative_path}/topic/{./slug}">
96
105
  <i class="fa fa-share"></i>
package/theme.js CHANGED
@@ -9,11 +9,9 @@ const meta = require.main.require('./src/meta');
9
9
  const user = require.main.require('./src/user');
10
10
  const _ = require.main.require('lodash');
11
11
 
12
- const controllers = require('./lib/controllers');
13
-
14
12
  const theme = {};
15
13
 
16
- // JAVIS defaults - openSidebars is 'on' by default
14
+ // Harmony's defaults - we override openSidebars to 'on'
17
15
  const defaults = {
18
16
  enableQuickReply: 'on',
19
17
  enableBreadcrumbs: 'on',
@@ -35,24 +33,24 @@ theme.init = async function (params) {
35
33
  const { router } = params;
36
34
  const routeHelpers = require.main.require('./src/routes/helpers');
37
35
 
38
- // Admin panel route
39
- routeHelpers.setupAdminPageRoute(router, '/admin/plugins/javis', [], controllers.renderAdminPage);
36
+ // Admin panel route - render using Harmony's admin template structure
37
+ routeHelpers.setupAdminPageRoute(router, '/admin/plugins/javis', [], (req, res) => {
38
+ res.render('admin/plugins/javis', {
39
+ title: 'JAVIS Theme',
40
+ });
41
+ });
40
42
  };
41
43
 
42
44
  /**
43
- * Load theme config
45
+ * Load theme config - replaces Harmony's loadThemeConfig
44
46
  * Uses JAVIS defaults (with openSidebars: 'on')
45
47
  */
46
48
  async function loadThemeConfig(uid) {
47
- const [harmonyConfig, javisConfig, userConfig] = await Promise.all([
48
- meta.settings.get('harmony'), // Read from Harmony's settings (parent theme)
49
- meta.settings.get('javis'), // Read from JAVIS-specific settings
49
+ const [themeConfig, userConfig] = await Promise.all([
50
+ meta.settings.get('harmony'),
50
51
  user.getSettings(uid),
51
52
  ]);
52
53
 
53
- // Merge: Harmony settings first, then JAVIS overrides
54
- const themeConfig = { ...harmonyConfig, ...javisConfig };
55
-
56
54
  // 3-tier cascade: JAVIS defaults -> theme settings -> user settings
57
55
  const config = { ...defaults, ...themeConfig, ...(_.pick(userConfig, Object.keys(defaults))) };
58
56
 
@@ -71,9 +69,6 @@ async function loadThemeConfig(uid) {
71
69
  return config;
72
70
  }
73
71
 
74
- // Export loadThemeConfig for potential use by other modules
75
- theme.loadThemeConfig = loadThemeConfig;
76
-
77
72
  /**
78
73
  * Hook: filter:config.get
79
74
  * Sets config.theme with JAVIS-specific defaults
@@ -84,20 +79,6 @@ theme.getThemeConfig = async function (config) {
84
79
  return config;
85
80
  };
86
81
 
87
- /**
88
- * Hook: filter:settings.get
89
- * Provide default values for admin settings
90
- */
91
- theme.getAdminSettings = async function (hookData) {
92
- if (hookData.plugin === 'javis') {
93
- hookData.values = {
94
- ...defaults,
95
- ...hookData.values,
96
- };
97
- }
98
- return hookData;
99
- };
100
-
101
82
  theme.defineWidgetAreas = async (areas) => {
102
83
  // Define widget areas like Harmony does
103
84
  const locations = ['header', 'sidebar', 'footer'];