@wxn0brp/db 0.0.1

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 (53) hide show
  1. package/CollectionManager.js +119 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -0
  4. package/action.js +250 -0
  5. package/cacheManager.js +83 -0
  6. package/database.js +208 -0
  7. package/executor.js +54 -0
  8. package/file/find.js +88 -0
  9. package/file/index.js +3 -0
  10. package/file/remove.js +75 -0
  11. package/file/update.js +83 -0
  12. package/file/utils.js +27 -0
  13. package/format.js +29 -0
  14. package/gen.js +97 -0
  15. package/graph.js +130 -0
  16. package/more.js +103 -0
  17. package/package.json +32 -0
  18. package/remote/client/database.js +229 -0
  19. package/remote/client/graph.js +139 -0
  20. package/remote/client/index.js +10 -0
  21. package/remote/server/auth.js +100 -0
  22. package/remote/server/db.js +197 -0
  23. package/remote/server/function.js +43 -0
  24. package/remote/server/graph.js +121 -0
  25. package/remote/server/gui/css/main.css +130 -0
  26. package/remote/server/gui/css/scrool.css +81 -0
  27. package/remote/server/gui/css/style.css +61 -0
  28. package/remote/server/gui/favicon.svg +12 -0
  29. package/remote/server/gui/html/data.html +15 -0
  30. package/remote/server/gui/html/main.html +46 -0
  31. package/remote/server/gui/html/nav.html +25 -0
  32. package/remote/server/gui/html/popup.html +51 -0
  33. package/remote/server/gui/index.html +49 -0
  34. package/remote/server/gui/js/api.js +166 -0
  35. package/remote/server/gui/js/index.js +17 -0
  36. package/remote/server/gui/js/loadHTML.js +16 -0
  37. package/remote/server/gui/js/popUp.js +72 -0
  38. package/remote/server/gui/js/queryApi.js +51 -0
  39. package/remote/server/gui/js/queryDb.js +79 -0
  40. package/remote/server/gui/js/queryGraph.js +144 -0
  41. package/remote/server/gui/js/render.js +64 -0
  42. package/remote/server/gui/js/templates.js +31 -0
  43. package/remote/server/gui/js/utils.js +36 -0
  44. package/remote/server/gui/js/vars.js +9 -0
  45. package/remote/server/gui/libs/core.js +176 -0
  46. package/remote/server/gui/libs/d3.v7.min.js +2 -0
  47. package/remote/server/gui/libs/handlebars.min.js +29 -0
  48. package/remote/server/gui/libs/json5.min.js +1 -0
  49. package/remote/server/index.js +63 -0
  50. package/remote/server/initDataBases.js +20 -0
  51. package/remote/server/pathUtils.js +7 -0
  52. package/remote/server/secret.js +23 -0
  53. package/remote/serverMgmt/index.js +86 -0
@@ -0,0 +1,100 @@
1
+ import jwt from "jwt-simple";
2
+ import NodeCache from "node-cache";
3
+ import getSecret from "./secret.js";
4
+ import crypto from "crypto";
5
+
6
+ const TOKEN_CACHE_TTL = process.env.TOKEN_CACHE_TTL || 900; // 15 minutes
7
+ const cache = new NodeCache({ stdTTL: TOKEN_CACHE_TTL, checkperiod: TOKEN_CACHE_TTL });
8
+ const secret = getSecret();
9
+
10
+ export async function authMiddleware(req, res, next){
11
+ const token = req.headers["authorization"];
12
+ if(!token){
13
+ return res.status(401).json({ err: true, msg: "Access denied. No token provided." });
14
+ }
15
+
16
+ if(cache.has(token)){
17
+ req.user = cache.get(token);
18
+ return next();
19
+ }
20
+
21
+ try{
22
+ const user = jwt.decode(token, secret);
23
+ if(!user || !user._id){
24
+ return res.status(401).json({ err: true, msg: "Invalid token." });
25
+ }
26
+
27
+ const tokenD = await global.db.findOne("token", { token });
28
+ if(!tokenD){
29
+ return res.status(401).json({ err: true, msg: "Invalid token." });
30
+ }
31
+
32
+ const userD = await global.db.findOne("user", { _id: user._id });
33
+ if(!userD){
34
+ return res.status(401).json({ err: true, msg: "Invalid token." });
35
+ }
36
+
37
+ req.user = user;
38
+ cache.set(token, user);
39
+ next();
40
+ }catch(err){
41
+ res.status(400).json({ err: true, msg: "An error occurred during authentication." });
42
+ }
43
+ }
44
+
45
+ export async function loginFunction(login, password){
46
+ const { err, user } = await checkUserAccess(login, password);
47
+ if(err){
48
+ return { err: true, msg: "Invalid login or password." };
49
+ }
50
+
51
+ const token = await generateToken({ _id: user._id });
52
+ cache.set(token, user);
53
+ return { err: false, token };
54
+ }
55
+
56
+ export async function generateToken(payload){
57
+ const token = jwt.encode(payload, secret);
58
+ await global.db.add("token", { token }, false);
59
+ return token;
60
+ }
61
+
62
+ export async function removeToken(token){
63
+ return await global.db.removeOne("token", { token });
64
+ }
65
+
66
+ export async function addUserAccess(login, password){
67
+ if(!/^[a-zA-Z0-9]+$/.test(login)) return { err: true, msg: "Login can only contain letters and numbers." };
68
+ if(login.length < 3 || login.length > 10) return { err: true, msg: "Login must be between 3 and 10 characters." };
69
+ if(password.length < 8 || password.length > 300) return { err: true, msg: "Password must be between 8 and 300 characters." };
70
+
71
+ const userExists = await global.db.findOne("user", { login });
72
+ if(userExists) return { err: true, msg: "Login already exists." };
73
+
74
+ password = generateHash(password);
75
+
76
+ const user = await global.db.add("user", {
77
+ login,
78
+ password
79
+ });
80
+ return { err: false, user };
81
+ }
82
+
83
+ export async function checkUserAccess(login, password){
84
+ const user = await global.db.findOne("user", { login });
85
+ if(!user) return { err: true, msg: "Invalid login or password." };
86
+
87
+ const hash = generateHash(password);
88
+ if(hash !== user.password) return { err: true, msg: "Invalid login or password." };
89
+
90
+ delete user.password;
91
+ return { err: false, user };
92
+ }
93
+
94
+ export async function removeUser(idOrLogin){
95
+ return await global.db.removeOne("user", { $or: [{ _id: idOrLogin }, { login: idOrLogin }] });
96
+ }
97
+
98
+ function generateHash(password){
99
+ return crypto.createHash("sha256").update(password).digest("hex");
100
+ }
@@ -0,0 +1,197 @@
1
+ import { Router } from "express";
2
+ import parseParam from "./function.js";
3
+ import { isPathSafe } from "./pathUtils.js";
4
+ const router = Router();
5
+
6
+ router.use((req, res, next) => {
7
+ if(req.dbType == "database") return next();
8
+ return res.status(400).json({ err: true, msg: "Invalid data center type." });
9
+ });
10
+
11
+ router.post("/getCollections", async (req, res) => {
12
+ try{
13
+ const db = req.dataCenter;
14
+ const result = await db.getCollections();
15
+ res.json({ err: false, result });
16
+ }catch(err){
17
+ console.error(err);
18
+ res.status(500).json({ err: true, msg: err.message });
19
+ }
20
+ });
21
+
22
+ router.post("/checkCollection", async (req, res) => {
23
+ const { collection } = req.body;
24
+ if(!collection) return res.status(400).json({ err: true, msg: "collection is required" });
25
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
26
+
27
+ try{
28
+ const db = req.dataCenter;
29
+ await db.checkCollection(collection);
30
+ res.json({ err: false, result: true });
31
+ }catch(err){
32
+ console.error(err);
33
+ res.status(500).json({ err: true, msg: err.message });
34
+ }
35
+ });
36
+
37
+ router.post("/issetCollection", async (req, res) => {
38
+ const { collection } = req.body;
39
+ if(!collection) return res.status(400).json({ err: true, msg: "collection is required" });
40
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
41
+
42
+ try{
43
+ const db = req.dataCenter;
44
+ const result = await db.issetCollection(collection);
45
+ res.json({ err: false, result });
46
+ }catch(err){
47
+ console.error(err);
48
+ res.status(500).json({ err: true, msg: err.message });
49
+ }
50
+ });
51
+
52
+ router.post("/add", async (req, res) => {
53
+ const { collection, data } = req.body;
54
+ if(!collection || !data) return res.status(400).json({ err: true, msg: "collection & data is required" });
55
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
56
+
57
+ try{
58
+ const db = req.dataCenter;
59
+ const result = await db.add(collection, data, req.body.id_gen || true);
60
+ res.json({ err: false, result });
61
+ }catch(err){
62
+ console.error(err);
63
+ res.status(500).json({ err: true, msg: err.message });
64
+ }
65
+ });
66
+
67
+ router.post("/find", async (req, res) => {
68
+ const { collection, search, context, options, findOpts } = req.body;
69
+ if(!collection || !search) return res.status(400).json({ err: true, msg: "collection & search is required" });
70
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
71
+
72
+ try{
73
+ const parsedSearch = parseParam(search);
74
+ const db = req.dataCenter;
75
+ const result = await db.find(collection, parsedSearch, context || {}, options || {}, findOpts || {});
76
+ res.json({ err: false, result });
77
+ }catch(err){
78
+ console.error(err);
79
+ res.status(500).json({ err: true, msg: err.message });
80
+ }
81
+ });
82
+
83
+ router.post("/findOne", async (req, res) => {
84
+ const { collection, search, context, findOpts } = req.body;
85
+ if(!collection || !search) return res.status(400).json({ err: true, msg: "collection & search is required" });
86
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
87
+
88
+ try{
89
+ const parsedSearch = parseParam(search);
90
+ const db = req.dataCenter;
91
+ const result = await db.findOne(collection, parsedSearch, context || {}, findOpts || {});
92
+ res.json({ err: false, result });
93
+ }catch(err){
94
+ console.error(err);
95
+ res.status(500).json({ err: true, msg: err.message });
96
+ }
97
+ });
98
+
99
+ router.post("/update", async (req, res) => {
100
+ const { collection, search, arg, context } = req.body;
101
+ if(!collection || !search || !arg) return res.status(400).json({ err: true, msg: "collection & search & arg is required" });
102
+
103
+ try{
104
+ const parsedSearch = parseParam(search);
105
+ const parsedArg = parseParam(arg);
106
+ const db = req.dataCenter;
107
+ const result = await db.update(collection, parsedSearch, parsedArg, context || {});
108
+ res.json({ err: false, result });
109
+ }catch(err){
110
+ console.error(err);
111
+ res.status(500).json({ err: true, msg: err.message });
112
+ }
113
+ });
114
+
115
+ router.post("/updateOne", async (req, res) => {
116
+ const { collection, search, arg, context } = req.body;
117
+ if(!collection || !search || !arg) return res.status(400).json({ err: true, msg: "collection & search & arg is required" });
118
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
119
+
120
+ try{
121
+ const parsedSearch = parseParam(search);
122
+ const parsedArg = parseParam(arg);
123
+ const db = req.dataCenter;
124
+ const result = await db.updateOne(collection, parsedSearch, parsedArg, context || {});
125
+ res.json({ err: false, result });
126
+ }catch(err){
127
+ console.error(err);
128
+ res.status(500).json({ err: true, msg: err.message });
129
+ }
130
+ });
131
+
132
+ router.post("/remove", async (req, res) => {
133
+ const { collection, search, context } = req.body;
134
+ if(!collection || !search) return res.status(400).json({ err: true, msg: "collection & search is required" });
135
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
136
+
137
+ try{
138
+ const parsedSearch = parseParam(search);
139
+ const db = req.dataCenter;
140
+ const result = await db.remove(collection, parsedSearch, context || {});
141
+ res.json({ err: false, result });
142
+ }catch(err){
143
+ console.error(err);
144
+ res.status(500).json({ err: true, msg: err.message });
145
+ }
146
+ });
147
+
148
+ router.post("/removeOne", async (req, res) => {
149
+ const { collection, search, context } = req.body;
150
+ if(!collection || !search) return res.status(400).json({ err: true, msg: "collection & search is required" });
151
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
152
+
153
+ try{
154
+ const parsedSearch = parseParam(search);
155
+ const db = req.dataCenter;
156
+ const result = await db.removeOne(collection, parsedSearch, context || {});
157
+ res.json({ err: false, result });
158
+ }catch(err){
159
+ console.error(err);
160
+ res.status(500).json({ err: true, msg: err.message });
161
+ }
162
+ });
163
+
164
+ router.post("/updateOneOrAdd", async (req, res) => {
165
+ const { collection, search, arg, add_arg, context, id_gen } = req.body;
166
+ if(!collection || !search || !arg) return res.status(400).json({ err: true, msg: "collection & search & arg is required" });
167
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
168
+
169
+ try{
170
+ const parsedSearch = parseParam(search);
171
+ const parsedArg = parseParam(arg);
172
+ const parsedAddArg = parseParam(add_arg);
173
+ const db = req.dataCenter;
174
+ const result = await db.updateOneOrAdd(collection, parsedSearch, parsedArg, parsedAddArg || {}, context || {}, id_gen || true);
175
+ res.json({ err: false, result });
176
+ }catch(err){
177
+ console.error(err);
178
+ res.status(500).json({ err: true, msg: err.message });
179
+ }
180
+ });
181
+
182
+ router.post("/removeDb", async (req, res) => {
183
+ const { name } = req.body;
184
+ if(!name) return res.status(400).json({ err: true, msg: "name is required" });
185
+ if(!isPathSafe(baseDir, name)) return res.status(400).json({ err: true, msg: "invalid name" });
186
+
187
+ try{
188
+ const db = req.dataCenter;
189
+ const result = await db.removeDb(name);
190
+ res.json({ err: false, result });
191
+ }catch(err){
192
+ console.error(err);
193
+ res.status(500).json({ err: true, msg: err.message });
194
+ }
195
+ });
196
+
197
+ export default router;
@@ -0,0 +1,43 @@
1
+ import { createContext, runInContext } from "node:vm";
2
+
3
+ const sandbox = {
4
+ console: {
5
+ log: console.log,
6
+ },
7
+ Math,
8
+ Date,
9
+ setTimeout: undefined,
10
+ setInterval: undefined,
11
+ setImmediate: undefined,
12
+ clearTimeout: undefined,
13
+ clearInterval: undefined,
14
+ clearImmediate: undefined,
15
+ require: undefined,
16
+ module: undefined,
17
+ exports: undefined,
18
+ process: undefined,
19
+ Buffer: undefined,
20
+ };
21
+
22
+ const context = createContext(sandbox);
23
+
24
+ function changeStringToFunction(func){
25
+ try{
26
+ const userFunction = runInContext(`(${func})`, context);
27
+ return userFunction;
28
+ }catch(e){
29
+ throw new Error("Invalid function");
30
+ }
31
+ }
32
+
33
+ function parseParam(param){
34
+ if(typeof param === "string"){
35
+ const userFunction = changeStringToFunction(param);
36
+ if(typeof userFunction !== "function") throw new Error("Invalid function");
37
+ return userFunction;
38
+ }else{
39
+ return param;
40
+ }
41
+ }
42
+
43
+ export default parseParam;
@@ -0,0 +1,121 @@
1
+ import { Router } from "express";
2
+ import { isPathSafe } from "./pathUtils.js";
3
+ const router = Router();
4
+
5
+ router.use((req, res, next) => {
6
+ if(req.dbType == "graph") return next();
7
+ return res.status(400).json({ err: true, msg: "Invalid data center type." });
8
+ });
9
+
10
+ router.post("/add", async (req, res) => {
11
+ const { collection, a, b } = req.body;
12
+ if(!collection || !a || !b) return res.status(400).json({ err: true, msg: "collection & a & b is required" });
13
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
14
+
15
+ const db = req.dataCenter;
16
+ try{
17
+ const result = await db.add(collection, a, b);
18
+ res.json({ err: false, result });
19
+ }catch(err){
20
+ res.status(500).json({ err: true, msg: err.message });
21
+ }
22
+ });
23
+
24
+ router.post("/find", async (req, res) => {
25
+ const { collection, node } = req.body;
26
+ if(!collection || !node) return res.status(400).json({ err: true, msg: "collection & node is required" });
27
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
28
+
29
+ const db = req.dataCenter;
30
+ try{
31
+ const result = await db.find(collection, node);
32
+ res.json({ err: false, result });
33
+ }catch(err){
34
+ res.status(500).json({ err: true, msg: err.message });
35
+ }
36
+ });
37
+
38
+ router.post("/findOne", async (req, res) => {
39
+ const { collection, nodeA, nodeB } = req.body;
40
+ if(!collection || !nodeA || !nodeB) return res.status(400).json({ err: true, msg: "collection & nodeA & nodeB is required" });
41
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
42
+
43
+ const db = req.dataCenter;
44
+ try{
45
+ const result = await db.findOne(collection, nodeA, nodeB);
46
+ res.json({ err: false, result });
47
+ }catch(err){
48
+ res.status(500).json({ err: true, msg: err.message });
49
+ }
50
+ });
51
+
52
+ router.post("/getAll", async (req, res) => {
53
+ const { collection } = req.body;
54
+ if(!collection) return res.status(400).json({ err: true, msg: "collection is required" });
55
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
56
+
57
+ const db = req.dataCenter;
58
+ try{
59
+ const result = await db.getAll(collection);
60
+ res.json({ err: false, result });
61
+ }catch(err){
62
+ res.status(500).json({ err: true, msg: err.message });
63
+ }
64
+ });
65
+
66
+ router.post("/remove", async (req, res) => {
67
+ const { collection, nodeA, nodeB } = req.body;
68
+ if(!collection || !nodeA || !nodeB) return res.status(400).json({ err: true, msg: "collection & nodeA & nodeB is required" });
69
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
70
+
71
+ const db = req.dataCenter;
72
+ try{
73
+ const result = await db.remove(collection, nodeA, nodeB);
74
+ res.json({ err: false, result });
75
+ }catch(err){
76
+ res.status(500).json({ err: true, msg: err.message });
77
+ }
78
+ });
79
+
80
+ router.post("/getCollections", async (req, res) => {
81
+ try{
82
+ const db = req.dataCenter;
83
+ const result = await db.getCollections();
84
+ res.json({ err: false, result });
85
+ }catch(err){
86
+ console.error(err);
87
+ res.status(500).json({ err: true, msg: err.message });
88
+ }
89
+ });
90
+
91
+ router.post("/checkCollection", async (req, res) => {
92
+ const { collection } = req.body;
93
+ if(!collection) return res.status(400).json({ err: true, msg: "collection is required" });
94
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
95
+
96
+ try{
97
+ const db = req.dataCenter;
98
+ await db.checkCollection(collection);
99
+ res.json({ err: false, result: true });
100
+ }catch(err){
101
+ console.error(err);
102
+ res.status(500).json({ err: true, msg: err.message });
103
+ }
104
+ });
105
+
106
+ router.post("/issetCollection", async (req, res) => {
107
+ const { collection } = req.body;
108
+ if(!collection) return res.status(400).json({ err: true, msg: "collection is required" });
109
+ if(!isPathSafe(baseDir, collection)) return res.status(400).json({ err: true, msg: "invalid collection" });
110
+
111
+ try{
112
+ const db = req.dataCenter;
113
+ const result = await db.issetCollection(collection);
114
+ res.json({ err: false, result });
115
+ }catch(err){
116
+ console.error(err);
117
+ res.status(500).json({ err: true, msg: err.message });
118
+ }
119
+ });
120
+
121
+ export default router;
@@ -0,0 +1,130 @@
1
+ #app{
2
+ display: flex;
3
+ height: 100vh;
4
+ }
5
+
6
+ nav{
7
+ width: 200px;
8
+ max-width: 200px;
9
+ height: 100vh;
10
+ background-color: var(--panel);
11
+ }
12
+
13
+ #database-server-list > li{
14
+ margin-top: 0.5rem;
15
+ border-bottom: 1px solid var(--accent);
16
+ padding-block: 0.5rem;
17
+ }
18
+
19
+ .database-nav{
20
+ list-style: none;
21
+ text-align: left;
22
+ padding-left: 0.7rem;
23
+ }
24
+
25
+ .database-nav-meta{
26
+ list-style: square;
27
+ text-align: left;
28
+ padding-left: 1.3rem;
29
+ }
30
+
31
+ main{
32
+ flex-grow: 1;
33
+ height: 100vh;
34
+ overflow-y: scroll;
35
+ }
36
+
37
+ #query-div{
38
+ text-align: left;
39
+ padding-left: 1rem;
40
+ }
41
+
42
+ #query-div h2{
43
+ margin-bottom: 0.7rem;
44
+ }
45
+
46
+ #query-div label{
47
+ margin-bottom: 5px;
48
+ display: block;
49
+ }
50
+
51
+ input, select, button{
52
+ background-color: var(--back);
53
+ border: 1px solid var(--accent);
54
+ border-radius: 5px;
55
+ padding: 5px;
56
+ color: var(--txt);
57
+ }
58
+
59
+
60
+ #data-output h1{
61
+ text-align: center;
62
+ color: var(--txt);
63
+ }
64
+
65
+ #data-output div{
66
+ padding: 10px;
67
+ text-align: left;
68
+ border-top: 1px solid var(--border);
69
+ }
70
+
71
+ #data-output ul{
72
+ list-style-type: none;
73
+ padding: 0;
74
+ }
75
+
76
+ #data-output li{
77
+ margin: 5px 0;
78
+ }
79
+
80
+ #data-output pre{
81
+ white-space: pre-wrap;
82
+ word-wrap: break-word;
83
+ }
84
+
85
+ .selectMeta{
86
+ cursor: pointer;
87
+ }
88
+
89
+ .selectMeta:hover{
90
+ text-decoration: underline;
91
+ }
92
+
93
+ #nav__toggle{
94
+ display: none;
95
+ }
96
+
97
+ #popUpContainer > div{
98
+ display: none;
99
+ position: absolute;
100
+ top: 50%;
101
+ left: 50%;
102
+ transform: translate(-50%, -50%);
103
+ z-index: 10;
104
+ background-color: var(--back);
105
+ color: var(--txt);
106
+ border: 2px solid var(--accent);
107
+ border-radius: 1rem;
108
+ padding: 1rem;
109
+ width: 600px;
110
+ max-width: 90vw;
111
+ max-height: 90vh;
112
+ }
113
+
114
+ @media only screen and (max-width: 600px){
115
+ nav{
116
+ position: absolute;
117
+ top: 0;
118
+ transition: 0.5s;
119
+ }
120
+
121
+ #nav__toggle{
122
+ display: block;
123
+ position: absolute;
124
+ top: 0;
125
+ right: 0;
126
+ margin: 1rem;
127
+ cursor: pointer;
128
+ z-index: 99;
129
+ }
130
+ }
@@ -0,0 +1,81 @@
1
+ :root{
2
+ --scrollbar-width: 5px;
3
+ }
4
+
5
+ /* WebKit (Chromium, Chrome, Opera, Safari) */
6
+ ::-webkit-scrollbar{
7
+ width: var(--scrollbar-width);
8
+ }
9
+
10
+ ::-webkit-scrollbar-track{
11
+ background: var(--back);
12
+ }
13
+
14
+ ::-webkit-scrollbar-thumb{
15
+ background: var(--accent);
16
+ }
17
+
18
+ ::-webkit-scrollbar-thumb:hover{
19
+ background: var(--border);
20
+ }
21
+
22
+ /* Gecko (Firefox) */
23
+ *{
24
+ scrollbar-width: thin;
25
+ scrollbar-color: var(--accent) var(--back);
26
+ }
27
+
28
+ *::-webkit-scrollbar{
29
+ width: var(--scrollbar-width);
30
+ }
31
+
32
+ *::-webkit-scrollbar-track{
33
+ background: var(--back);
34
+ }
35
+
36
+ *::-webkit-scrollbar-thumb{
37
+ background-color: var(--accent);
38
+ }
39
+
40
+ *::-webkit-scrollbar-thumb:hover{
41
+ background-color: var(--border);
42
+ }
43
+
44
+ /* Electron */
45
+ html::-webkit-scrollbar{
46
+ width: var(--scrollbar-width);
47
+ }
48
+
49
+ html::-webkit-scrollbar-track{
50
+ background: var(--back);
51
+ }
52
+
53
+ html::-webkit-scrollbar-thumb{
54
+ background: var(--accent);
55
+ }
56
+
57
+ html::-webkit-scrollbar-thumb:hover{
58
+ background: var(--border);
59
+ }
60
+
61
+ /* WebView in React Native */
62
+ body{
63
+ scrollbar-width: var(--scrollbar-width);
64
+ scrollbar-color: var(--accent) var(--back);
65
+ }
66
+
67
+ body::-webkit-scrollbar{
68
+ width: var(--scrollbar-width);
69
+ }
70
+
71
+ body::-webkit-scrollbar-track{
72
+ background: var(--back);
73
+ }
74
+
75
+ body::-webkit-scrollbar-thumb{
76
+ background-color: var(--accent);
77
+ }
78
+
79
+ body::-webkit-scrollbar-thumb:hover{
80
+ background-color: var(--border);
81
+ }