mbkbucket 3.8.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.env.example +15 -14
  2. package/README.md +561 -377
  3. package/index.js +32 -30
  4. package/lib/cli.js +154 -565
  5. package/lib/public/bucketadmin.css +2042 -1708
  6. package/lib/public/bucketadmin.js +1622 -1286
  7. package/lib/public/mbkbucket-helper-demo.html +4 -24
  8. package/lib/public/mbkbucket-helper.css +28 -34
  9. package/lib/public/mbkbucket-helper.js +4 -3
  10. package/lib/src/config/validation.js +86 -73
  11. package/lib/src/controllers/api.controller.js +459 -410
  12. package/lib/src/controllers/dashboard.controller.js +69 -49
  13. package/lib/src/controllers/info.controller.js +34 -3
  14. package/lib/src/controllers/view.controller.js +233 -231
  15. package/lib/src/core/storage/errors.js +62 -0
  16. package/lib/src/core/storage/gdrive-auth.js +193 -0
  17. package/lib/src/core/storage/gdrive-provider.js +656 -0
  18. package/lib/src/core/storage/index.js +14 -0
  19. package/lib/src/core/storage/models.js +80 -0
  20. package/lib/src/core/storage/path-strategy.js +65 -0
  21. package/lib/src/core/storage/s3-provider.js +491 -0
  22. package/lib/src/core/storage/storage-manager.js +203 -0
  23. package/lib/src/core/storage/storage-provider.js +217 -0
  24. package/lib/src/middleware/bucket-resolver.js +37 -37
  25. package/lib/src/routes/api.routes.js +5 -1
  26. package/lib/src/routes/index.js +60 -60
  27. package/lib/src/routes/view.routes.js +28 -25
  28. package/lib/src/services/s3.service.js +42 -594
  29. package/lib/src/services/storage.service.js +213 -0
  30. package/lib/src/utils/errors.js +56 -46
  31. package/package.json +11 -12
  32. package/views/bucket.hbs +47 -71
  33. package/views/bucketadmin_move_modal.hbs +35 -0
  34. package/views/bucketadmin_preview_modal.hbs +108 -40
  35. package/views/bucketadmincontent.hbs +193 -144
  36. package/views/mbkbucket_info.hbs +134 -232
  37. package/.mbkbucket.config.example.json +0 -14
  38. package/lib/src/cli/auth-helper.js +0 -141
@@ -0,0 +1,213 @@
1
+ /**
2
+ * MBKBucket - Provider-Independent Storage Service
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import dotenv from "dotenv";
8
+ import { storageManager } from "../core/storage/storage-manager.js";
9
+ import { PathStrategy } from "../core/storage/path-strategy.js";
10
+ import { StorageValidationError } from "../core/storage/errors.js";
11
+ import { createLogger } from "#logger";
12
+ import { trimSlashes } from "#helpers";
13
+
14
+ dotenv.config();
15
+ const debugStorage = createLogger('storage-service');
16
+
17
+ export const getAvailableStorageNames = () => storageManager.getAvailableConnectionNames();
18
+ export const getAvailableBucketNames = getAvailableStorageNames;
19
+
20
+ export function resolveStorageName(name) {
21
+ return storageManager.resolveConnectionName(name);
22
+ }
23
+ export const resolveBucketName = resolveStorageName;
24
+
25
+ export const getAppName = () => PathStrategy.getAppName();
26
+ export const validateKeySafety = (str, isKey = false) => PathStrategy.validateSafety(str, isKey);
27
+ export const ensureKeyHasAppPrefix = (key = '') => PathStrategy.applyKeyPrefix(key);
28
+ export const ensurePrefix = (prefix = '') => PathStrategy.applyPrefix(prefix);
29
+
30
+ export function getStorageProvider(connectionName) {
31
+ return storageManager.getProvider(connectionName);
32
+ }
33
+
34
+ export function getStorageConfig(connectionName) {
35
+ return storageManager.getConnectionConfig(connectionName);
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Health Checks
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export async function checkHealth(connectionName) {
43
+ return storageManager.checkHealth(connectionName);
44
+ }
45
+
46
+ export async function runHealthCheck(connectionName) {
47
+ try {
48
+ const health = await checkHealth(connectionName);
49
+ if (health.status === 'healthy') {
50
+ debugStorage('Connected to storage: %s (%sms)', health.bucket || health.name || 'default', health.responseTime);
51
+ } else {
52
+ console.error('[mbkbucket] Storage connection failed:', health.error);
53
+ }
54
+ return health;
55
+ } catch (err) {
56
+ console.error("[mbkbucket] Storage connection test error:", err.message);
57
+ }
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Unified Storage Operations
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
65
+ if (!key || !fileBuffer) throw new StorageValidationError('Key and file buffer are required');
66
+ const keyToUpload = ensureKeyHasAppPrefix(key);
67
+ const provider = getStorageProvider(options.bucketName || options.connectionName);
68
+ return provider.uploadFile(keyToUpload, fileBuffer, contentType, options);
69
+ }
70
+
71
+ export async function downloadFile(key, options = {}) {
72
+ if (!key) throw new StorageValidationError('Key is required');
73
+ const keyToDownload = ensureKeyHasAppPrefix(key);
74
+ const provider = getStorageProvider(options.bucketName || options.connectionName);
75
+ return provider.downloadFile(keyToDownload, options);
76
+ }
77
+
78
+ export async function deleteFile(key, connectionName) {
79
+ if (!key) throw new StorageValidationError('Key is required');
80
+ const keyToDelete = ensureKeyHasAppPrefix(key);
81
+ const provider = getStorageProvider(connectionName);
82
+ return provider.deleteFile(keyToDelete);
83
+ }
84
+
85
+ export async function deleteFiles(keys, connectionName) {
86
+ if (!keys || !Array.isArray(keys) || !keys.length) {
87
+ throw new StorageValidationError('Keys array is required and must not be empty');
88
+ }
89
+ const keysToDelete = keys.map(k => ensureKeyHasAppPrefix(k));
90
+ const provider = getStorageProvider(connectionName);
91
+ return provider.deleteFiles(keysToDelete);
92
+ }
93
+
94
+ export async function deleteFolder(prefix, connectionName) {
95
+ if (!prefix) throw new StorageValidationError('Prefix is required');
96
+ const effectivePrefix = `${ensurePrefix(trimSlashes(prefix))}/`;
97
+ const provider = getStorageProvider(connectionName);
98
+ return provider.deleteFolder(effectivePrefix);
99
+ }
100
+
101
+ export async function createFolder(prefix, connectionName, options = {}) {
102
+ const effectivePrefix = ensurePrefix(prefix);
103
+ const provider = getStorageProvider(connectionName);
104
+ return provider.createFolder(effectivePrefix, options);
105
+ }
106
+
107
+ export async function listfiles(prefix = '', options = {}) {
108
+ const effectivePrefix = ensurePrefix(prefix);
109
+ const provider = getStorageProvider(options.bucketName || options.connectionName);
110
+ return provider.listFiles(effectivePrefix, options);
111
+ }
112
+
113
+ export async function getFileMetadata(key, connectionName) {
114
+ if (!key) throw new StorageValidationError('Key is required');
115
+ const keyToCheck = ensureKeyHasAppPrefix(key);
116
+ const provider = getStorageProvider(connectionName);
117
+ return provider.getFileMetadata(keyToCheck);
118
+ }
119
+
120
+ export async function fileExists(key, connectionName) {
121
+ try {
122
+ const meta = await getFileMetadata(key, connectionName);
123
+ return Boolean(meta?.exists);
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ export async function getFileSize(key, connectionName) {
130
+ try {
131
+ const meta = await getFileMetadata(key, connectionName);
132
+ return meta?.exists ? (meta.ContentLength ?? null) : null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, connectionName) {
139
+ const keyToUse = ensureKeyHasAppPrefix(key);
140
+ const provider = getStorageProvider(connectionName);
141
+ return provider.generateSignedUrl(keyToUse, operation, expiresIn);
142
+ }
143
+
144
+ export async function copyFile(sourceKey, destKey, options = {}, connectionName) {
145
+ const src = ensureKeyHasAppPrefix(sourceKey);
146
+ const dst = ensureKeyHasAppPrefix(destKey);
147
+ const provider = getStorageProvider(connectionName);
148
+ return provider.copyFile(src, dst, options);
149
+ }
150
+
151
+ export async function moveFile(sourceKey, destKey, options = {}, connectionName) {
152
+ const src = ensureKeyHasAppPrefix(sourceKey);
153
+ const dst = ensureKeyHasAppPrefix(destKey);
154
+ const provider = getStorageProvider(connectionName);
155
+ return provider.moveFile(src, dst, options);
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Multipart Upload Helpers (For S3 / compatible providers)
160
+ // ---------------------------------------------------------------------------
161
+
162
+ export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}, connectionName) {
163
+ const keyToUse = ensureKeyHasAppPrefix(key);
164
+ const provider = getStorageProvider(connectionName);
165
+ if (typeof provider.createMultipartUpload !== 'function') {
166
+ throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
167
+ }
168
+ return provider.createMultipartUpload(keyToUse, contentType, metadata);
169
+ }
170
+
171
+ export async function uploadPart(key, uploadId, partNumber, buffer, connectionName) {
172
+ const keyToUse = ensureKeyHasAppPrefix(key);
173
+ const provider = getStorageProvider(connectionName);
174
+ if (typeof provider.uploadPart !== 'function') {
175
+ throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
176
+ }
177
+ return provider.uploadPart(keyToUse, uploadId, partNumber, buffer);
178
+ }
179
+
180
+ export async function completeMultipartUpload(key, uploadId, parts, connectionName) {
181
+ const keyToUse = ensureKeyHasAppPrefix(key);
182
+ const provider = getStorageProvider(connectionName);
183
+ if (typeof provider.completeMultipartUpload !== 'function') {
184
+ throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
185
+ }
186
+ return provider.completeMultipartUpload(keyToUse, uploadId, parts);
187
+ }
188
+
189
+ export async function abortMultipartUpload(key, uploadId, connectionName) {
190
+ const keyToUse = ensureKeyHasAppPrefix(key);
191
+ const provider = getStorageProvider(connectionName);
192
+ if (typeof provider.abortMultipartUpload !== 'function') {
193
+ throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
194
+ }
195
+ return provider.abortMultipartUpload(keyToUse, uploadId);
196
+ }
197
+
198
+ export async function listIncompleteMultipartUploads(prefix = '', connectionName) {
199
+ const effectivePrefix = ensurePrefix(prefix || '');
200
+ const provider = getStorageProvider(connectionName);
201
+ if (typeof provider.listIncompleteMultipartUploads !== 'function') {
202
+ return [];
203
+ }
204
+ return provider.listIncompleteMultipartUploads(effectivePrefix);
205
+ }
206
+
207
+ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefix = '', connectionName) {
208
+ const provider = getStorageProvider(connectionName);
209
+ if (typeof provider.cleanupIncompleteMultipartUploads !== 'function') {
210
+ return { abortedCount: 0, uploads: [] };
211
+ }
212
+ return provider.cleanupIncompleteMultipartUploads(olderThanDays, prefix);
213
+ }
@@ -1,46 +1,56 @@
1
- /**
2
- * Centralized error classification for API and View routes.
3
- * Maps error messages to HTTP status codes and machine-readable error codes.
4
- */
5
-
6
- const API_ERROR_RULES = [
7
- { match: 'bucketconnection environment variable is not set', status: 503, code: 'BUCKET_CONFIG_ERROR' },
8
- { match: ['no bucket selected', 'not found in bucketconnection'], status: 400, code: 'INVALID_BUCKET' },
9
- { match: 'already exists', status: 409, code: 'CONFLICT' },
10
- { match: 'not found', status: 404, code: 'NOT_FOUND' },
11
- { match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
12
- { match: ['required', 'invalid', 'must be'], status: 400, code: 'VALIDATION_ERROR' },
13
- ];
14
-
15
- const VIEW_ERROR_RULES = [
16
- { match: ['no bucket selected', 'not found in bucketconnection'], status: 400, code: 'INVALID_BUCKET' },
17
- { match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
18
- { match: 'not found', status: 404, code: 'NOT_FOUND' },
19
- { match: ['invalid', 'required'], status: 400, code: 'VALIDATION_ERROR' },
20
- ];
21
-
22
- function matchError(rules, error, fallbackMessage) {
23
- const message = String(error?.message || fallbackMessage);
24
- const lower = message.toLowerCase();
25
- const found = rules.find(r => Array.isArray(r.match) ? r.match.some(m => lower.includes(m)) : lower.includes(r.match));
26
- return { status: found?.status ?? 500, code: found?.code ?? 'INTERNAL_ERROR', message };
27
- }
28
-
29
- export const classifyApiError = (error, fallbackMessage = 'Request failed') => matchError(API_ERROR_RULES, error, fallbackMessage);
30
- export const classifyViewError = (error, fallbackMessage = 'Request failed') => matchError(VIEW_ERROR_RULES, error, fallbackMessage);
31
-
32
- export function sendApiError(res, error, fallbackMessage = 'Request failed') {
33
- const { status, code, message } = classifyApiError(error, fallbackMessage);
34
- return res.status(status).json({
35
- success: false,
36
- code,
37
- error: { code, message },
38
- message,
39
- timestamp: new Date().toISOString(),
40
- });
41
- }
42
-
43
- export function sendViewError(res, error, fallbackMessage = 'Request failed') {
44
- const { status, code, message } = classifyViewError(error, fallbackMessage);
45
- return res.status(status).json({ code, message });
46
- }
1
+ /**
2
+ * Centralized error classification for API and View routes.
3
+ * Maps typed StorageErrors and error messages to HTTP status codes and machine-readable error codes.
4
+ */
5
+
6
+ import { StorageError } from "../core/storage/errors.js";
7
+
8
+ const API_ERROR_RULES = [
9
+ { match: 'bucketconnection environment variable is not set', status: 503, code: 'BUCKET_CONFIG_ERROR' },
10
+ { match: ['no bucket selected', 'not found in bucketconnection', 'no storage connection'], status: 400, code: 'INVALID_BUCKET' },
11
+ { match: 'already exists', status: 409, code: 'CONFLICT' },
12
+ { match: 'not found', status: 404, code: 'NOT_FOUND' },
13
+ { match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
14
+ { match: ['required', 'invalid', 'must be', 'path traversal'], status: 400, code: 'VALIDATION_ERROR' },
15
+ ];
16
+
17
+ const VIEW_ERROR_RULES = [
18
+ { match: ['no bucket selected', 'not found in bucketconnection', 'no storage connection'], status: 400, code: 'INVALID_BUCKET' },
19
+ { match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
20
+ { match: 'not found', status: 404, code: 'NOT_FOUND' },
21
+ { match: ['invalid', 'required', 'path traversal'], status: 400, code: 'VALIDATION_ERROR' },
22
+ ];
23
+
24
+ function matchError(rules, error, fallbackMessage) {
25
+ if (error instanceof StorageError) {
26
+ return {
27
+ status: error.status,
28
+ code: error.code,
29
+ message: error.message || fallbackMessage
30
+ };
31
+ }
32
+
33
+ const message = String(error?.message || fallbackMessage);
34
+ const lower = message.toLowerCase();
35
+ const found = rules.find(r => Array.isArray(r.match) ? r.match.some(m => lower.includes(m)) : lower.includes(r.match));
36
+ return { status: found?.status ?? 500, code: found?.code ?? 'INTERNAL_ERROR', message };
37
+ }
38
+
39
+ export const classifyApiError = (error, fallbackMessage = 'Request failed') => matchError(API_ERROR_RULES, error, fallbackMessage);
40
+ export const classifyViewError = (error, fallbackMessage = 'Request failed') => matchError(VIEW_ERROR_RULES, error, fallbackMessage);
41
+
42
+ export function sendApiError(res, error, fallbackMessage = 'Request failed') {
43
+ const { status, code, message } = classifyApiError(error, fallbackMessage);
44
+ return res.status(status).json({
45
+ success: false,
46
+ code,
47
+ error: { code, message },
48
+ message,
49
+ timestamp: new Date().toISOString(),
50
+ });
51
+ }
52
+
53
+ export function sendViewError(res, error, fallbackMessage = 'Request failed') {
54
+ const { status, code, message } = classifyViewError(error, fallbackMessage);
55
+ return res.status(status).json({ code, message });
56
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mbkbucket",
3
- "version": "3.8.2",
4
- "description": "Flexible S3/R2 bucket management — library, Express router, and CLI for mbktech.org applications. Provides upload, download, listing, multipart utilities, signed URLs, and a command-line interface with built-in configuration validation and health checks",
3
+ "version": "4.0.0",
4
+ "description": "Flexible S3/R2 bucket management — library, Express router, and standalone server CLI for mbktech.org applications. Provides upload, download, listing, multipart utilities, signed URLs, and a standalone server runner with built-in configuration validation and health checks",
5
5
  "author": "Muhammad Bin Khalid",
6
6
  "homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
7
7
  "license": "MIT",
@@ -12,6 +12,7 @@
12
12
  "mbkbucket": "./lib/cli.js"
13
13
  },
14
14
  "scripts": {
15
+ "start": "node lib/cli.js",
15
16
  "dev": "cross-env NODE_ENV=dev nodemon index.js",
16
17
  "publish": "node scripts/publish.js",
17
18
  "test": "vitest run"
@@ -23,22 +24,22 @@
23
24
  "dependencies": {
24
25
  "@aws-sdk/client-s3": "^3.1127.0",
25
26
  "@aws-sdk/s3-request-presigner": "^3.1127.0",
26
- "compression": "^1.8.0",
27
+ "compression": "^1.8.2",
27
28
  "debug": "^4.4.3",
28
- "dotenv": "^16.6.1",
29
- "express": "^5.1.0",
29
+ "dotenv": "^18.0.5",
30
+ "express": "^5.2.1",
30
31
  "express-handlebars": "^9.0.1",
31
32
  "express-rate-limit": "^7.5.1",
32
- "mbkauthe": "6.2.1",
33
- "multer": "^2.0.2",
34
- "pg": "^8.16.3"
33
+ "mbkauthe": "^6.2.3",
34
+ "multer": "^2.4.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/supertest": "^7.2.1",
38
38
  "cross-env": "^7.0.3",
39
39
  "nodemon": "^3.1.14",
40
- "supertest": "^7.2.2",
41
- "vitest": "^4.1.11"
40
+ "supertest": "^7.3.1",
41
+ "vite": "^8.3.2",
42
+ "vitest": "5.0.3"
42
43
  },
43
44
  "overrides": {
44
45
  "csurf": {
@@ -56,8 +57,6 @@
56
57
  "lib",
57
58
  "views",
58
59
  ".env.example",
59
- ".mbkbucket.config.example.json",
60
- "cli.js",
61
60
  "index.js",
62
61
  "LICENSE",
63
62
  "package.json",
package/views/bucket.hbs CHANGED
@@ -22,86 +22,63 @@
22
22
  <style>
23
23
  :root {
24
24
  --primary-color-ml: #0f766e;
25
- --primary-light-color-ml: #d7f4ee;
26
- --primary-dark-color-ml: #0b4f4a;
27
- --secondary-color-ml: #1f8f84;
28
- --dark-color-ml: #1f2a2a;
29
- --light-color-ml: #f4f7f3;
30
- --gray-color-ml: #667c79;
31
- --gray-light-color-ml: #dce8e3;
32
- --danger-color-ml: #dc2626;
33
- --warning-color-ml: #d97706;
34
- --success-color-ml: #16a34a;
35
- --info-color-ml: #3b82f6;
25
+ --primary-light-color-ml: #f0fdfa;
26
+ --primary-dark-color-ml: #115e59;
27
+ --secondary-color-ml: #14b8a6;
28
+ --dark-color-ml: #0f172a;
29
+ --light-color-ml: #f8fafc;
30
+ --gray-color-ml: #64748b;
31
+ --gray-light-color-ml: #e2e8f0;
32
+ --danger-color-ml: #ef4444;
33
+ --warning-color-ml: #f59e0b;
34
+ --success-color-ml: #10b981;
35
+ --info-color-ml: #0284c7;
36
36
 
37
37
  --primary: var(--primary-color-ml);
38
38
  --primary-lt: var(--primary-light-color-ml);
39
39
  --primary-dk: var(--primary-dark-color-ml);
40
- --primary-hover: #0d635c;
40
+ --primary-hover: #134e4a;
41
41
 
42
42
  --body-bg: var(--light-color-ml);
43
- --bg-surface: rgba(255, 255, 255, 0.88);
44
- --bg-surface-secondary: rgba(244, 247, 243, 0.7);
45
- --bg-surface-tertiary: #e9f2ee;
43
+ --bg-surface: #ffffff;
44
+ --bg-surface-secondary: #f8fafc;
45
+ --bg-surface-tertiary: #f1f5f9;
46
46
 
47
- --border-color: rgba(15, 118, 110, 0.15);
48
- --border-color-light: rgba(15, 118, 110, 0.08);
47
+ --border-color: #e2e8f0;
48
+ --border-color-light: #f1f5f9;
49
49
 
50
50
  --text: var(--dark-color-ml);
51
- --text-secondary: #4a5c5a;
51
+ --text-secondary: #475569;
52
52
  --text-muted: var(--gray-color-ml);
53
- --text-disabled: #a0b2af;
53
+ --text-disabled: #94a3b8;
54
54
 
55
- --font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
56
- --font-family-headings: "Sora", "Segoe UI", sans-serif;
57
- --font-mono: 'JetBrains Mono', monospace;
55
+ --font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
56
+ --font-family-headings: "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
57
+ --font-mono: "Segoe UI Mono", "Cascadia Code", Consolas, "Courier New", monospace;
58
58
  }
59
59
 
60
60
  * {
61
61
  margin: 0;
62
62
  padding: 0;
63
63
  box-sizing: border-box;
64
+ font-family: inherit;
65
+ }
66
+
67
+ body, button, input, select, textarea {
68
+ font-family: var(--font-family);
64
69
  }
65
70
 
66
71
  body {
67
72
  font-family: var(--font-family);
68
- background:
69
- radial-gradient(circle at 12% 14%, rgba(16, 185, 129, 0.12), transparent 44%),
70
- radial-gradient(circle at 86% 8%, rgba(244, 114, 38, 0.12), transparent 40%),
71
- linear-gradient(160deg, #eef7f2 0, #f8fbf7 52%, #eaf2ef 100%);
72
- color: var(--dark-color-ml);
73
+ background: var(--body-bg);
74
+ color: var(--text);
73
75
  display: flex;
74
76
  min-height: 100vh;
75
77
  overflow-x: hidden;
76
- line-height: 1.6;
78
+ line-height: 1.5;
77
79
  font-size: 14px;
78
- position: relative;
79
- isolation: isolate;
80
- }
81
-
82
- body::before,
83
- body::after {
84
- content: "";
85
- position: fixed;
86
- border-radius: 999px;
87
- pointer-events: none;
88
- z-index: -1;
89
- }
90
-
91
- body::before {
92
- width: 260px;
93
- height: 260px;
94
- left: -80px;
95
- top: 18vh;
96
- background: radial-gradient(circle, rgba(15, 118, 110, 0.16) 0, rgba(15, 118, 110, 0) 70%);
97
- }
98
-
99
- body::after {
100
- width: 300px;
101
- height: 300px;
102
- right: -120px;
103
- bottom: 10vh;
104
- background: radial-gradient(circle, rgba(251, 146, 60, 0.18) 0, rgba(251, 146, 60, 0) 72%);
80
+ -webkit-font-smoothing: antialiased;
81
+ -moz-osx-font-smoothing: grayscale;
105
82
  }
106
83
 
107
84
  .main-content {
@@ -113,33 +90,32 @@
113
90
  }
114
91
 
115
92
  .top-nav {
116
- background: rgba(255, 255, 255, .85);
117
- backdrop-filter: blur(10px);
118
- padding: .75rem 1.5rem;
93
+ background: #ffffff;
94
+ padding: 0.75rem 1.5rem;
119
95
  display: flex;
120
96
  align-items: center;
121
97
  justify-content: space-between;
122
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.06), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
98
+ border-bottom: 1px solid var(--border-color);
123
99
  z-index: 90;
124
- border-bottom: 1px solid rgba(255, 255, 255, .75);
125
100
  gap: 1rem;
126
101
  }
127
102
 
128
103
  .logo {
129
104
  font-family: var(--font-family-headings);
130
- font-size: 1.5rem;
105
+ font-size: 1.25rem;
131
106
  font-weight: 700;
132
- color: #0b4f4a;
107
+ color: var(--primary-dk);
133
108
  display: flex;
134
109
  align-items: center;
135
- gap: .5rem;
110
+ gap: 0.6rem;
136
111
  cursor: pointer;
137
112
  text-decoration: none;
113
+ letter-spacing: -0.01em;
138
114
  }
139
115
 
140
116
  .logo img {
141
- width: 32px;
142
- height: 32px;
117
+ width: 28px;
118
+ height: 28px;
143
119
  }
144
120
 
145
121
  .content-area {
@@ -149,7 +125,7 @@
149
125
  }
150
126
 
151
127
  .content-area::-webkit-scrollbar {
152
- width: 8px;
128
+ width: 6px;
153
129
  }
154
130
 
155
131
  .content-area::-webkit-scrollbar-track {
@@ -157,20 +133,20 @@
157
133
  }
158
134
 
159
135
  .content-area::-webkit-scrollbar-thumb {
160
- background: var(--gray-color-ml);
161
- border-radius: 4px;
136
+ background: #cbd5e1;
137
+ border-radius: 3px;
162
138
  }
163
139
 
164
140
  .content-area::-webkit-scrollbar-thumb:hover {
165
- background: var(--primary-color-ml);
141
+ background: #94a3b8;
166
142
  }
167
143
 
168
144
  @media (max-width: 768px) {
169
145
  .top-nav {
170
- padding: .5rem 1rem;
146
+ padding: 0.6rem 1rem;
171
147
  }
172
148
  .content-area {
173
- padding: 0.85rem;
149
+ padding: 0.75rem;
174
150
  }
175
151
  }
176
152
  </style>
@@ -0,0 +1,35 @@
1
+ <!-- Move / Rename Modal -->
2
+ <div class="modal" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
3
+ <div class="modal-dialog modal-dialog-centered">
4
+ <div class="modal-content">
5
+ <div class="modal-header">
6
+ <h5 class="modal-title" id="moveModalLabel">
7
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="modal-title-icon"><path d="M11 2L14 5L11 8M14 5H5C3.34315 5 2 6.34315 2 8V14"/></svg>
8
+ <span id="moveModalTitle">Move or Rename</span>
9
+ </h5>
10
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="closeMoveModal()">&times;</button>
11
+ </div>
12
+ <div class="modal-body">
13
+ <form id="moveForm" onsubmit="event.preventDefault(); submitMoveItem();">
14
+ <div class="modal-form-row mb-3">
15
+ <label class="form-label text-muted small">Current Path</label>
16
+ <div class="p-2 rounded text-truncate font-mono small" id="moveSourcePathPreview" style="background: var(--bg-surface-secondary); border: 1px solid var(--border-color); color: var(--dark-color-ml);"></div>
17
+ <input type="hidden" id="moveSourceKeyInput">
18
+ <input type="hidden" id="moveIsCopyInput" value="false">
19
+ </div>
20
+ <div class="modal-form-row mb-3">
21
+ <label for="moveDestNameInput" class="form-label" id="moveDestNameLabel">New Name or Path</label>
22
+ <input type="text" id="moveDestNameInput" class="form-control" autocomplete="off" required>
23
+ <div class="form-text" id="moveHelpText">Enter the new file name or folder destination path.</div>
24
+ </div>
25
+ </form>
26
+ </div>
27
+ <div class="modal-footer">
28
+ <button type="button" class="btn btn-outline" onclick="closeMoveModal()">Cancel</button>
29
+ <button type="button" class="btn btn-primary" id="submitMoveBtn" onclick="submitMoveItem()">
30
+ <span id="moveSubmitBtnText">Save Changes</span>
31
+ </button>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>