@okjavis/nodebb-theme-javis 2.3.0 → 2.5.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/scss/_feed.scss CHANGED
@@ -4,22 +4,206 @@
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
+ }
16
27
 
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
- // }
28
+ // Hide the original "New Topic" button via CSS to prevent flicker
29
+ // The composer prompt card will replace it via JS
30
+ #new_topic {
31
+ display: none !important;
32
+ }
33
+
34
+ // Hide the controls row (category dropdown, options)
35
+ // The composer prompt card replaces this functionality
36
+ > .row > div > .d-flex.justify-content-between.py-2.mb-2 {
37
+ display: none !important;
38
+ }
39
+ }
40
+
41
+ // ===========================================================
42
+ // LINKEDIN-STYLE COMPOSER PROMPT CARD
43
+ // ===========================================================
44
+ .composer-prompt-card {
45
+ background: $jv-surface;
46
+ border: 1px solid $jv-border-subtle;
47
+ border-radius: $jv-radius-lg;
48
+ padding: $jv-space-4 $jv-space-6; // 16px top/bottom, 24px left/right
49
+ margin-top: $jv-space-4; // Align with trending tags padding-top
50
+ margin-bottom: $jv-space-4;
51
+ box-shadow: $jv-shadow-sm;
52
+ transition: box-shadow $jv-transition-fast, border-color $jv-transition-fast;
53
+
54
+ &:hover {
55
+ box-shadow: $jv-shadow-md;
56
+ border-color: $jv-border-strong;
57
+ }
58
+
59
+ // Guest variant
60
+ &.composer-prompt-guest {
61
+ .composer-prompt-input {
62
+ cursor: pointer;
63
+ }
64
+ }
65
+ }
66
+
67
+ .composer-prompt-inner {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: $jv-space-3;
71
+ }
72
+
73
+ .composer-prompt-avatar {
74
+ flex-shrink: 0;
75
+
76
+ .avatar {
77
+ width: 48px !important;
78
+ height: 48px !important;
79
+ border-radius: 50% !important;
80
+ object-fit: cover;
81
+ border: 2px solid rgba(0, 0, 0, 0.05);
82
+ }
83
+
84
+ .avatar-placeholder {
85
+ width: 48px;
86
+ height: 48px;
87
+ border-radius: 50%;
88
+ background: linear-gradient(135deg, $jv-primary-soft 0%, rgba(0, 81, 255, 0.2) 100%);
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ color: $jv-primary;
93
+ font-size: 18px;
94
+ }
95
+ }
96
+
97
+ .composer-prompt-input {
98
+ flex: 1;
99
+ display: flex;
100
+ align-items: center;
101
+ padding: $jv-space-3 $jv-space-4;
102
+ background: $jv-bg;
103
+ border: 1px solid $jv-border-subtle;
104
+ border-radius: $jv-radius-pill;
105
+ cursor: pointer;
106
+ transition: all $jv-transition-fast;
107
+ text-decoration: none;
108
+ min-height: 48px;
109
+
110
+ &:hover {
111
+ background: rgba(0, 0, 0, 0.04);
112
+ border-color: $jv-border-strong;
113
+ }
114
+
115
+ &:focus {
116
+ outline: none;
117
+ border-color: $jv-primary;
118
+ box-shadow: $jv-focus-ring;
119
+ }
120
+
121
+ // Reset button styles
122
+ &.composer-prompt-input {
123
+ text-align: left;
124
+ font-family: inherit;
125
+ font-size: inherit;
126
+ }
127
+ }
128
+
129
+ .composer-prompt-placeholder {
130
+ color: $jv-text-muted;
131
+ font-size: $jv-font-size-base;
132
+ font-weight: 500;
133
+ }
134
+
135
+ .composer-prompt-actions {
136
+ display: flex;
137
+ gap: $jv-space-1;
138
+ flex-shrink: 0;
139
+ }
140
+
141
+ .composer-prompt-action {
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ width: 40px;
146
+ height: 40px;
147
+ border: none;
148
+ background: transparent;
149
+ border-radius: 50%;
150
+ color: $jv-text-muted;
151
+ cursor: pointer;
152
+ transition: all $jv-transition-fast;
153
+
154
+ &:hover {
155
+ background: $jv-hover-bg;
156
+ color: $jv-primary;
157
+ }
158
+
159
+ &:focus {
160
+ outline: none;
161
+ box-shadow: $jv-focus-ring;
162
+ }
163
+
164
+ i {
165
+ font-size: 18px;
166
+ }
167
+ }
168
+
169
+ // Feed options row (settings dropdown only)
170
+ .feed-options-row {
171
+ margin-bottom: $jv-space-2 !important;
172
+ padding: $jv-space-2 0 !important;
173
+ }
174
+
175
+ // Mobile responsive
176
+ @media (max-width: 576px) {
177
+ .composer-prompt-card {
178
+ padding: $jv-space-3;
179
+ border-radius: $jv-radius-md;
180
+ }
181
+
182
+ .composer-prompt-avatar {
183
+ .avatar,
184
+ .avatar-placeholder {
185
+ width: 40px !important;
186
+ height: 40px !important;
187
+ }
188
+
189
+ .avatar-placeholder {
190
+ font-size: 16px;
191
+ }
192
+ }
193
+
194
+ .composer-prompt-input {
195
+ padding: $jv-space-2 $jv-space-3;
196
+ min-height: 40px;
197
+ }
198
+
199
+ .composer-prompt-placeholder {
200
+ font-size: $jv-font-size-sm;
201
+ }
202
+
203
+ .composer-prompt-actions {
204
+ display: none; // Hide action buttons on mobile for cleaner UI
205
+ }
206
+ }
23
207
 
24
208
  // ===========================================================
25
209
  // FEED CONTAINER
@@ -131,22 +315,18 @@
131
315
  .overflow-hidden {
132
316
  border-radius: $jv-radius-md $jv-radius-md 0 0 !important; // Match card radius
133
317
  max-height: 280px !important; // Reduced from 350px - more balanced ratio
318
+ background: rgba(0, 0, 0, 0.02); // Subtle background for letterboxing
134
319
 
135
320
  img {
136
321
  width: 100%;
137
322
  height: 280px; // Fixed height for consistency
138
- object-fit: cover;
323
+ object-fit: contain; // Prevent stretching, maintain aspect ratio
139
324
  }
140
325
  }
141
326
 
142
- // Additional thumbnails
327
+ // Hide additional thumbnails - only show main image in feed
143
328
  .position-absolute {
144
- padding: $jv-space-4 !important;
145
-
146
- img {
147
- border: 2px solid $jv-surface;
148
- box-shadow: $jv-shadow-soft;
149
- }
329
+ display: none !important;
150
330
  }
151
331
  }
152
332
  }
@@ -191,7 +371,7 @@
191
371
  font-weight: 700 !important; // Bolder for better hierarchy
192
372
  line-height: 1.4 !important; // Better readability
193
373
  color: $jv-text-main !important;
194
- margin-bottom: $jv-space-3; // 6px - more breathing room
374
+ margin-bottom: 0;
195
375
  letter-spacing: -0.02em; // Tighter for modern feel
196
376
 
197
377
  &:hover {
@@ -314,6 +494,95 @@
314
494
  border-color: $jv-primary !important;
315
495
  }
316
496
  }
497
+
498
+ // ===========================================================
499
+ // FEED IMAGE CAROUSEL (matching topic page design)
500
+ // ===========================================================
501
+ .post-image-carousel {
502
+ border-radius: $jv-radius-md;
503
+ overflow: hidden;
504
+ margin: $jv-space-4 0;
505
+ background: rgba(0, 0, 0, 0.03);
506
+
507
+ .carousel-inner {
508
+ border-radius: $jv-radius-md;
509
+ }
510
+
511
+ .carousel-item {
512
+ // Fixed height container to prevent jumping
513
+ height: 400px;
514
+
515
+ img {
516
+ width: 100%;
517
+ height: 100%;
518
+ object-fit: contain;
519
+ background: rgba(0, 0, 0, 0.02);
520
+ }
521
+ }
522
+
523
+ // Navigation arrows - matching topic page
524
+ .carousel-control-prev,
525
+ .carousel-control-next {
526
+ width: 48px;
527
+ height: 48px;
528
+ top: 50%;
529
+ transform: translateY(-50%);
530
+ background: rgba(0, 0, 0, 0.5);
531
+ border-radius: 50%;
532
+ opacity: 0;
533
+ transition: opacity $jv-transition-fast;
534
+
535
+ &:hover {
536
+ background: rgba(0, 0, 0, 0.7);
537
+ }
538
+
539
+ .carousel-control-prev-icon,
540
+ .carousel-control-next-icon {
541
+ width: 20px;
542
+ height: 20px;
543
+ }
544
+ }
545
+
546
+ .carousel-control-prev {
547
+ left: $jv-space-3;
548
+ }
549
+
550
+ .carousel-control-next {
551
+ right: $jv-space-3;
552
+ }
553
+
554
+ &:hover {
555
+ .carousel-control-prev,
556
+ .carousel-control-next {
557
+ opacity: 1;
558
+ }
559
+ }
560
+
561
+ // Indicator dots - matching topic page
562
+ .carousel-indicators {
563
+ margin-bottom: $jv-space-3;
564
+ gap: $jv-space-2;
565
+
566
+ button {
567
+ width: 8px;
568
+ height: 8px;
569
+ border-radius: 50%;
570
+ background: rgba(255, 255, 255, 0.5);
571
+ border: none;
572
+ opacity: 1;
573
+ transition: background-color $jv-transition-fast, transform $jv-transition-fast;
574
+
575
+ &.active {
576
+ background: #fff;
577
+ transform: scale(1.2);
578
+ }
579
+
580
+ &:hover {
581
+ background: rgba(255, 255, 255, 0.8);
582
+ }
583
+ }
584
+ }
585
+ }
317
586
  }
318
587
 
319
588
  // ===========================================================
@@ -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,80 @@
370
374
  });
371
375
  }
372
376
 
377
+ /**
378
+ * Initialize LinkedIn-style composer prompt card on feed page
379
+ * Injects the card HTML and sets up click handlers
380
+ * The original "New Topic" button is hidden via CSS to prevent flicker
381
+ */
382
+ function initFeedComposerPromptHandler() {
383
+ // Only run on feed page
384
+ if (!$('.feed').length) {
385
+ return;
386
+ }
387
+
388
+ // Don't re-inject if already present
389
+ if ($('.composer-prompt-card').length) {
390
+ return;
391
+ }
392
+
393
+ // Find the existing controls row (contains New Topic button)
394
+ var $controlsRow = $('.feed .d-flex.justify-content-between.py-2.mb-2').first();
395
+ if (!$controlsRow.length) {
396
+ return;
397
+ }
398
+
399
+ // Get user info for avatar - clone from header avatar which is already correctly rendered
400
+ var isLoggedIn = false;
401
+ var avatarHtml = '';
402
+
403
+ if (typeof app !== 'undefined' && app.user && app.user.uid) {
404
+ isLoggedIn = true;
405
+ // Clone the avatar from the header - it's already correctly rendered by NodeBB
406
+ var $headerAvatar = $('[component="header/avatar"] img, [component="header/avatar"] [component="avatar/picture"]').first();
407
+ if ($headerAvatar.length) {
408
+ avatarHtml = '<img src="' + $headerAvatar.attr('src') + '" alt="Avatar" class="avatar avatar-rounded" />';
409
+ } else {
410
+ // Fallback to icon-based avatar from header
411
+ var $headerIcon = $('[component="header/avatar"] [component="avatar/icon"]').first();
412
+ if ($headerIcon.length) {
413
+ avatarHtml = $headerIcon.clone().addClass('avatar avatar-rounded').get(0).outerHTML;
414
+ } else {
415
+ // Last resort: use placeholder
416
+ avatarHtml = '<div class="avatar avatar-rounded avatar-placeholder"><i class="fa fa-user"></i></div>';
417
+ }
418
+ }
419
+ } else {
420
+ avatarHtml = '<div class="avatar avatar-rounded avatar-placeholder"><i class="fa fa-user"></i></div>';
421
+ }
422
+
423
+ var cardHtml = '<div class="composer-prompt-card' + (isLoggedIn ? '' : ' composer-prompt-guest') + '">' +
424
+ '<div class="composer-prompt-inner">' +
425
+ '<div class="composer-prompt-avatar">' + avatarHtml + '</div>' +
426
+ '<button class="composer-prompt-input" type="button" id="composer-prompt-btn">' +
427
+ '<span class="composer-prompt-placeholder">' + (isLoggedIn ? 'Start a discussion...' : 'Sign in to start a discussion...') + '</span>' +
428
+ '</button>' +
429
+ '</div>' +
430
+ '</div>';
431
+
432
+ // Insert the card before the controls row
433
+ $controlsRow.before(cardHtml);
434
+
435
+ // Bind click handler to open composer
436
+ $('.composer-prompt-card').on('click', '.composer-prompt-input, .composer-prompt-action', function(e) {
437
+ e.preventDefault();
438
+
439
+ if (!isLoggedIn) {
440
+ // Redirect to login
441
+ window.location.href = config.relative_path + '/login';
442
+ return;
443
+ }
444
+
445
+ // Trigger the original New Topic button click
446
+ var $newTopicBtn = $('#new_topic');
447
+ if ($newTopicBtn.length) {
448
+ $newTopicBtn.trigger('click');
449
+ }
450
+ });
451
+ }
452
+
373
453
  })();
@@ -0,0 +1,76 @@
1
+ <div class="acp-page-container">
2
+ <!-- IMPORT admin/partials/settings/header.tpl -->
3
+
4
+ <div class="row m-0">
5
+ <div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
6
+ <form role="form" class="javis-settings">
7
+ <div class="mb-4">
8
+ <h5 class="fw-bold">JAVIS Theme Settings</h5>
9
+ <p class="text-muted">Configure the default settings for the JAVIS Community theme.</p>
10
+ </div>
11
+
12
+ <div class="form-check form-switch mb-3">
13
+ <input type="checkbox" class="form-check-input" id="enableQuickReply" name="enableQuickReply" />
14
+ <label for="enableQuickReply" class="form-check-label">Enable Quick Reply</label>
15
+ <p class="form-text">Allow users to quickly reply to topics without opening the full composer.</p>
16
+ </div>
17
+
18
+ <div class="form-check form-switch mb-3">
19
+ <input type="checkbox" class="form-check-input" id="enableBreadcrumbs" name="enableBreadcrumbs" />
20
+ <label for="enableBreadcrumbs" class="form-check-label">Enable Breadcrumbs</label>
21
+ <p class="form-text">Show navigation breadcrumbs at the top of pages.</p>
22
+ </div>
23
+
24
+ <div class="form-check form-switch mb-3">
25
+ <input type="checkbox" class="form-check-input" id="centerHeaderElements" name="centerHeaderElements" />
26
+ <label for="centerHeaderElements" class="form-check-label">Center Header Elements</label>
27
+ <p class="form-text">Center-align the header navigation elements.</p>
28
+ </div>
29
+
30
+ <div class="form-check form-switch mb-3">
31
+ <input type="checkbox" class="form-check-input" id="mobileTopicTeasers" name="mobileTopicTeasers" />
32
+ <label for="mobileTopicTeasers" class="form-check-label">Mobile Topic Teasers</label>
33
+ <p class="form-text">Show topic teasers on mobile devices.</p>
34
+ </div>
35
+
36
+ <div class="form-check form-switch mb-3">
37
+ <input type="checkbox" class="form-check-input" id="stickyToolbar" name="stickyToolbar" />
38
+ <label for="stickyToolbar" class="form-check-label">Sticky Toolbar</label>
39
+ <p class="form-text">Keep the topic toolbar visible when scrolling.</p>
40
+ </div>
41
+
42
+ <div class="form-check form-switch mb-3">
43
+ <input type="checkbox" class="form-check-input" id="topicSidebarTools" name="topicSidebarTools" />
44
+ <label for="topicSidebarTools" class="form-check-label">Topic Sidebar Tools</label>
45
+ <p class="form-text">Show topic tools in the sidebar instead of inline.</p>
46
+ </div>
47
+
48
+ <div class="form-check form-switch mb-3">
49
+ <input type="checkbox" class="form-check-input" id="autohideBottombar" name="autohideBottombar" />
50
+ <label for="autohideBottombar" class="form-check-label">Auto-hide Bottom Bar</label>
51
+ <p class="form-text">Automatically hide the bottom navigation bar when scrolling down.</p>
52
+ </div>
53
+
54
+ <div class="form-check form-switch mb-3">
55
+ <input type="checkbox" class="form-check-input" id="topMobilebar" name="topMobilebar" />
56
+ <label for="topMobilebar" class="form-check-label">Top Mobile Bar</label>
57
+ <p class="form-text">Show mobile navigation at the top instead of bottom.</p>
58
+ </div>
59
+
60
+ <div class="form-check form-switch mb-3">
61
+ <input type="checkbox" class="form-check-input" id="openSidebars" name="openSidebars" />
62
+ <label for="openSidebars" class="form-check-label">Open Sidebars by Default</label>
63
+ <p class="form-text">Keep sidebars expanded by default (JAVIS default: enabled).</p>
64
+ </div>
65
+
66
+ <div class="form-check form-switch mb-3">
67
+ <input type="checkbox" class="form-check-input" id="chatModals" name="chatModals" />
68
+ <label for="chatModals" class="form-check-label">Chat Modals</label>
69
+ <p class="form-text">Open chats in modal windows instead of navigating to the chat page.</p>
70
+ </div>
71
+ </form>
72
+ </div>
73
+
74
+ <!-- IMPORT admin/partials/settings/toc.tpl -->
75
+ </div>
76
+ </div>
package/theme.js CHANGED
@@ -9,9 +9,11 @@ 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
+
12
14
  const theme = {};
13
15
 
14
- // Harmony's defaults - we override openSidebars to 'on'
16
+ // JAVIS defaults - openSidebars is 'on' by default
15
17
  const defaults = {
16
18
  enableQuickReply: 'on',
17
19
  enableBreadcrumbs: 'on',
@@ -26,12 +28,24 @@ const defaults = {
26
28
  };
27
29
 
28
30
  /**
29
- * Load theme config - replaces Harmony's loadThemeConfig
31
+ * Hook: static:app.load
32
+ * Initialize routes for admin panel
33
+ */
34
+ theme.init = async function (params) {
35
+ const { router } = params;
36
+ const routeHelpers = require.main.require('./src/routes/helpers');
37
+
38
+ // Admin panel route
39
+ routeHelpers.setupAdminPageRoute(router, '/admin/plugins/javis', [], controllers.renderAdminPage);
40
+ };
41
+
42
+ /**
43
+ * Load theme config
30
44
  * Uses JAVIS defaults (with openSidebars: 'on')
31
45
  */
32
46
  async function loadThemeConfig(uid) {
33
47
  const [themeConfig, userConfig] = await Promise.all([
34
- meta.settings.get('harmony'),
48
+ meta.settings.get('javis'),
35
49
  user.getSettings(uid),
36
50
  ]);
37
51
 
@@ -53,6 +67,9 @@ async function loadThemeConfig(uid) {
53
67
  return config;
54
68
  }
55
69
 
70
+ // Export loadThemeConfig for potential use by other modules
71
+ theme.loadThemeConfig = loadThemeConfig;
72
+
56
73
  /**
57
74
  * Hook: filter:config.get
58
75
  * Sets config.theme with JAVIS-specific defaults
@@ -63,6 +80,20 @@ theme.getThemeConfig = async function (config) {
63
80
  return config;
64
81
  };
65
82
 
83
+ /**
84
+ * Hook: filter:settings.get
85
+ * Provide default values for admin settings
86
+ */
87
+ theme.getAdminSettings = async function (hookData) {
88
+ if (hookData.plugin === 'javis') {
89
+ hookData.values = {
90
+ ...defaults,
91
+ ...hookData.values,
92
+ };
93
+ }
94
+ return hookData;
95
+ };
96
+
66
97
  theme.defineWidgetAreas = async (areas) => {
67
98
  // Define widget areas like Harmony does
68
99
  const locations = ['header', 'sidebar', 'footer'];
@@ -125,7 +156,7 @@ theme.defineWidgetAreas = async (areas) => {
125
156
 
126
157
  theme.addAdminNavigation = (header) => {
127
158
  header.plugins.push({
128
- route: '/plugins/javis-community-theme',
159
+ route: '/plugins/javis',
129
160
  icon: 'fa-paint-brush',
130
161
  name: 'JAVIS Theme',
131
162
  });
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";