underpost 2.8.881 → 2.8.883

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.
Files changed (36) hide show
  1. package/.github/workflows/release.cd.yml +1 -2
  2. package/README.md +50 -36
  3. package/bin/db.js +1 -4
  4. package/cli.md +86 -86
  5. package/conf.js +1 -0
  6. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  7. package/manifests/deployment/dd-test-development/deployment.yaml +6 -6
  8. package/manifests/maas/device-scan.sh +1 -1
  9. package/package.json +1 -1
  10. package/src/api/document/document.service.js +9 -1
  11. package/src/cli/repository.js +2 -0
  12. package/src/client/components/core/Auth.js +258 -89
  13. package/src/client/components/core/BtnIcon.js +10 -1
  14. package/src/client/components/core/CssCore.js +36 -27
  15. package/src/client/components/core/Docs.js +188 -85
  16. package/src/client/components/core/LoadingAnimation.js +5 -10
  17. package/src/client/components/core/Modal.js +262 -120
  18. package/src/client/components/core/ObjectLayerEngine.js +154 -158
  19. package/src/client/components/core/Panel.js +2 -0
  20. package/src/client/components/core/PanelForm.js +94 -60
  21. package/src/client/components/core/Router.js +15 -15
  22. package/src/client/components/core/ToolTip.js +83 -19
  23. package/src/client/components/core/Translate.js +1 -1
  24. package/src/client/components/core/VanillaJs.js +4 -3
  25. package/src/client/components/core/windowGetDimensions.js +202 -0
  26. package/src/client/components/default/MenuDefault.js +11 -0
  27. package/src/client/ssr/Render.js +1 -1
  28. package/src/index.js +1 -1
  29. package/src/runtime/lampp/Lampp.js +253 -128
  30. package/src/server/auth.js +68 -17
  31. package/src/server/crypto.js +195 -76
  32. package/src/server/peer.js +47 -5
  33. package/src/server/process.js +85 -1
  34. package/src/server/runtime.js +13 -32
  35. package/test/crypto.test.js +117 -0
  36. package/src/runtime/xampp/Xampp.js +0 -83
@@ -53,12 +53,20 @@ const DocumentService = {
53
53
  const limit = req.query.limit ? parseInt(req.query.limit, 10) : 6;
54
54
  const skip = req.query.skip ? parseInt(req.query.skip, 10) : 0;
55
55
 
56
- return await Document.find(queryPayload)
56
+ const data = await Document.find(queryPayload)
57
57
  .sort(sort)
58
58
  .limit(limit)
59
59
  .skip(skip)
60
60
  .populate(DocumentDto.populate.file())
61
61
  .populate(user && user.role !== 'guest' ? DocumentDto.populate.user() : null);
62
+
63
+ const lastDoc = await Document.findOne(queryPayload, '_id').sort({ createdAt: 1 });
64
+ const lastId = lastDoc ? lastDoc._id : null;
65
+
66
+ return {
67
+ data,
68
+ lastId,
69
+ };
62
70
  }
63
71
 
64
72
  switch (req.params.id) {
@@ -153,6 +153,8 @@ class UnderpostRepository {
153
153
  if (fs.existsSync(privateRepoPath)) fs.removeSync(privateRepoPath);
154
154
  shellExec(`cd .. && underpost clone ${process.env.GITHUB_USERNAME}/${privateRepoName}`);
155
155
  shellExec(`cd ${privateRepoPath} && underpost pull . ${process.env.GITHUB_USERNAME}/${privateRepoName}`);
156
+ shellExec(`underpost run secret`);
157
+ shellExec(`underpost run underpost-config`);
156
158
  const packageJsonDeploy = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/package.json`, 'utf8'));
157
159
  const packageJsonEngine = JSON.parse(fs.readFileSync(`./package.json`, 'utf8'));
158
160
  if (packageJsonDeploy.version !== packageJsonEngine.version) {
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Utility class for authentication state management and session lifecycle control.
3
+ * This class is designed to be used as a singleton instance (exported as 'Auth').
4
+ * @module src/client/components/core/Auth.js
5
+ * @namespace AuthClient
6
+ */
7
+
1
8
  import { UserMock, UserService } from '../../services/user/user.service.js';
2
9
  import { Account } from './Account.js';
3
10
  import { loggerFactory } from './Logger.js';
@@ -9,95 +16,202 @@ import { s } from './VanillaJs.js';
9
16
 
10
17
  const logger = loggerFactory(import.meta, { trace: true });
11
18
 
12
- const token = Symbol('token');
13
-
14
- const guestToken = Symbol('guestToken');
15
-
16
- const refreshTimeout = Symbol('refreshTimeout');
17
-
18
- const Auth = {
19
- [token]: '',
20
- [guestToken]: '',
21
- setToken: function (value = '') {
22
- return (this[token] = value);
23
- },
24
- deleteToken: function () {
25
- return (this[token] = '');
26
- },
27
- getToken: function () {
28
- return this[token];
29
- },
30
- setGuestToken: function (value = '') {
31
- return (this[guestToken] = value);
32
- },
33
- deleteGuestToken: function () {
34
- return (this[guestToken] = '');
35
- },
36
- getGuestToken: function () {
37
- return this[guestToken];
38
- },
39
- getJWT: function () {
40
- if (Auth.getToken()) return `Bearer ${Auth.getToken()}`;
41
- if (Auth.getGuestToken()) return `Bearer ${Auth.getGuestToken()}`;
19
+ /**
20
+ * Manages user authentication state, tokens, and session lifecycle.
21
+ * @memberof AuthClient
22
+ */
23
+ class Auth {
24
+ /**
25
+ * The current user access token (JWT).
26
+ * @type {string}
27
+ * @private
28
+ */
29
+ #token = '';
30
+
31
+ /**
32
+ * The token for anonymous guest sessions.
33
+ * @type {string}
34
+ * @private
35
+ */
36
+ #guestToken = '';
37
+
38
+ /**
39
+ * Timeout ID for the token refresh schedule.
40
+ * @type {number | undefined}
41
+ * @private
42
+ */
43
+ #refreshTimeout;
44
+
45
+ /**
46
+ * Creates an instance of Auth.
47
+ */
48
+ constructor() {
49
+ // Private fields are initialized above.
50
+ }
51
+
52
+ // --- Token Management ---
53
+
54
+ /**
55
+ * Sets the user's access token.
56
+ * @memberof AuthClient.Auth
57
+ * @param {string} [value=''] - The JWT token value.
58
+ * @returns {string} The set token value.
59
+ */
60
+ setToken(value = '') {
61
+ return (this.#token = value);
62
+ }
63
+
64
+ /**
65
+ * Clears the user's access token.
66
+ * @memberof AuthClient.Auth
67
+ * @returns {string} An empty string.
68
+ */
69
+ deleteToken() {
70
+ return (this.#token = '');
71
+ }
72
+
73
+ /**
74
+ * Gets the user's access token.
75
+ * @memberof AuthClient.Auth
76
+ * @returns {string} The JWT token.
77
+ */
78
+ getToken() {
79
+ return this.#token;
80
+ }
81
+
82
+ /**
83
+ * Sets the anonymous guest token.
84
+ * @memberof AuthClient.Auth
85
+ * @param {string} [value=''] - The guest token value.
86
+ * @returns {string} The set guest token value.
87
+ */
88
+ setGuestToken(value = '') {
89
+ return (this.#guestToken = value);
90
+ }
91
+
92
+ /**
93
+ * Clears the anonymous guest token.
94
+ * @memberof AuthClient.Auth
95
+ * @returns {string} An empty string.
96
+ */
97
+ deleteGuestToken() {
98
+ return (this.#guestToken = '');
99
+ }
100
+
101
+ /**
102
+ * Gets the anonymous guest token.
103
+ * @memberof AuthClient.Auth
104
+ * @returns {string} The guest token.
105
+ */
106
+ getGuestToken() {
107
+ return this.#guestToken;
108
+ }
109
+
110
+ /**
111
+ * Generates the JWT header string (e.g., "Bearer [token]") using the active token (user or guest).
112
+ * @memberof AuthClient.Auth
113
+ * @returns {string} The Bearer token string or an empty string.
114
+ */
115
+ getJWT() {
116
+ if (this.getToken()) return `Bearer ${this.getToken()}`;
117
+ if (this.getGuestToken()) return `Bearer ${this.getGuestToken()}`;
42
118
  return '';
43
- },
44
- decodeJwt: function (token) {
119
+ }
120
+
121
+ /**
122
+ * Decodes the payload section of a JWT token.
123
+ * @static
124
+ * @memberof AuthClient.Auth
125
+ * @param {string} token - The JWT string.
126
+ * @returns {object | null} The decoded JWT payload object, or null on failure.
127
+ */
128
+ static decodeJwt(token) {
45
129
  try {
130
+ // Uses atob for base64 decoding of the middle part of the JWT
46
131
  return JSON.parse(atob(token.split('.')[1]));
47
132
  } catch (e) {
133
+ logger.error('Failed to decode JWT:', e);
48
134
  return null;
49
135
  }
50
- },
51
- scheduleTokenRefresh: function () {
52
- if (this[refreshTimeout]) {
53
- clearTimeout(this[refreshTimeout]);
136
+ }
137
+
138
+ // --- Session Management ---
139
+
140
+ /**
141
+ * Schedules the access token to be refreshed shortly before it expires.
142
+ * Clears any existing refresh timeout before setting a new one.
143
+ * @memberof AuthClient.Auth
144
+ * @returns {void}
145
+ */
146
+ scheduleTokenRefresh() {
147
+ if (this.#refreshTimeout) {
148
+ clearTimeout(this.#refreshTimeout);
149
+ this.#refreshTimeout = undefined;
54
150
  }
55
151
 
56
- const currentToken = Auth.getToken();
152
+ const currentToken = this.getToken();
57
153
  if (!currentToken) return;
58
154
 
59
155
  const payload = Auth.decodeJwt(currentToken);
60
- if (!payload || !payload.refreshExpiresAt) return;
156
+ if (!payload || !payload.refreshExpiresAt) return; // Requires refreshExpiresAt in milliseconds
61
157
 
62
158
  const expiresIn = payload.refreshExpiresAt - Date.now();
63
- const refreshBuffer = 2 * 60 * 1000; // 2 minutes
159
+ const refreshBuffer = 2 * 60 * 1000; // 2 minutes buffer before expiry
64
160
  const refreshIn = expiresIn - refreshBuffer;
65
161
 
66
- logger.info(`Token refresh in ${refreshIn / (1000 * 60)} minutes`);
162
+ logger.info(`Token refresh scheduled in ${refreshIn / (1000 * 60)} minutes`);
67
163
 
68
- if (refreshIn <= 0) return; // Already expired or close to it
164
+ if (refreshIn <= 0) {
165
+ logger.warn('Token already expired or too close to expiry, skipping refresh schedule.');
166
+ return;
167
+ }
69
168
 
70
- this[refreshTimeout] = setTimeout(async () => {
71
- const { data, status } = await UserService.get({ id: 'auth' });
72
- if (status === 'success') {
73
- logger.info('Refreshed access token.');
74
- Auth.setToken(data.token);
169
+ this.#refreshTimeout = setTimeout(async () => {
170
+ const { data, status } = await UserService.get({ id: 'auth' }); // API call to get a fresh token
171
+ if (status === 'success' && data?.token) {
172
+ logger.info('Successfully refreshed access token.');
173
+ this.setToken(data.token);
75
174
  localStorage.setItem('jwt', data.token);
76
- Auth.scheduleTokenRefresh();
77
- } else Auth.sessionOut();
175
+ this.scheduleTokenRefresh(); // Schedule the next refresh
176
+ } else {
177
+ logger.warn('Token refresh failed, attempting session out.');
178
+ this.sessionOut();
179
+ }
78
180
  }, refreshIn);
79
- },
80
- sessionIn: async function (userServicePayload) {
181
+ }
182
+
183
+ /**
184
+ * Establishes a user session (logged-in) or falls back to a guest session.
185
+ * It attempts to use the provided token or a token from localStorage ('jwt').
186
+ * @memberof AuthClient.Auth
187
+ * @param {object} [userServicePayload] - Payload from a successful login/signup call to UserService.
188
+ * @returns {Promise<{user: object}>} A promise resolving to the current user object.
189
+ */
190
+ async sessionIn(userServicePayload) {
81
191
  try {
82
- const token = userServicePayload?.data?.token ? userServicePayload.data.token : localStorage.getItem('jwt');
192
+ let token = userServicePayload?.data?.token || localStorage.getItem('jwt');
193
+
83
194
  if (token) {
84
- Auth.setToken(token);
195
+ this.setToken(token);
85
196
 
86
197
  const result = userServicePayload
87
198
  ? userServicePayload // From login/signup
88
- : await UserService.get({ id: 'auth' });
199
+ : await UserService.get({ id: 'auth' }); // Verify token with backend
89
200
 
90
201
  const { status, data, message } = result;
91
- if (status === 'success') {
92
- Auth.setToken(data.token);
93
- localStorage.setItem('jwt', token);
94
- Auth.renderSessionUI();
202
+
203
+ if (status === 'success' && data.token) {
204
+ // A valid user token was found/refreshed
205
+ this.setToken(data.token);
206
+ localStorage.setItem('jwt', data.token);
207
+ this.renderSessionUI();
95
208
  await LogIn.Trigger({ user: data.user });
96
209
  await Account.updateForm(data.user);
97
- Auth.scheduleTokenRefresh();
210
+ this.scheduleTokenRefresh();
98
211
  return { user: data.user };
99
- }
100
- if (message && message.match('expired'))
212
+ } else if (message && message.match('expired')) {
213
+ logger.warn('User session token expired.');
214
+ // Redirect to login modal and push notification
101
215
  setTimeout(() => {
102
216
  s(`.main-btn-log-in`).click();
103
217
  NotificationManager.Push({
@@ -105,69 +219,124 @@ const Auth = {
105
219
  status: 'warning',
106
220
  });
107
221
  });
222
+ }
108
223
  }
109
- // Important delete session token if guest token already exists
110
- Auth.deleteToken();
224
+
225
+ // Cleanup failed user session attempt
226
+ this.deleteToken();
111
227
  localStorage.removeItem('jwt');
112
228
 
113
- // Anon guest session
229
+ // Anon guest session attempt
114
230
  let guestToken = localStorage.getItem('jwt.g');
115
231
  if (guestToken) {
116
- Auth.setGuestToken(guestToken);
117
- let { data, status, message } = await UserService.get({ id: 'auth' });
118
- if (status === 'success') {
232
+ this.setGuestToken(guestToken);
233
+ let { data, status, message } = await UserService.get({ id: 'auth' }); // Verify guest token
234
+ if (status === 'success' && data.token) {
235
+ // Guest token is valid and refreshed
236
+ this.setGuestToken(data.token);
237
+ localStorage.setItem('jwt.g', data.token);
119
238
  await LogIn.Trigger(data);
120
239
  await Account.updateForm(data.user);
121
240
  return data;
122
- } else logger.error(message);
241
+ } else {
242
+ logger.error(`Guest token validation failed: ${message}`);
243
+ // Fall through to full sessionOut to re-create guest session
244
+ }
123
245
  }
124
- return await Auth.sessionOut();
246
+
247
+ // If all attempts fail, create a new guest session (which calls sessionIn recursively)
248
+ return await this.sessionOut();
125
249
  } catch (error) {
126
- logger.error(error);
250
+ logger.error('Error during sessionIn process:', error);
251
+ // Fallback to a mock user object
127
252
  return { user: UserMock.default };
128
253
  }
129
- },
130
- sessionOut: async function () {
131
- {
254
+ }
255
+
256
+ /**
257
+ * Ends the current user session (logout) and initiates a new anonymous guest session.
258
+ * @memberof AuthClient.Auth
259
+ * @returns {Promise<object>} A promise resolving to the newly created guest session data.
260
+ */
261
+ async sessionOut() {
262
+ // 1. End User Session
263
+ try {
132
264
  const result = await UserService.delete({ id: 'logout' });
133
265
  localStorage.removeItem('jwt');
134
- Auth.deleteToken();
135
- if (this[refreshTimeout]) {
136
- clearTimeout(this[refreshTimeout]);
266
+ this.deleteToken();
267
+ if (this.#refreshTimeout) {
268
+ clearTimeout(this.#refreshTimeout);
269
+ this.#refreshTimeout = undefined;
137
270
  }
138
- Auth.renderGuestUi();
271
+ this.renderGuestUi();
272
+ // Reset user data in the LogIn state/model
139
273
  LogIn.Scope.user.main.model.user = {};
140
274
  await LogOut.Trigger(result);
275
+ } catch (error) {
276
+ logger.error('Error during user logout:', error);
141
277
  }
142
- {
278
+
279
+ // 2. Start Guest Session
280
+ try {
143
281
  localStorage.removeItem('jwt.g');
144
- Auth.deleteGuestToken();
145
- const result = await UserService.post({ id: 'guest' });
146
- localStorage.setItem('jwt.g', result.data.token);
147
- Auth.setGuestToken(result.data.token);
148
- return await Auth.sessionIn();
282
+ this.deleteGuestToken();
283
+ const result = await UserService.post({ id: 'guest' }); // Request a new guest token
284
+
285
+ if (result.status === 'success' && result.data.token) {
286
+ localStorage.setItem('jwt.g', result.data.token);
287
+ this.setGuestToken(result.data.token);
288
+ // Recursively call sessionIn to complete the guest login process (UI update, etc.)
289
+ return await this.sessionIn();
290
+ } else {
291
+ logger.error('Failed to get a new guest token.');
292
+ return { user: UserMock.default };
293
+ }
294
+ } catch (error) {
295
+ logger.error('Error during guest session creation:', error);
296
+ return { user: UserMock.default };
149
297
  }
150
- },
151
- renderSessionUI: function () {
298
+ }
299
+
300
+ // --- UI Rendering ---
301
+
302
+ /**
303
+ * Renders the UI for a logged-in user (hides Log In/Sign Up, shows Log Out/Account).
304
+ * Also closes any active login/signup modals.
305
+ * @memberof AuthClient.Auth
306
+ * @returns {void}
307
+ */
308
+ renderSessionUI() {
152
309
  s(`.main-btn-log-in`).style.display = 'none';
153
310
  s(`.main-btn-sign-up`).style.display = 'none';
154
311
  s(`.main-btn-log-out`).style.display = null;
155
312
  s(`.main-btn-account`).style.display = null;
156
313
  setTimeout(() => {
314
+ // Close any open login/signup modals
157
315
  if (s(`.modal-log-in`)) s(`.btn-close-modal-log-in`).click();
158
316
  if (s(`.modal-sign-up`)) s(`.btn-close-modal-sign-up`).click();
159
317
  });
160
- },
161
- renderGuestUi: function () {
318
+ }
319
+
320
+ /**
321
+ * Renders the UI for a guest user (shows Log In/Sign Up, hides Log Out/Account).
322
+ * Also closes any active logout/account modals.
323
+ * @memberof AuthClient.Auth
324
+ * @returns {void}
325
+ */
326
+ renderGuestUi() {
162
327
  s(`.main-btn-log-in`).style.display = null;
163
328
  s(`.main-btn-sign-up`).style.display = null;
164
329
  s(`.main-btn-log-out`).style.display = 'none';
165
330
  s(`.main-btn-account`).style.display = 'none';
166
331
  setTimeout(() => {
332
+ // Close any open logout/account modals
167
333
  if (s(`.modal-log-out`)) s(`.btn-close-modal-log-out`).click();
168
334
  if (s(`.modal-account`)) s(`.btn-close-modal-account`).click();
169
335
  });
170
- },
171
- };
336
+ }
337
+ }
338
+
339
+ // Export a singleton instance of the Auth class to maintain the original utility object access pattern.
340
+ const AuthSingleton = new Auth();
172
341
 
173
- export { Auth };
342
+ export { AuthSingleton as Auth };
@@ -15,9 +15,12 @@ const BtnIcon = {
15
15
  labelStyle: '',
16
16
  tabHref: '',
17
17
  tooltipHtml: '',
18
+ useVisibilityHover: false,
19
+ useMenuBtn: false,
18
20
  },
19
21
  ) {
20
22
  const tokenId = getId(this.Tokens, 'btn-token-');
23
+ if (options.useMenuBtn) options.class += ' main-menu-btn-selector';
21
24
  this.Tokens[tokenId] = { ...options };
22
25
  setTimeout(() => {
23
26
  if (s(`.a-${tokenId}`)) s(`.a-${tokenId}`).onclick = (e) => e.preventDefault();
@@ -54,7 +57,13 @@ const BtnIcon = {
54
57
  if (options.tooltipHtml)
55
58
  setTimeout(() => {
56
59
  if (s(`.${tokenId}`))
57
- ToolTip.Render({ container: `.${tokenId}`, id: tokenId, htmlRender: options.tooltipHtml });
60
+ ToolTip.Render({
61
+ container: `.${tokenId}`,
62
+ id: tokenId,
63
+ htmlRender: options.tooltipHtml,
64
+ useVisibilityHover: !!options.useVisibilityHover,
65
+ useMenuBtn: !!options.useMenuBtn,
66
+ });
58
67
  });
59
68
  return render;
60
69
  },
@@ -130,19 +130,20 @@ const CssCommonCore = async () => {
130
130
  opacity: 0;
131
131
  }
132
132
  }
133
- .title-view-modal {
133
+ .title-main-modal {
134
134
  top: 8px;
135
- font-size: 21px !important;
136
- position: absolute !important;
135
+ font-size: 21px;
136
+ position: absolute;
137
137
  }
138
- .title-view-modal .view-title-icon {
139
- font-size: 21px !important;
138
+ .title-main-modal .view-title-icon {
139
+ font-size: 21px;
140
140
  }
141
141
  .down-arrow-submenu {
142
- top: -20px;
143
- text-align: right;
144
- padding-right: 42px;
142
+ top: 0px;
145
143
  color: #5f5f5f;
144
+ left: 115px;
145
+ transform-origin: center;
146
+ width: 0px;
146
147
  }
147
148
  .main-body-btn {
148
149
  width: 50px;
@@ -153,6 +154,9 @@ const CssCommonCore = async () => {
153
154
  .main-body-btn:hover {
154
155
  font-size: 21px;
155
156
  }
157
+ .main-menu-btn-selector {
158
+ overflow: hidden;
159
+ }
156
160
  </style>
157
161
  <style>
158
162
  .lds-dual-ring,
@@ -219,6 +223,16 @@ const CssCommonCore = async () => {
219
223
  height: 100px;
220
224
  color: gray;
221
225
  }
226
+ .menu-label-text {
227
+ transition: 0.3s;
228
+ position: relative;
229
+ }
230
+ .menu-btn-icon {
231
+ font-size: 20px;
232
+ width: 40px;
233
+ overflow: hidden;
234
+ text-align: center;
235
+ }
222
236
  </style>
223
237
  ${boxShadow({ selector: '.account-profile-image' })}
224
238
  <div class="ag-grid-style"></div>`;
@@ -329,11 +343,12 @@ const CssCoreDark = {
329
343
  }
330
344
  .main-btn-menu {
331
345
  text-align: left;
346
+ transition: none; /* sortable necessary */
332
347
  padding: 15px;
333
- transition: none;
334
348
  margin: 0;
335
349
  border: 0;
336
350
  height: 52px;
351
+ background: #121212;
337
352
  }
338
353
  .main-btn-menu-active {
339
354
  background: #212020;
@@ -435,10 +450,6 @@ const CssCoreDark = {
435
450
  margin: 15px 5px 5px;
436
451
  text-align: left;
437
452
  }
438
- .menu-btn-icon {
439
- font-size: 20px;
440
- margin: 12px;
441
- }
442
453
  .view-title-icon {
443
454
  font-size: 35px;
444
455
  margin: 20px;
@@ -535,6 +546,10 @@ const CssCoreDark = {
535
546
  padding: 5px;
536
547
  margin: 5px;
537
548
  }
549
+ .submenu-btn {
550
+ background: rgba(255, 255, 255, 0.1);
551
+ border-radius: 0;
552
+ }
538
553
  </style>
539
554
  ${scrollBarDarkRender()} ${borderChar(1, 'black', ['.main-body-btn-container'])}
540
555
  `,
@@ -583,12 +598,7 @@ const CssCoreLight = {
583
598
  .hover-active {
584
599
  background: #bbbbbb;
585
600
  }
586
- .title-modal {
587
- cursor: default;
588
- font-size: 20px;
589
- padding: 5px;
590
- margin: 5px;
591
- }
601
+
592
602
  .box-shadow {
593
603
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
594
604
  }
@@ -649,11 +659,12 @@ const CssCoreLight = {
649
659
  }
650
660
  .main-btn-menu {
651
661
  text-align: left;
662
+ transition: none; /* sortable necessary */
652
663
  padding: 15px;
653
- transition: none;
654
664
  margin: 0;
655
665
  border: 0;
656
666
  height: 52px;
667
+ background: #fff;
657
668
  }
658
669
  .main-btn-menu-active {
659
670
  background: #d8d8d8;
@@ -731,9 +742,7 @@ const CssCoreLight = {
731
742
  color: #333;
732
743
  background: 0 0;
733
744
  }
734
- .title-modal {
735
- color: #000;
736
- }
745
+
737
746
  input {
738
747
  cursor: pointer;
739
748
  color: #272727;
@@ -752,10 +761,6 @@ const CssCoreLight = {
752
761
  margin: 15px 5px 5px;
753
762
  text-align: left;
754
763
  }
755
- .menu-btn-icon {
756
- font-size: 20px;
757
- margin: 12px;
758
- }
759
764
  .view-title-icon {
760
765
  font-size: 35px;
761
766
  margin: 20px;
@@ -865,6 +870,10 @@ const CssCoreLight = {
865
870
  padding: 5px;
866
871
  margin: 5px;
867
872
  }
873
+ .submenu-btn {
874
+ background: rgba(0, 0, 0, 0.1);
875
+ border-radius: 0;
876
+ }
868
877
  </style>
869
878
  ${scrollBarLightRender()} ${borderChar(1, 'white', ['.main-body-btn-container'])}
870
879
  `,