nodebb-theme-flawless 0.1.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/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # nodebb-theme-flawless
2
+
3
+ Dark Flawless RP child theme for NodeBB v4. It inherits templates and client behavior from `nodebb-theme-harmony` and ports the styling from the existing Flawless UCP dashboard into NodeBB forum pages.
4
+
5
+ ## Compatibility
6
+
7
+ - Target NodeBB: v4.11.3 and later v4 releases
8
+ - Base theme: `nodebb-theme-harmony`
9
+ - Package compatibility: `^4.0.0`
10
+
11
+ ## Install from a local checkout
12
+
13
+ From the root of your NodeBB install:
14
+
15
+ ```bash
16
+ npm install /absolute/path/to/nodebb-theme-flawless
17
+ ./nodebb build
18
+ ./nodebb activate nodebb-theme-flawless
19
+ ./nodebb restart
20
+ ```
21
+
22
+ You can also activate it from ACP > Appearance > Themes after installing and rebuilding.
23
+
24
+ ## Notes
25
+
26
+ - Update `theme.json` if you want the `url` field to point at your real GitHub repository.
27
+ - This is a child theme. Missing templates automatically fall back to Harmony.
28
+ - The theme keeps Harmony's settings page and widget areas so existing NodeBB v4 forum behavior remains available.
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const Controllers = module.exports;
4
+
5
+ const accountHelpers = require.main.require('./src/controllers/accounts/helpers');
6
+ const helpers = require.main.require('./src/controllers/helpers');
7
+
8
+ Controllers.renderAdminPage = (req, res) => {
9
+ res.render('admin/plugins/theme-flawless', {
10
+ title: 'Flawless RP Theme',
11
+ });
12
+ };
13
+
14
+ Controllers.renderThemeSettings = async (req, res, next) => {
15
+ const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query);
16
+ if (!userData) {
17
+ return next();
18
+ }
19
+
20
+ const lib = require('./theme');
21
+ userData.theme = await lib.loadThemeConfig(userData.uid);
22
+
23
+ userData.title = '[[themes/harmony:settings.title]]';
24
+ userData.breadcrumbs = helpers.buildBreadcrumbs([
25
+ { text: userData.username, url: `/user/${userData.userslug}` },
26
+ { text: '[[themes/harmony:settings.title]]' },
27
+ ]);
28
+
29
+ res.render('account/theme', userData);
30
+ };
package/lib/theme.js ADDED
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ const nconf = require.main.require('nconf');
4
+ const meta = require.main.require('./src/meta');
5
+ const _ = require.main.require('lodash');
6
+ const user = require.main.require('./src/user');
7
+
8
+ const controllers = require('./controllers');
9
+
10
+ const library = module.exports;
11
+
12
+ const defaults = {
13
+ enableQuickReply: 'on',
14
+ enableBreadcrumbs: 'on',
15
+ centerHeaderElements: 'off',
16
+ mobileTopicTeasers: 'off',
17
+ stickyToolbar: 'on',
18
+ topicSidebarTools: 'on',
19
+ topMobilebar: 'off',
20
+ autohideBottombar: 'on',
21
+ openSidebars: 'off',
22
+ chatModals: 'off',
23
+ };
24
+
25
+ library.init = async function (params) {
26
+ const { router, middleware } = params;
27
+ const routeHelpers = require.main.require('./src/routes/helpers');
28
+
29
+ routeHelpers.setupAdminPageRoute(router, '/admin/plugins/theme-flawless', [], controllers.renderAdminPage);
30
+
31
+ routeHelpers.setupPageRoute(router, '/user/:userslug/theme', [
32
+ middleware.exposeUid,
33
+ middleware.ensureLoggedIn,
34
+ middleware.canViewUsers,
35
+ middleware.checkAccountPermissions,
36
+ ], controllers.renderThemeSettings);
37
+
38
+ if (nconf.get('isPrimary') && process.env.NODE_ENV === 'production') {
39
+ setTimeout(buildSkins, 0);
40
+ }
41
+ };
42
+
43
+ async function buildSkins() {
44
+ try {
45
+ const plugins = require.main.require('./src/plugins');
46
+ await plugins.prepareForBuild(['client side styles']);
47
+ for (const skin of meta.css.supportedSkins) {
48
+ // eslint-disable-next-line no-await-in-loop
49
+ await meta.css.buildBundle(`client-${skin}`, true);
50
+ }
51
+ require.main.require('./src/meta/minifier').killAll();
52
+ } catch (err) {
53
+ console.error(err.stack);
54
+ }
55
+ }
56
+
57
+ library.addAdminNavigation = async function (header) {
58
+ header.plugins.push({
59
+ route: '/plugins/theme-flawless',
60
+ icon: 'fa-paint-brush',
61
+ name: 'Flawless RP Theme',
62
+ });
63
+ return header;
64
+ };
65
+
66
+ library.addProfileItem = async (data) => {
67
+ data.links.push({
68
+ id: 'theme',
69
+ route: 'theme',
70
+ icon: 'fa-paint-brush',
71
+ name: '[[themes/harmony:settings.title]]',
72
+ visibility: {
73
+ self: true,
74
+ other: false,
75
+ moderator: false,
76
+ globalMod: false,
77
+ admin: false,
78
+ },
79
+ });
80
+
81
+ return data;
82
+ };
83
+
84
+ library.defineWidgetAreas = async function (areas) {
85
+ const locations = ['header', 'sidebar', 'footer'];
86
+ const templates = [
87
+ 'categories.tpl', 'category.tpl', 'topic.tpl', 'users.tpl',
88
+ 'unread.tpl', 'recent.tpl', 'popular.tpl', 'top.tpl', 'tags.tpl', 'tag.tpl',
89
+ 'login.tpl', 'register.tpl', 'world.tpl',
90
+ ];
91
+
92
+ function capitalizeFirst(str) {
93
+ return str.charAt(0).toUpperCase() + str.slice(1);
94
+ }
95
+
96
+ templates.forEach((template) => {
97
+ locations.forEach((location) => {
98
+ areas.push({
99
+ name: `${capitalizeFirst(template.split('.')[0])} ${capitalizeFirst(location)}`,
100
+ template: template,
101
+ location: location,
102
+ });
103
+ });
104
+ });
105
+
106
+ areas = areas.concat([
107
+ {
108
+ name: 'Main post header',
109
+ template: 'topic.tpl',
110
+ location: 'mainpost-header',
111
+ },
112
+ {
113
+ name: 'Main post footer',
114
+ template: 'topic.tpl',
115
+ location: 'mainpost-footer',
116
+ },
117
+ {
118
+ name: 'Sidebar Footer',
119
+ template: 'global',
120
+ location: 'sidebar-footer',
121
+ },
122
+ {
123
+ name: 'Brand Header',
124
+ template: 'global',
125
+ location: 'brand-header',
126
+ },
127
+ {
128
+ name: 'About me (before)',
129
+ template: 'account/profile.tpl',
130
+ location: 'profile-aboutme-before',
131
+ },
132
+ {
133
+ name: 'About me (after)',
134
+ template: 'account/profile.tpl',
135
+ location: 'profile-aboutme-after',
136
+ },
137
+ ]);
138
+
139
+ return areas;
140
+ };
141
+
142
+ library.loadThemeConfig = async function (uid) {
143
+ const [themeConfig, userConfig] = await Promise.all([
144
+ meta.settings.get('harmony'),
145
+ user.getSettings(uid),
146
+ ]);
147
+
148
+ const config = { ...defaults, ...themeConfig, ...(_.pick(userConfig, Object.keys(defaults))) };
149
+ config.enableQuickReply = config.enableQuickReply === 'on';
150
+ config.enableBreadcrumbs = config.enableBreadcrumbs === 'on';
151
+ config.centerHeaderElements = config.centerHeaderElements === 'on';
152
+ config.mobileTopicTeasers = config.mobileTopicTeasers === 'on';
153
+ config.stickyToolbar = config.stickyToolbar === 'on';
154
+ config.topicSidebarTools = config.topicSidebarTools === 'on';
155
+ config.autohideBottombar = config.autohideBottombar === 'on';
156
+ config.topMobilebar = config.topMobilebar === 'on';
157
+ config.openSidebars = config.openSidebars === 'on';
158
+ config.chatModals = config.chatModals === 'on';
159
+ return config;
160
+ };
161
+
162
+ library.getThemeConfig = async function (config) {
163
+ config.theme = await library.loadThemeConfig(config.uid);
164
+ config.openDraftsOnPageLoad = false;
165
+ return config;
166
+ };
167
+
168
+ library.getAdminSettings = async function (hookData) {
169
+ if (hookData.plugin === 'harmony') {
170
+ hookData.values = {
171
+ ...defaults,
172
+ ...hookData.values,
173
+ };
174
+ }
175
+ return hookData;
176
+ };
177
+
178
+ library.saveUserSettings = async function (hookData) {
179
+ Object.keys(defaults).forEach((key) => {
180
+ if (hookData.data.hasOwnProperty(key)) {
181
+ hookData.settings[key] = hookData.data[key] || undefined;
182
+ }
183
+ });
184
+ return hookData;
185
+ };
186
+
187
+ library.filterMiddlewareRenderHeader = async function (hookData) {
188
+ hookData.templateData.bootswatchSkinOptions = await meta.css.getSkinSwitcherOptions(hookData.req.uid);
189
+ return hookData;
190
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "nodebb-theme-flawless",
3
+ "version": "0.1.0",
4
+ "description": "A dark Flawless Roleplay child theme for NodeBB v4 and Harmony.",
5
+ "main": "lib/theme.js",
6
+ "keywords": [
7
+ "nodebb",
8
+ "nodebb-theme",
9
+ "theme",
10
+ "harmony",
11
+ "roleplay"
12
+ ],
13
+ "license": "MIT",
14
+ "dependencies": {
15
+ "@fontsource/barlow-condensed": "5.2.8",
16
+ "@fontsource/dm-sans": "5.2.8",
17
+ "@fontsource/inter": "5.2.8",
18
+ "@fontsource/jetbrains-mono": "5.2.8",
19
+ "@fontsource/poppins": "5.2.7"
20
+ },
21
+ "nbbpm": {
22
+ "compatibility": "^4.0.0"
23
+ }
24
+ }
package/plugin.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "id": "nodebb-theme-flawless",
3
+ "hooks": [
4
+ { "hook": "static:app.load", "method": "init" },
5
+ { "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
6
+ { "hook": "filter:widgets.getAreas", "method": "defineWidgetAreas" },
7
+ { "hook": "filter:config.get", "method": "getThemeConfig" },
8
+ { "hook": "filter:settings.get", "method": "getAdminSettings" },
9
+ { "hook": "filter:user.saveSettings", "method": "saveUserSettings" },
10
+ { "hook": "filter:user.profileMenu", "method": "addProfileItem" },
11
+ { "hook": "filter:middleware.renderHeader", "method": "filterMiddlewareRenderHeader" }
12
+ ],
13
+ "scripts": [
14
+ "public/client.js",
15
+ "../nodebb-theme-harmony/public/harmony.js"
16
+ ],
17
+ "templates": "templates",
18
+ "modules": {
19
+ "../admin/plugins/theme-flawless.js": "public/admin.js",
20
+ "../client/account/theme.js": "../nodebb-theme-harmony/public/settings.js"
21
+ },
22
+ "staticDirs": {
23
+ "barlow-condensed": "node_modules/@fontsource/barlow-condensed/files",
24
+ "dm-sans": "node_modules/@fontsource/dm-sans/files",
25
+ "inter": "node_modules/@fontsource/inter/files",
26
+ "jetbrains-mono": "node_modules/@fontsource/jetbrains-mono/files",
27
+ "poppins": "node_modules/@fontsource/poppins/files"
28
+ }
29
+ }
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ define('admin/plugins/theme-flawless', ['settings'], function (Settings) {
4
+ const ACP = {};
5
+
6
+ ACP.init = function () {
7
+ Settings.load('harmony', $('.harmony-settings'));
8
+
9
+ $('#save').on('click', function () {
10
+ Settings.save('harmony', $('.harmony-settings'));
11
+ });
12
+ };
13
+
14
+ return ACP;
15
+ });
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ function markFlawlessTheme() {
4
+ document.body.classList.add('flawless-theme');
5
+ }
6
+
7
+ $(document).ready(markFlawlessTheme);
8
+ $(window).on('action:ajaxify.end', markFlawlessTheme);
@@ -0,0 +1,779 @@
1
+ @font-face {
2
+ font-family: "DM Sans";
3
+ font-style: normal;
4
+ font-weight: 400;
5
+ font-display: swap;
6
+ src: url("#{$font-path}/dm-sans/dm-sans-latin-400-normal.woff2") format("woff2");
7
+ }
8
+
9
+ @font-face {
10
+ font-family: "DM Sans";
11
+ font-style: normal;
12
+ font-weight: 500;
13
+ font-display: swap;
14
+ src: url("#{$font-path}/dm-sans/dm-sans-latin-500-normal.woff2") format("woff2");
15
+ }
16
+
17
+ @font-face {
18
+ font-family: "DM Sans";
19
+ font-style: normal;
20
+ font-weight: 600;
21
+ font-display: swap;
22
+ src: url("#{$font-path}/dm-sans/dm-sans-latin-600-normal.woff2") format("woff2");
23
+ }
24
+
25
+ @font-face {
26
+ font-family: "DM Sans";
27
+ font-style: normal;
28
+ font-weight: 700;
29
+ font-display: swap;
30
+ src: url("#{$font-path}/dm-sans/dm-sans-latin-700-normal.woff2") format("woff2");
31
+ }
32
+
33
+ @font-face {
34
+ font-family: "Barlow Condensed";
35
+ font-style: normal;
36
+ font-weight: 400;
37
+ font-display: swap;
38
+ src: url("#{$font-path}/barlow-condensed/barlow-condensed-latin-400-normal.woff2") format("woff2");
39
+ }
40
+
41
+ @font-face {
42
+ font-family: "Barlow Condensed";
43
+ font-style: normal;
44
+ font-weight: 500;
45
+ font-display: swap;
46
+ src: url("#{$font-path}/barlow-condensed/barlow-condensed-latin-500-normal.woff2") format("woff2");
47
+ }
48
+
49
+ @font-face {
50
+ font-family: "Barlow Condensed";
51
+ font-style: normal;
52
+ font-weight: 600;
53
+ font-display: swap;
54
+ src: url("#{$font-path}/barlow-condensed/barlow-condensed-latin-600-normal.woff2") format("woff2");
55
+ }
56
+
57
+ @font-face {
58
+ font-family: "Barlow Condensed";
59
+ font-style: normal;
60
+ font-weight: 700;
61
+ font-display: swap;
62
+ src: url("#{$font-path}/barlow-condensed/barlow-condensed-latin-700-normal.woff2") format("woff2");
63
+ }
64
+
65
+ @font-face {
66
+ font-family: "JetBrains Mono";
67
+ font-style: normal;
68
+ font-weight: 400;
69
+ font-display: swap;
70
+ src: url("#{$font-path}/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2") format("woff2");
71
+ }
72
+
73
+ @font-face {
74
+ font-family: "JetBrains Mono";
75
+ font-style: normal;
76
+ font-weight: 500;
77
+ font-display: swap;
78
+ src: url("#{$font-path}/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2") format("woff2");
79
+ }
80
+
81
+ :root,
82
+ [data-bs-theme="light"],
83
+ [data-bs-theme="dark"] {
84
+ --ucp-bg: #0d0d0f;
85
+ --ucp-sidebar: #111113;
86
+ --ucp-card: #18181b;
87
+ --ucp-card-soft: rgba(39, 39, 42, 0.5);
88
+ --ucp-border: #27272a;
89
+ --ucp-border-strong: #3f3f46;
90
+ --ucp-text: #fafafa;
91
+ --ucp-text-muted: #a1a1aa;
92
+ --ucp-text-subtle: #71717a;
93
+ --ucp-accent: #f59e0b;
94
+ --ucp-accent-hover: #fbbf24;
95
+ --ucp-success: #22c55e;
96
+ --ucp-danger: #ef4444;
97
+ --ucp-warning: #f59e0b;
98
+ --ucp-info: #3b82f6;
99
+ --ucp-cyan: #06b6d4;
100
+
101
+ --bs-body-bg: var(--ucp-bg);
102
+ --bs-body-color: var(--ucp-text);
103
+ --bs-emphasis-color: var(--ucp-text);
104
+ --bs-secondary-color: var(--ucp-text-muted);
105
+ --bs-tertiary-color: var(--ucp-text-subtle);
106
+ --bs-secondary-bg: var(--ucp-sidebar);
107
+ --bs-tertiary-bg: var(--ucp-card);
108
+ --bs-border-color: var(--ucp-border);
109
+ --bs-border-color-translucent: rgba(63, 63, 70, 0.55);
110
+ --bs-link-color: var(--ucp-accent);
111
+ --bs-link-hover-color: var(--ucp-accent-hover);
112
+ --bs-primary: var(--ucp-accent);
113
+ --bs-success: var(--ucp-success);
114
+ --bs-danger: var(--ucp-danger);
115
+ --bs-warning: var(--ucp-warning);
116
+ --bs-info: var(--ucp-info);
117
+ --bs-light: #f4f4f5;
118
+ --bs-dark: var(--ucp-bg);
119
+ --bs-heading-color: var(--ucp-text);
120
+ --bs-border-radius: 8px;
121
+ --bs-border-radius-sm: 6px;
122
+ --bs-border-radius-lg: 12px;
123
+ --btn-ghost-hover-color: rgba(245, 158, 11, 0.08);
124
+ --btn-ghost-active-color: rgba(245, 158, 11, 0.14);
125
+ }
126
+
127
+ html,
128
+ body {
129
+ background: var(--ucp-bg);
130
+ color: var(--ucp-text);
131
+ font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
132
+ -webkit-font-smoothing: antialiased;
133
+ -moz-osx-font-smoothing: grayscale;
134
+ }
135
+
136
+ body.flawless-theme {
137
+ background:
138
+ linear-gradient(180deg, rgba(245, 158, 11, 0.03), transparent 22rem),
139
+ var(--ucp-bg);
140
+ }
141
+
142
+ a {
143
+ color: var(--ucp-accent);
144
+ }
145
+
146
+ a:hover,
147
+ a:focus {
148
+ color: var(--ucp-accent-hover);
149
+ }
150
+
151
+ hr,
152
+ .border,
153
+ .border-top,
154
+ .border-end,
155
+ .border-bottom,
156
+ .border-start,
157
+ .border-gray-300 {
158
+ border-color: var(--ucp-border) !important;
159
+ }
160
+
161
+ .text-muted,
162
+ .text-secondary,
163
+ .link-secondary {
164
+ color: var(--ucp-text-muted) !important;
165
+ }
166
+
167
+ .text-body,
168
+ .text-reset {
169
+ color: var(--ucp-text) !important;
170
+ }
171
+
172
+ .bg-body,
173
+ .bg-light,
174
+ .text-bg-light {
175
+ background-color: var(--ucp-card) !important;
176
+ color: var(--ucp-text) !important;
177
+ }
178
+
179
+ .ff-secondary,
180
+ .font-display,
181
+ .tracking-tight,
182
+ h1,
183
+ h2,
184
+ h3,
185
+ h4,
186
+ h5,
187
+ h6 {
188
+ font-family: "Barlow Condensed", "DM Sans", system-ui, sans-serif;
189
+ letter-spacing: 0.02em;
190
+ }
191
+
192
+ .ff-base,
193
+ .ff-sans {
194
+ font-family: "DM Sans", system-ui, sans-serif;
195
+ }
196
+
197
+ .ff-monospace,
198
+ code,
199
+ kbd,
200
+ pre,
201
+ .cmd {
202
+ font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
203
+ }
204
+
205
+ .layout-container {
206
+ background: transparent;
207
+ }
208
+
209
+ #panel {
210
+ background: transparent;
211
+ }
212
+
213
+ #content {
214
+ color: var(--ucp-text);
215
+ }
216
+
217
+ [component="sidebar/left"] {
218
+ width: 220px;
219
+ min-width: 220px;
220
+ background: var(--ucp-sidebar) !important;
221
+ color: var(--ucp-text-muted) !important;
222
+ border-color: var(--ucp-border) !important;
223
+ }
224
+
225
+ [component="sidebar/left"]:not(.open) {
226
+ width: 64px;
227
+ min-width: 64px;
228
+ }
229
+
230
+ [component="sidebar/left"] .navigation-link,
231
+ [component="sidebar/left"] [component="sidebar/toggle"],
232
+ .bottombar-nav .nav-link {
233
+ color: var(--ucp-text-muted) !important;
234
+ border-radius: 8px;
235
+ padding: 10px 12px;
236
+ transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
237
+ }
238
+
239
+ [component="sidebar/left"] .navigation-link:hover,
240
+ [component="sidebar/left"] [component="sidebar/toggle"]:hover,
241
+ .bottombar-nav .nav-link:hover {
242
+ background: rgba(245, 158, 11, 0.05);
243
+ color: var(--ucp-text) !important;
244
+ }
245
+
246
+ [component="sidebar/left"] .nav-item.active .navigation-link,
247
+ [component="sidebar/left"] .navigation-link.active,
248
+ .navigation-dropdown .nav-item.active .navigation-link {
249
+ background: rgba(245, 158, 11, 0.1);
250
+ color: var(--ucp-accent) !important;
251
+ box-shadow: inset 3px 0 0 var(--ucp-accent);
252
+ }
253
+
254
+ [component="sidebar/left"] .badge,
255
+ .bottombar-nav .badge {
256
+ background: var(--ucp-accent) !important;
257
+ color: #000 !important;
258
+ }
259
+
260
+ .brand-container > .border-bottom {
261
+ border-color: var(--ucp-border) !important;
262
+ }
263
+
264
+ [component="brand/wrapper"] {
265
+ background: var(--ucp-sidebar);
266
+ border: 1px solid var(--ucp-border);
267
+ border-radius: 12px !important;
268
+ }
269
+
270
+ [component="siteTitle"] h1 {
271
+ color: var(--ucp-accent) !important;
272
+ font-size: 1.35rem !important;
273
+ font-weight: 700;
274
+ text-transform: uppercase;
275
+ }
276
+
277
+ [component="brand/logo"] {
278
+ max-height: 42px;
279
+ }
280
+
281
+ .card,
282
+ .modal-content,
283
+ .dropdown-menu,
284
+ .popover,
285
+ .toast,
286
+ .category-header,
287
+ .chats-list > li,
288
+ .quick-search-container,
289
+ [data-widget-area] .card {
290
+ background: var(--ucp-card);
291
+ border: 1px solid var(--ucp-border);
292
+ border-radius: 12px;
293
+ color: var(--ucp-text);
294
+ box-shadow: none;
295
+ }
296
+
297
+ .card-header,
298
+ .card-footer,
299
+ .modal-header,
300
+ .modal-footer,
301
+ .dropdown-divider {
302
+ background: transparent;
303
+ border-color: var(--ucp-border);
304
+ }
305
+
306
+ .dropdown-menu {
307
+ padding: 6px;
308
+ box-shadow: 0 18px 42px rgba(0, 0, 0, 0.45);
309
+ }
310
+
311
+ .dropdown-item,
312
+ .dropdown-menu .nav-link {
313
+ color: var(--ucp-text-muted);
314
+ border-radius: 6px;
315
+ }
316
+
317
+ .dropdown-item:hover,
318
+ .dropdown-item:focus,
319
+ .dropdown-item.active,
320
+ .dropdown-menu .nav-link:hover,
321
+ .dropdown-menu .nav-link.active {
322
+ background: rgba(245, 158, 11, 0.1);
323
+ color: var(--ucp-accent);
324
+ }
325
+
326
+ .categories-list,
327
+ .topics-list {
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: 8px;
331
+ }
332
+
333
+ [component="categories/category"],
334
+ [component="category/topic"] {
335
+ background: var(--ucp-card);
336
+ border: 1px solid var(--ucp-border) !important;
337
+ border-radius: 12px;
338
+ padding: 16px !important;
339
+ transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
340
+ }
341
+
342
+ [component="categories/category"]:hover,
343
+ [component="category/topic"]:hover {
344
+ background: #1d1d21;
345
+ border-color: var(--ucp-border-strong) !important;
346
+ }
347
+
348
+ [component="categories/category"].unread,
349
+ [component="category/topic"].unread {
350
+ border-left: 4px solid var(--ucp-accent) !important;
351
+ padding-left: 13px !important;
352
+ }
353
+
354
+ [component="categories/category"] .title a,
355
+ [component="category/topic"] [component="topic/header"] a,
356
+ .topic-title {
357
+ color: var(--ucp-text) !important;
358
+ }
359
+
360
+ [component="categories/category"] .description,
361
+ [component="category/topic"] .post-content,
362
+ .lastpost,
363
+ .teaser {
364
+ color: var(--ucp-text-muted) !important;
365
+ }
366
+
367
+ .meta.stats .card {
368
+ background: rgba(39, 39, 42, 0.5);
369
+ border: 1px solid var(--ucp-border) !important;
370
+ border-radius: 8px !important;
371
+ color: var(--ucp-text);
372
+ }
373
+
374
+ .meta.stats .card span:first-child {
375
+ color: var(--ucp-text);
376
+ font-family: "JetBrains Mono", monospace;
377
+ font-weight: 500;
378
+ }
379
+
380
+ .category-header {
381
+ padding: 20px;
382
+ }
383
+
384
+ .category-header h1,
385
+ [component="post/header"] {
386
+ color: var(--ucp-text);
387
+ font-weight: 700;
388
+ }
389
+
390
+ .badge,
391
+ .tag-list .tag,
392
+ .category-label {
393
+ border-radius: 6px !important;
394
+ font-size: 0.72rem;
395
+ font-weight: 600;
396
+ letter-spacing: 0.05em;
397
+ text-transform: uppercase;
398
+ }
399
+
400
+ .badge.border,
401
+ .tag-list .tag,
402
+ [component="topic/labels"] .badge {
403
+ background: rgba(161, 161, 170, 0.1);
404
+ border-color: rgba(113, 113, 122, 0.3) !important;
405
+ color: var(--ucp-text-muted) !important;
406
+ }
407
+
408
+ .tag-list .tag:hover,
409
+ [component="topic/labels"] .badge:hover {
410
+ background: rgba(245, 158, 11, 0.1);
411
+ border-color: rgba(245, 158, 11, 0.3) !important;
412
+ color: var(--ucp-accent) !important;
413
+ }
414
+
415
+ .bg-primary,
416
+ .text-bg-primary {
417
+ background-color: var(--ucp-accent) !important;
418
+ color: #000 !important;
419
+ }
420
+
421
+ .bg-success,
422
+ .text-bg-success {
423
+ background-color: var(--ucp-success) !important;
424
+ color: #000 !important;
425
+ }
426
+
427
+ .bg-danger,
428
+ .text-bg-danger {
429
+ background-color: var(--ucp-danger) !important;
430
+ color: #fff !important;
431
+ }
432
+
433
+ .bg-warning,
434
+ .text-bg-warning {
435
+ background-color: var(--ucp-warning) !important;
436
+ color: #000 !important;
437
+ }
438
+
439
+ .bg-info,
440
+ .text-bg-info {
441
+ background-color: var(--ucp-info) !important;
442
+ color: #fff !important;
443
+ }
444
+
445
+ .btn {
446
+ border-radius: 8px;
447
+ font-size: 0.875rem;
448
+ font-weight: 600;
449
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
450
+ }
451
+
452
+ .btn-sm {
453
+ border-radius: 6px;
454
+ font-size: 0.78rem;
455
+ }
456
+
457
+ .btn-primary {
458
+ --bs-btn-bg: var(--ucp-accent);
459
+ --bs-btn-border-color: var(--ucp-accent);
460
+ --bs-btn-color: #000;
461
+ --bs-btn-hover-bg: var(--ucp-accent-hover);
462
+ --bs-btn-hover-border-color: var(--ucp-accent-hover);
463
+ --bs-btn-hover-color: #000;
464
+ --bs-btn-active-bg: #d97706;
465
+ --bs-btn-active-border-color: #d97706;
466
+ --bs-btn-active-color: #000;
467
+ }
468
+
469
+ .btn-secondary,
470
+ .btn-outline-secondary,
471
+ .btn-light,
472
+ .btn-ghost {
473
+ --bs-btn-bg: rgba(39, 39, 42, 0.85);
474
+ --bs-btn-border-color: var(--ucp-border);
475
+ --bs-btn-color: var(--ucp-text);
476
+ --bs-btn-hover-bg: rgba(63, 63, 70, 0.85);
477
+ --bs-btn-hover-border-color: var(--ucp-border-strong);
478
+ --bs-btn-hover-color: var(--ucp-text);
479
+ --bs-btn-active-bg: rgba(245, 158, 11, 0.12);
480
+ --bs-btn-active-border-color: rgba(245, 158, 11, 0.35);
481
+ --bs-btn-active-color: var(--ucp-accent);
482
+ }
483
+
484
+ .btn-success {
485
+ --bs-btn-bg: var(--ucp-success);
486
+ --bs-btn-border-color: var(--ucp-success);
487
+ --bs-btn-color: #000;
488
+ --bs-btn-hover-bg: #4ade80;
489
+ --bs-btn-hover-border-color: #4ade80;
490
+ --bs-btn-hover-color: #000;
491
+ }
492
+
493
+ .btn-danger {
494
+ --bs-btn-bg: var(--ucp-danger);
495
+ --bs-btn-border-color: var(--ucp-danger);
496
+ --bs-btn-color: #fff;
497
+ --bs-btn-hover-bg: #f87171;
498
+ --bs-btn-hover-border-color: #f87171;
499
+ --bs-btn-hover-color: #fff;
500
+ }
501
+
502
+ .form-control,
503
+ .form-select,
504
+ .form-control:focus,
505
+ .bootstrap-tagsinput,
506
+ textarea,
507
+ input[type="text"],
508
+ input[type="search"],
509
+ input[type="email"],
510
+ input[type="password"] {
511
+ background-color: var(--ucp-sidebar);
512
+ border-color: var(--ucp-border);
513
+ color: var(--ucp-text);
514
+ border-radius: 8px;
515
+ }
516
+
517
+ .form-control:focus,
518
+ .form-select:focus,
519
+ .bootstrap-tagsinput:focus-within,
520
+ textarea:focus,
521
+ input:focus {
522
+ border-color: rgba(245, 158, 11, 0.55);
523
+ box-shadow: 0 0 0 0.2rem rgba(245, 158, 11, 0.12);
524
+ }
525
+
526
+ .form-control::placeholder,
527
+ textarea::placeholder,
528
+ input::placeholder {
529
+ color: var(--ucp-text-subtle);
530
+ }
531
+
532
+ .form-check-input {
533
+ background-color: var(--ucp-sidebar);
534
+ border-color: var(--ucp-border-strong);
535
+ }
536
+
537
+ .form-check-input:checked {
538
+ background-color: var(--ucp-accent);
539
+ border-color: var(--ucp-accent);
540
+ }
541
+
542
+ .alert {
543
+ border-radius: 8px;
544
+ border-width: 1px;
545
+ }
546
+
547
+ .alert-info {
548
+ background: rgba(59, 130, 246, 0.1);
549
+ border-color: rgba(59, 130, 246, 0.25);
550
+ color: #93c5fd;
551
+ }
552
+
553
+ .alert-success {
554
+ background: rgba(34, 197, 94, 0.1);
555
+ border-color: rgba(34, 197, 94, 0.25);
556
+ color: #86efac;
557
+ }
558
+
559
+ .alert-warning {
560
+ background: rgba(245, 158, 11, 0.12);
561
+ border-color: rgba(245, 158, 11, 0.3);
562
+ color: #fcd34d;
563
+ }
564
+
565
+ .alert-danger {
566
+ background: rgba(239, 68, 68, 0.1);
567
+ border-color: rgba(239, 68, 68, 0.25);
568
+ color: #fca5a5;
569
+ }
570
+
571
+ .breadcrumb {
572
+ gap: 6px;
573
+ margin-bottom: 0;
574
+ }
575
+
576
+ .breadcrumb-item,
577
+ .breadcrumb-item a {
578
+ color: var(--ucp-text-muted);
579
+ font-size: 0.82rem;
580
+ }
581
+
582
+ .breadcrumb-item.active {
583
+ color: var(--ucp-text) !important;
584
+ }
585
+
586
+ .posts.timeline {
587
+ padding-left: 0 !important;
588
+ }
589
+
590
+ .posts.timeline > [component="post"] {
591
+ position: relative;
592
+ border-left: 2px solid var(--ucp-border);
593
+ margin-left: 5px;
594
+ padding-left: 18px;
595
+ }
596
+
597
+ .posts.timeline > [component="post"]::before {
598
+ position: absolute;
599
+ top: 1.25rem;
600
+ left: -6px;
601
+ width: 10px;
602
+ height: 10px;
603
+ content: "";
604
+ background: var(--ucp-accent);
605
+ border: 2px solid var(--ucp-card);
606
+ border-radius: 50%;
607
+ }
608
+
609
+ .post-container {
610
+ background: var(--ucp-card);
611
+ border: 1px solid var(--ucp-border);
612
+ border-radius: 12px;
613
+ padding: 16px;
614
+ }
615
+
616
+ .post-header,
617
+ .post-footer {
618
+ border-color: var(--ucp-border) !important;
619
+ }
620
+
621
+ [component="post/content"] {
622
+ color: var(--ucp-text);
623
+ line-height: 1.65;
624
+ }
625
+
626
+ [component="post/signature"] {
627
+ border-top: 1px solid var(--ucp-border);
628
+ padding-top: 10px;
629
+ }
630
+
631
+ [component="post/replies/container"] {
632
+ background: rgba(17, 17, 19, 0.9);
633
+ border-color: var(--ucp-border) !important;
634
+ }
635
+
636
+ .topic-sidebar-tools .btn,
637
+ [component="topic/navigator"] .btn,
638
+ .topic [component="post/actions"] .btn {
639
+ color: var(--ucp-text-muted);
640
+ }
641
+
642
+ .topic-sidebar-tools .btn:hover,
643
+ [component="topic/navigator"] .btn:hover,
644
+ .topic [component="post/actions"] .btn:hover {
645
+ color: var(--ucp-accent);
646
+ }
647
+
648
+ .table {
649
+ --bs-table-bg: transparent;
650
+ --bs-table-color: var(--ucp-text);
651
+ --bs-table-border-color: var(--ucp-border);
652
+ --bs-table-hover-bg: rgba(255, 255, 255, 0.025);
653
+ color: var(--ucp-text);
654
+ }
655
+
656
+ .table th {
657
+ color: var(--ucp-text-muted);
658
+ font-size: 0.7rem;
659
+ font-weight: 700;
660
+ letter-spacing: 0.05em;
661
+ text-transform: uppercase;
662
+ }
663
+
664
+ .pagination .page-link {
665
+ background: var(--ucp-card);
666
+ border-color: var(--ucp-border);
667
+ color: var(--ucp-text-muted);
668
+ }
669
+
670
+ .pagination .page-link:hover,
671
+ .pagination .page-item.active .page-link {
672
+ background: rgba(245, 158, 11, 0.12);
673
+ border-color: rgba(245, 158, 11, 0.35);
674
+ color: var(--ucp-accent);
675
+ }
676
+
677
+ .avatar,
678
+ [component="user/picture"] {
679
+ border: 1px solid rgba(245, 158, 11, 0.22);
680
+ }
681
+
682
+ [component="user/status"].online,
683
+ .status.online {
684
+ background: var(--ucp-success) !important;
685
+ }
686
+
687
+ [component="user/status"].away,
688
+ .status.away {
689
+ background: var(--ucp-warning) !important;
690
+ }
691
+
692
+ [component="user/status"].dnd,
693
+ .status.dnd,
694
+ [component="user/status"].offline,
695
+ .status.offline {
696
+ background: var(--ucp-danger) !important;
697
+ }
698
+
699
+ .bottombar-nav {
700
+ background: var(--ucp-sidebar) !important;
701
+ border-color: var(--ucp-border) !important;
702
+ color: var(--ucp-text) !important;
703
+ }
704
+
705
+ .navigation-dropdown,
706
+ .bottombar .dropdown-menu {
707
+ background: var(--ucp-card);
708
+ border-color: var(--ucp-border);
709
+ }
710
+
711
+ .cmd {
712
+ display: inline-flex;
713
+ align-items: center;
714
+ padding: 4px 8px;
715
+ background: rgba(245, 158, 11, 0.1);
716
+ border: 1px solid rgba(245, 158, 11, 0.3);
717
+ border-radius: 4px;
718
+ color: var(--ucp-accent);
719
+ font-size: 0.75rem;
720
+ }
721
+
722
+ .blocked-reason {
723
+ display: flex;
724
+ gap: 8px;
725
+ align-items: flex-start;
726
+ padding: 10px 12px;
727
+ background: rgba(239, 68, 68, 0.1);
728
+ border: 1px solid rgba(239, 68, 68, 0.2);
729
+ border-radius: 6px;
730
+ color: #fca5a5;
731
+ font-size: 0.75rem;
732
+ }
733
+
734
+ .progress,
735
+ .progress-bar-bg {
736
+ background: var(--ucp-border);
737
+ border-radius: 4px;
738
+ }
739
+
740
+ .progress-bar {
741
+ background: var(--ucp-accent);
742
+ border-radius: 4px;
743
+ }
744
+
745
+ ::-webkit-scrollbar {
746
+ width: 6px;
747
+ height: 6px;
748
+ }
749
+
750
+ ::-webkit-scrollbar-track {
751
+ background: transparent;
752
+ }
753
+
754
+ ::-webkit-scrollbar-thumb {
755
+ background: var(--ucp-border);
756
+ border-radius: 3px;
757
+ }
758
+
759
+ ::-webkit-scrollbar-thumb:hover {
760
+ background: var(--ucp-border-strong);
761
+ }
762
+
763
+ @media (max-width: 991.98px) {
764
+ body {
765
+ padding-bottom: 4.25rem;
766
+ }
767
+
768
+ [component="categories/category"],
769
+ [component="category/topic"],
770
+ .post-container,
771
+ .category-header {
772
+ border-radius: 10px;
773
+ padding: 14px !important;
774
+ }
775
+
776
+ .brand-container {
777
+ padding-inline: 12px;
778
+ }
779
+ }
@@ -0,0 +1,57 @@
1
+ // Bootstrap and Harmony variable overrides for the Flawless RP theme.
2
+
3
+ $white: #fafafa !default;
4
+ $gray-100: #f4f4f5 !default;
5
+ $gray-200: #e4e4e7 !default;
6
+ $gray-300: #d4d4d8 !default;
7
+ $gray-400: #a1a1aa !default;
8
+ $gray-500: #71717a !default;
9
+ $gray-600: #52525b !default;
10
+ $gray-700: #3f3f46 !default;
11
+ $gray-800: #27272a !default;
12
+ $gray-900: #18181b !default;
13
+ $black: #000 !default;
14
+
15
+ $blue: #3b82f6 !default;
16
+ $red: #ef4444 !default;
17
+ $yellow: #f59e0b !default;
18
+ $green: #22c55e !default;
19
+ $cyan: #06b6d4 !default;
20
+
21
+ $primary: $yellow !default;
22
+ $secondary: $gray-400 !default;
23
+ $success: $green !default;
24
+ $info: $blue !default;
25
+ $warning: $yellow !default;
26
+ $danger: $red !default;
27
+ $light: $gray-100 !default;
28
+ $dark: #0d0d0f !default;
29
+
30
+ $body-color: $white !default;
31
+ $body-bg: #0d0d0f !default;
32
+ $body-tertiary-bg: $gray-900 !default;
33
+ $text-muted: $gray-400 !default;
34
+ $border-color: $gray-800 !default;
35
+ $link-color: #fbbf24 !default;
36
+ $link-hover-color: #fcd34d !default;
37
+
38
+ $form-check-input-border: var(--bs-border-width) solid $gray-600 !default;
39
+ $input-placeholder-color: $gray-500 !default;
40
+
41
+ $enable-caret: false;
42
+ $enable-smooth-scroll: false;
43
+ $enable-shadows: true;
44
+
45
+ $link-decoration: none;
46
+ $link-hover-decoration: none;
47
+
48
+ $font-family-sans-serif: "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
49
+ $font-family-secondary: "Barlow Condensed", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
50
+ $font-family-monospace: "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace !default;
51
+ $font-weight-semibold: 600 !default;
52
+ $small-font-size: 0.875rem !default;
53
+
54
+ $breadcrumb-divider: "/" !default;
55
+ $breadcrumb-divider-color: $gray-500 !default;
56
+ $breadcrumb-active-color: $body-color !default;
57
+ $breadcrumb-item-padding-x: 12px !default;
@@ -0,0 +1,72 @@
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="harmony-settings">
7
+ <div class="form-check form-switch">
8
+ <input type="checkbox" class="form-check-input" id="enableQuickReply" name="enableQuickReply" />
9
+ <label for="enableQuickReply" class="form-check-label">[[themes/harmony:settings.enableQuickReply]]</label>
10
+ </div>
11
+ <div class="form-check form-switch">
12
+ <input type="checkbox" class="form-check-input" id="enableBreadcrumbs" name="enableBreadcrumbs" />
13
+ <label for="enableBreadcrumbs" class="form-check-label">[[themes/harmony:settings.enableBreadcrumbs]]</label>
14
+ <p class="form-text">[[themes/harmony:settings.enableBreadcrumbs.why]]</p>
15
+ </div>
16
+ <div class="form-check form-switch">
17
+ <input type="checkbox" class="form-check-input" id="centerHeaderElements" name="centerHeaderElements" />
18
+ <label for="centerHeaderElements" class="form-check-label">[[themes/harmony:settings.centerHeaderElements]]</label>
19
+ </div>
20
+ <div class="form-check form-switch">
21
+ <input type="checkbox" class="form-check-input" id="mobileTopicTeasers" name="mobileTopicTeasers" />
22
+ <label for="mobileTopicTeasers" class="form-check-label">[[themes/harmony:settings.mobileTopicTeasers]]</label>
23
+ </div>
24
+ <div class="form-check form-switch">
25
+ <input type="checkbox" class="form-check-input" id="stickyToolbar" name="stickyToolbar" />
26
+ <div for="stickyToolbar" class="form-check-label">
27
+ [[themes/harmony:settings.stickyToolbar]]
28
+ <p class="form-text">
29
+ [[themes/harmony:settings.stickyToolbar.help]]
30
+ </p>
31
+ </div>
32
+ </div>
33
+ <div class="form-check form-switch">
34
+ <input type="checkbox" class="form-check-input" id="topicSidebarTools" name="topicSidebarTools" />
35
+ <div for="topicSidebarTools" class="form-check-label">
36
+ [[themes/harmony:settings.topicSidebarTools]]
37
+ <p class="form-text">
38
+ [[themes/harmony:settings.topicSidebarTools.help]]
39
+ </p>
40
+ </div>
41
+ </div>
42
+ <div class="form-check form-switch">
43
+ <input type="checkbox" class="form-check-input" id="autohideBottombar" name="autohideBottombar" />
44
+ <div for="autohideBottombar" class="form-check-label">
45
+ [[themes/harmony:settings.autohideBottombar]]
46
+ <p class="form-text">
47
+ [[themes/harmony:settings.autohideBottombar.help]]
48
+ </p>
49
+ </div>
50
+ </div>
51
+ <div class="form-check form-switch">
52
+ <input type="checkbox" class="form-check-input" id="topMobilebar" name="topMobilebar" />
53
+ <div for="topMobilebar" class="form-check-label">
54
+ [[themes/harmony:settings.topMobilebar]]
55
+ </div>
56
+ </div>
57
+ <div class="form-check form-switch">
58
+ <input type="checkbox" class="form-check-input" id="openSidebars" name="openSidebars" />
59
+ <label for="openSidebars" class="form-check-label">[[themes/harmony:settings.openSidebars]]</label>
60
+ </div>
61
+ <div class="form-check form-switch">
62
+ <input type="checkbox" class="form-check-input" id="chatModals" name="chatModals" />
63
+ <div for="chatModals" class="form-check-label">
64
+ [[themes/harmony:settings.chatModals]]
65
+ </div>
66
+ </div>
67
+ </form>
68
+ </div>
69
+
70
+ <!-- IMPORT admin/partials/settings/toc.tpl -->
71
+ </div>
72
+ </div>
package/theme.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "nodebb-theme-flawless",
3
+ "name": "Flawless RP",
4
+ "description": "A dark amber NodeBB theme based on the Flawless UCP dashboard styling.",
5
+ "url": "https://github.com/your-org/nodebb-theme-flawless",
6
+ "baseTheme": "nodebb-theme-harmony"
7
+ }
package/theme.scss ADDED
@@ -0,0 +1,5 @@
1
+ $font-path: "./plugins/nodebb-theme-flawless";
2
+
3
+ @import "./scss/overrides";
4
+ @import "../nodebb-theme-harmony/theme";
5
+ @import "./scss/flawless";