webspresso 0.0.107 β†’ 0.0.109

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -3
  3. package/bin/commands/add-esbuild.js +7 -3
  4. package/bin/commands/add-tailwind.js +9 -5
  5. package/bin/commands/build.js +20 -4
  6. package/bin/commands/dev.js +57 -7
  7. package/bin/commands/doctor.js +42 -1
  8. package/bin/commands/favicon-generate.js +10 -3
  9. package/bin/commands/new.js +8 -3
  10. package/bin/commands/start.js +3 -1
  11. package/bin/utils/add-command.js +16 -0
  12. package/core/app.js +52 -8
  13. package/core/applySchema.js +3 -72
  14. package/core/auth/authenticate.js +3 -3
  15. package/core/auth/hash.js +167 -43
  16. package/core/auth/index.js +13 -1
  17. package/core/auth/jwt.js +21 -2
  18. package/core/auth/manager.js +63 -14
  19. package/core/auth/middleware.js +4 -2
  20. package/core/auth/oauth.js +5 -1
  21. package/core/auth/tokens.js +7 -2
  22. package/core/auth/totp.js +42 -6
  23. package/core/auth/user-agent.js +2 -0
  24. package/core/cache/lru.js +10 -0
  25. package/core/cache/tagged-lru.js +4 -0
  26. package/core/client/adapter/datastar.js +1254 -0
  27. package/core/client/api.js +1033 -0
  28. package/core/client/controller.js +1081 -0
  29. package/core/client/debug.js +367 -0
  30. package/core/client/forms.js +1021 -0
  31. package/core/client/fragments.js +738 -0
  32. package/core/client/index.d.ts +40 -0
  33. package/core/client/index.js +347 -0
  34. package/core/client/lite.js +164 -0
  35. package/core/client/server-fragments.js +859 -0
  36. package/core/client/utils.js +144 -0
  37. package/core/compileSchema.js +3 -68
  38. package/core/compression/compressible.js +53 -6
  39. package/core/compression/middleware.js +277 -26
  40. package/core/compression/negotiate.js +31 -12
  41. package/core/context/app-context.js +223 -0
  42. package/core/context/index.js +55 -0
  43. package/core/context/request.js +269 -0
  44. package/core/errors/middleware.js +14 -0
  45. package/core/events/dispatcher.js +177 -15
  46. package/core/events/signal.js +9 -0
  47. package/core/forms/flash.js +136 -102
  48. package/core/health/middleware.js +36 -6
  49. package/core/health/probes.js +12 -2
  50. package/core/http/index.js +10 -1
  51. package/core/http/pipeline.js +558 -368
  52. package/core/index.js +45 -8
  53. package/core/kernel/view.js +41 -10
  54. package/core/metrics/middleware.js +30 -6
  55. package/core/metrics/registry.js +13 -5
  56. package/core/orm/cache/fingerprint.js +32 -11
  57. package/core/orm/cache/layer.js +19 -2
  58. package/core/orm/eager-loader.js +90 -55
  59. package/core/orm/hidden.js +145 -0
  60. package/core/orm/index.js +3 -39
  61. package/core/orm/model.js +54 -3
  62. package/core/orm/query-builder.js +343 -6
  63. package/core/orm/query-filters.js +14 -2
  64. package/core/orm/repository.js +64 -13
  65. package/core/orm/schema-helpers.js +68 -6
  66. package/core/orm/transaction.js +157 -18
  67. package/core/orm/types.js +4 -0
  68. package/core/plugins/index.js +8 -0
  69. package/core/plugins/plugin-manager.js +1098 -0
  70. package/core/queue/adapters/database.js +184 -2
  71. package/core/queue/adapters/memory.js +153 -5
  72. package/core/queue/adapters/redis.js +59 -4
  73. package/core/queue/job.js +14 -1
  74. package/core/queue/manager.js +170 -27
  75. package/core/rate-limit/stores/memory.js +23 -10
  76. package/core/resilience/circuit-breaker.js +61 -22
  77. package/core/schema-cache.js +2 -64
  78. package/core/services/builtins/auth.js +273 -0
  79. package/core/services/builtins/exchange.js +396 -0
  80. package/core/services/builtins/file-manager.js +214 -0
  81. package/core/services/builtins/mail.js +161 -0
  82. package/core/services/builtins/media.js +196 -0
  83. package/core/services/builtins/queue.js +144 -0
  84. package/core/services/builtins/security.js +118 -0
  85. package/core/services/builtins/system.js +219 -0
  86. package/core/services/builtins/webhook.js +110 -0
  87. package/core/services/discovery.js +133 -0
  88. package/core/services/executor.js +651 -0
  89. package/core/services/index.js +113 -5
  90. package/core/services/loader.js +161 -0
  91. package/core/services/memoize.js +408 -0
  92. package/core/services/registry.js +731 -0
  93. package/core/services/validator.js +424 -0
  94. package/core/shutdown/shutdown-manager.js +146 -85
  95. package/core/sse/stream.js +28 -5
  96. package/core/ssr/stream.js +109 -16
  97. package/core/stream/text-stream.js +45 -4
  98. package/core/utils/index.js +110 -0
  99. package/core/utils/timeout.js +12 -1
  100. package/core/validation/apply.js +74 -0
  101. package/core/validation/cache.js +66 -0
  102. package/core/validation/compiler.js +80 -0
  103. package/core/validation/index.js +22 -1
  104. package/index.d.ts +1201 -72
  105. package/index.js +188 -28
  106. package/package.json +75 -26
  107. package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
  108. package/plugins/admin-panel/index.js +19 -8
  109. package/plugins/csrf/index.js +3 -3
  110. package/plugins/data-exchange/export.js +214 -0
  111. package/plugins/data-exchange/import.js +48 -5
  112. package/plugins/data-exchange/index.js +70 -28
  113. package/plugins/data-exchange/preview.js +266 -0
  114. package/plugins/data-exchange/template.js +242 -0
  115. package/plugins/email/admin-component.js +783 -141
  116. package/plugins/email/api-handlers.js +51 -1
  117. package/plugins/email/index.js +5 -0
  118. package/plugins/email/service.js +48 -0
  119. package/plugins/file-manager/admin-component.js +1546 -70
  120. package/plugins/file-manager/api-handlers.js +166 -1
  121. package/plugins/file-manager/core.js +598 -8
  122. package/plugins/file-manager/index.js +8 -0
  123. package/plugins/index.js +7 -0
  124. package/plugins/mcp/auth.js +3 -3
  125. package/plugins/oauth/index.js +36 -3
  126. package/plugins/queue/admin-component.js +1092 -0
  127. package/plugins/queue/api-handlers.js +248 -0
  128. package/plugins/queue/index.js +69 -0
  129. package/plugins/rest-resources/index.js +7 -2
  130. package/plugins/security/admin-component.js +689 -0
  131. package/plugins/security/api-handlers.js +169 -0
  132. package/plugins/security/audit.js +153 -0
  133. package/plugins/security/bruteforce.js +244 -0
  134. package/plugins/security/firewall.js +459 -0
  135. package/plugins/security/headers.js +214 -0
  136. package/plugins/security/honeypot.js +234 -0
  137. package/plugins/security/incident-recorder.js +137 -0
  138. package/plugins/security/index.js +211 -0
  139. package/plugins/seo-checker/analyzer.js +20 -7
  140. package/plugins/upload/index.js +36 -0
  141. package/plugins/webhooks/admin-component.js +702 -175
  142. package/plugins/webhooks/api-handlers.js +75 -1
  143. package/plugins/webhooks/dispatcher.js +290 -11
  144. package/plugins/webhooks/index.js +37 -2
  145. package/plugins/webhooks/sender.js +56 -0
  146. package/plugins/xlsx/engine.js +19 -4
  147. package/src/api/api-loader.js +19 -1
  148. package/src/app-context.js +4 -196
  149. package/src/bundle/index.js +443 -17
  150. package/src/client-runtime/mount.js +9 -2
  151. package/src/dev/api-typegen.js +1300 -0
  152. package/src/dev/banner.js +10 -1
  153. package/src/dev/colors.js +1 -1
  154. package/src/dev/index.js +7 -1
  155. package/src/dev/typegen.js +174 -18
  156. package/src/dev-reload/index.js +1 -1
  157. package/src/discovery/file-route-parser.js +17 -3
  158. package/src/discovery/scan-routes.js +4 -1
  159. package/src/file-router.js +249 -69
  160. package/src/helpers.js +46 -26
  161. package/src/njk-frontmatter.js +30 -4
  162. package/src/plugin-manager.js +5 -1086
  163. package/src/routing/cascading.js +26 -8
  164. package/src/routing/mount-discovered-routes.js +69 -0
  165. package/src/server.js +238 -64
  166. package/src/services/builtins/auth.js +2 -273
  167. package/src/services/builtins/exchange.js +2 -229
  168. package/src/services/builtins/file-manager.js +2 -178
  169. package/src/services/builtins/mail.js +2 -134
  170. package/src/services/builtins/media.js +2 -196
  171. package/src/services/builtins/system.js +2 -219
  172. package/src/services/discovery.js +2 -133
  173. package/src/services/executor.js +2 -575
  174. package/src/services/index.js +4 -109
  175. package/src/services/loader.js +1 -119
  176. package/src/services/memoize.js +2 -408
  177. package/src/services/registry.js +1 -711
  178. package/src/services/validator.js +2 -424
  179. package/.agents/ADMIN.md +0 -426
  180. package/.agents/AGENTS.md +0 -93
  181. package/.agents/ARCHITECTURE.md +0 -163
  182. package/.agents/AUTH.md +0 -581
  183. package/.agents/CACHE.md +0 -147
  184. package/.agents/CLI.md +0 -649
  185. package/.agents/COMPRESSION.md +0 -59
  186. package/.agents/CONVENTIONS.md +0 -50
  187. package/.agents/EARLY_HINTS.md +0 -120
  188. package/.agents/EMAIL.md +0 -223
  189. package/.agents/ERRORS.md +0 -260
  190. package/.agents/EVENTS.md +0 -179
  191. package/.agents/FLAGS.md +0 -118
  192. package/.agents/HEALTH.md +0 -118
  193. package/.agents/LIMITS.md +0 -109
  194. package/.agents/LOGGING.md +0 -93
  195. package/.agents/MCP.md +0 -103
  196. package/.agents/METRICS.md +0 -127
  197. package/.agents/MIGRATION_JOBS.md +0 -95
  198. package/.agents/ORM.md +0 -626
  199. package/.agents/PLUGINS.md +0 -729
  200. package/.agents/QUEUE.md +0 -290
  201. package/.agents/RATE_LIMIT.md +0 -147
  202. package/.agents/REALTIME.md +0 -213
  203. package/.agents/RESILIENCE.md +0 -235
  204. package/.agents/ROUTING.md +0 -605
  205. package/.agents/SCALE_OUT.md +0 -109
  206. package/.agents/SECRETS.md +0 -117
  207. package/.agents/SERVICES.md +0 -341
  208. package/.agents/SHUTDOWN.md +0 -67
  209. package/.agents/STREAMING.md +0 -108
  210. package/.agents/WORKFLOW.md +0 -27
  211. package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
  212. package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
  213. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
  214. package/.agents/skills/webspresso-framework/SKILL.md +0 -88
  215. package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -118
  216. package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -114
  217. package/.agents/skills/webspresso-services/SKILL.md +0 -101
  218. package/.cursorrules +0 -14
  219. package/AGENTS.md +0 -200
  220. package/CLAUDE.md +0 -54
  221. package/core/kernel/run-demo.js +0 -58
  222. package/core/utils/array.js +0 -12
  223. package/core/utils/base64url.js +0 -8
  224. package/core/utils/ensure-leading-slash.js +0 -6
  225. package/core/utils/escape-html.js +0 -7
  226. package/core/utils/escape-regexp.js +0 -7
  227. package/core/utils/get-client-ip.js +0 -6
  228. package/core/utils/hmac.js +0 -6
  229. package/core/utils/random.js +0 -9
  230. package/core/utils/safe-compare.js +0 -3
  231. package/core/utils/sha256.js +0 -6
  232. package/core/utils/strip-trailing-slash.js +0 -6
  233. package/core/utils/walk.js +0 -7
  234. package/plugins/admin-panel/styles.js +0 -9
  235. package/plugins/data-exchange/export-xlsx.js +0 -119
@@ -85,6 +85,40 @@ function generateFileManagerComponent(options = {}) {
85
85
  ]);
86
86
  }
87
87
 
88
+ function renderShortcutItem(keys, desc) {
89
+ return m('div.flex.items-center.justify-between.p-2.rounded-lg.bg-gray-50.dark:bg-slate-800/70.border.border-gray-100.dark:border-slate-700/50', [
90
+ m('span.text-xs.text-gray-600.dark:text-slate-300', desc),
91
+ m('kbd.px-2.py-0.5.text-[11px].font-mono.font-semibold.rounded.bg-white.dark:bg-slate-700.text-gray-800.dark:text-slate-200.border.border-gray-200.dark:border-slate-600.shadow-xs', keys)
92
+ ]);
93
+ }
94
+
95
+ function renderCategoryCard(title, icon, cat, textColor, dotBg) {
96
+ var c = cat || { count: 0, bytes: 0, percent: 0, formattedBytes: '0 B' };
97
+ return m('div.p-2.rounded-lg.bg-gray-50.dark:bg-slate-900/60.border.border-gray-100.dark:border-slate-700/50.flex.flex-col.gap-0.5', [
98
+ m('div.flex.items-center.gap-1.5', [
99
+ m('span.w-2.h-2.rounded-full.' + dotBg),
100
+ m('span.font-semibold.text-gray-800.dark:text-slate-200.truncate', title)
101
+ ]),
102
+ m('div.flex.items-baseline.justify-between.mt-1', [
103
+ m('span.font-medium.' + textColor, c.formattedBytes),
104
+ m('span.text-[10px].text-gray-400', c.count + ' files')
105
+ ])
106
+ ]);
107
+ }
108
+
109
+ var TEXT_EXTS = [
110
+ 'txt', 'csv', 'md', 'markdown', 'json', 'xml', 'html', 'htm',
111
+ 'css', 'scss', 'sass', 'less', 'js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx',
112
+ 'sql', 'yaml', 'yml', 'env', 'sh', 'bash', 'log', 'ini', 'toml',
113
+ 'svg', 'gitignore', 'conf', 'config'
114
+ ];
115
+
116
+ function isTextFile(item) {
117
+ if (!item || item.isDir) return false;
118
+ var ext = (item.ext || '').toLowerCase();
119
+ return item.type === 'text' || item.type === 'code' || TEXT_EXTS.indexOf(ext) !== -1;
120
+ }
121
+
88
122
  function FileManagerView(isPickerMode, onSelectCallback, pickerAccept) {
89
123
  var currentPath = '';
90
124
  var breadcrumbs = [{ name: 'Root', path: '' }];
@@ -103,9 +137,235 @@ function generateFileManagerComponent(options = {}) {
103
137
  var copyingFeedback = null;
104
138
  var uploading = false;
105
139
  var uploadStatus = '';
140
+ var isDraggingOver = false;
141
+ var dragCounter = 0;
142
+ var activeXhr = null;
143
+ var uploadProgress = 0;
144
+ var uploadLoadedBytes = 0;
145
+ var uploadTotalBytes = 0;
146
+ var uploadFileCount = 0;
147
+ var uploadSuccessMessage = '';
148
+ var uploadSuccessTimer = null;
149
+ var selectedPaths = {};
150
+ var batchActionLoading = false;
151
+ var focusedIndex = 0;
152
+ var showShortcutsModal = false;
153
+ var storageStats = null;
154
+ var showStorageDetails = false;
155
+ var editMode = false;
156
+ var editorState = {
157
+ loaded: false,
158
+ rotation: 0,
159
+ flipH: false,
160
+ flipV: false,
161
+ aspectRatio: 'free',
162
+ naturalWidth: 0,
163
+ naturalHeight: 0,
164
+ cropX: 0,
165
+ cropY: 0,
166
+ cropW: 100,
167
+ cropH: 100,
168
+ targetWidth: 0,
169
+ targetHeight: 0,
170
+ lockRatio: true,
171
+ format: 'original',
172
+ saving: false,
173
+ saveStatus: '',
174
+ };
175
+ var loadedImageMeta = {};
176
+
177
+ var textEditMode = false;
178
+ var textEditorState = {
179
+ loading: false,
180
+ saving: false,
181
+ saveStatus: '',
182
+ error: null,
183
+ file: null,
184
+ content: '',
185
+ initialContent: '',
186
+ lineCount: 1,
187
+ charCount: 0,
188
+ wordWrap: false,
189
+ fontSize: 13,
190
+ isJson: false,
191
+ jsonError: null,
192
+ };
193
+
194
+ function openTextEditor(item) {
195
+ if (!item || item.isDir) return;
196
+ textEditMode = true;
197
+ modalState = null;
198
+ textEditorState.loading = true;
199
+ textEditorState.saving = false;
200
+ textEditorState.saveStatus = '';
201
+ textEditorState.error = null;
202
+ textEditorState.file = item;
203
+ textEditorState.content = '';
204
+ textEditorState.initialContent = '';
205
+ textEditorState.isJson = (item.ext || '').toLowerCase() === 'json';
206
+ textEditorState.jsonError = null;
207
+
208
+ api.get(API + '/content?path=' + encodeURIComponent(item.path)).then(function(res) {
209
+ textEditorState.loading = false;
210
+ textEditorState.content = res.content || '';
211
+ textEditorState.initialContent = res.content || '';
212
+ updateTextStats();
213
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
214
+ }).catch(function(err) {
215
+ textEditorState.loading = false;
216
+ textEditorState.error = err.message || String(err);
217
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
218
+ });
219
+ }
220
+
221
+ function updateTextStats() {
222
+ var lines = textEditorState.content.split(String.fromCharCode(10));
223
+ textEditorState.lineCount = lines.length;
224
+ textEditorState.charCount = textEditorState.content.length;
225
+ if (textEditorState.isJson) {
226
+ try {
227
+ if (textEditorState.content.trim()) {
228
+ JSON.parse(textEditorState.content);
229
+ }
230
+ textEditorState.jsonError = null;
231
+ } catch (e) {
232
+ textEditorState.jsonError = e.message;
233
+ }
234
+ }
235
+ }
236
+
237
+ function formatJsonContent() {
238
+ try {
239
+ var parsed = JSON.parse(textEditorState.content);
240
+ textEditorState.content = JSON.stringify(parsed, null, 2);
241
+ textEditorState.jsonError = null;
242
+ updateTextStats();
243
+ } catch (e) {
244
+ alert('Invalid JSON: ' + e.message);
245
+ }
246
+ }
247
+
248
+ function saveTextContent(overwrite) {
249
+ if (!textEditorState.file || textEditorState.saving) return;
250
+ textEditorState.saving = true;
251
+ textEditorState.saveStatus = 'Saving…';
252
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
253
+
254
+ api.post(API + '/save-content', {
255
+ path: textEditorState.file.path,
256
+ content: textEditorState.content,
257
+ overwrite: !!overwrite
258
+ }).then(function(res) {
259
+ textEditorState.saving = false;
260
+ textEditorState.saveStatus = 'Saved successfully!';
261
+ textEditorState.initialContent = textEditorState.content;
262
+ setTimeout(function() {
263
+ textEditorState.saveStatus = '';
264
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
265
+ }, 2500);
266
+ if (res && res.file) {
267
+ textEditorState.file = res.file;
268
+ selectedItem = res.file;
269
+ }
270
+ loadDirectory();
271
+ }).catch(function(err) {
272
+ textEditorState.saving = false;
273
+ textEditorState.saveStatus = '';
274
+ alert('Failed to save file: ' + (err.message || String(err)));
275
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
276
+ });
277
+ }
278
+
279
+ function getSelectedPathsList() {
280
+ return Object.keys(selectedPaths).filter(function(k) { return !!selectedPaths[k]; });
281
+ }
282
+
283
+ function isSelected(item) {
284
+ return !!selectedPaths[item.path];
285
+ }
286
+
287
+ function toggleSelect(item, e) {
288
+ if (e && e.stopPropagation) e.stopPropagation();
289
+ if (selectedPaths[item.path]) {
290
+ delete selectedPaths[item.path];
291
+ } else {
292
+ selectedPaths[item.path] = item;
293
+ }
294
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
295
+ }
296
+
297
+ function isAllSelected() {
298
+ if (!items || items.length === 0) return false;
299
+ for (var i = 0; i < items.length; i++) {
300
+ if (!selectedPaths[items[i].path]) return false;
301
+ }
302
+ return true;
303
+ }
304
+
305
+ function toggleSelectAll() {
306
+ if (isAllSelected()) {
307
+ clearSelection();
308
+ } else {
309
+ for (var i = 0; i < items.length; i++) {
310
+ selectedPaths[items[i].path] = items[i];
311
+ }
312
+ }
313
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
314
+ }
315
+
316
+ function clearSelection() {
317
+ selectedPaths = {};
318
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
319
+ }
320
+
321
+ function downloadBatchZip() {
322
+ var paths = getSelectedPathsList();
323
+ if (paths.length === 0) return;
324
+ var query = paths.map(function(p) { return 'paths=' + encodeURIComponent(p); }).join('&');
325
+ var downloadUrl = (window.__ADMIN_PATH__ || '/_admin') + '/api' + API + '/download-zip?' + query;
326
+ window.open(downloadUrl, '_blank');
327
+ }
328
+
329
+ function deleteBatchSelected() {
330
+ var paths = getSelectedPathsList();
331
+ if (paths.length === 0) return;
332
+ if (!confirm('Are you sure you want to delete ' + paths.length + ' selected item(s)?')) return;
333
+ batchActionLoading = true;
334
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
335
+
336
+ api.post(API + '/batch-delete', { paths: paths }).then(function(res) {
337
+ batchActionLoading = false;
338
+ clearSelection();
339
+ loadDirectory();
340
+ }).catch(function(err) {
341
+ batchActionLoading = false;
342
+ alert('Failed to delete selected items: ' + (err.message || String(err)));
343
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
344
+ });
345
+ }
346
+
347
+ function moveBatchSelected() {
348
+ var paths = getSelectedPathsList();
349
+ if (paths.length === 0) return;
350
+ var target = prompt('Enter target relative directory path to move selected items into:', currentPath);
351
+ if (target === null) return;
352
+ batchActionLoading = true;
353
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
354
+
355
+ api.post(API + '/batch-move', { paths: paths, destination: target }).then(function(res) {
356
+ batchActionLoading = false;
357
+ clearSelection();
358
+ loadDirectory();
359
+ }).catch(function(err) {
360
+ batchActionLoading = false;
361
+ alert('Failed to move selected items: ' + (err.message || String(err)));
362
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
363
+ });
364
+ }
106
365
 
107
366
  function loadDirectory(pathParam) {
108
367
  if (pathParam !== undefined) currentPath = pathParam;
368
+ selectedPaths = {};
109
369
  loading = true;
110
370
  error = null;
111
371
 
@@ -117,7 +377,11 @@ function generateFileManagerComponent(options = {}) {
117
377
  api.get(API + q).then(function(res) {
118
378
  items = res.items || [];
119
379
  breadcrumbs = res.breadcrumbs || [{ name: 'Root', path: '' }];
380
+ if (res.storage) {
381
+ storageStats = res.storage;
382
+ }
120
383
  loading = false;
384
+ focusedIndex = Math.max(0, Math.min(focusedIndex, Math.max(0, items.length - 1)));
121
385
  if (typeof m !== 'undefined' && m.redraw) m.redraw();
122
386
  }).catch(function(err) {
123
387
  error = err.message || String(err);
@@ -126,6 +390,15 @@ function generateFileManagerComponent(options = {}) {
126
390
  });
127
391
  }
128
392
 
393
+ function loadStorageStats(force) {
394
+ api.get(API + '/storage-stats' + (force ? '?refresh=true' : '')).then(function(res) {
395
+ if (res && res.stats) {
396
+ storageStats = res.stats;
397
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
398
+ }
399
+ }).catch(function() {});
400
+ }
401
+
129
402
  function createFolder() {
130
403
  if (!inputFolderValue.trim()) return;
131
404
  api.post(API + '/mkdir', { path: currentPath, name: inputFolderValue.trim() }).then(function() {
@@ -160,10 +433,27 @@ function generateFileManagerComponent(options = {}) {
160
433
  });
161
434
  }
162
435
 
436
+ function cancelUpload() {
437
+ if (activeXhr) {
438
+ try { activeXhr.abort(); } catch (_) {}
439
+ activeXhr = null;
440
+ }
441
+ uploading = false;
442
+ uploadProgress = 0;
443
+ uploadStatus = 'Upload cancelled';
444
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
445
+ }
446
+
163
447
  function handleFileUpload(fileList) {
164
448
  if (!fileList || fileList.length === 0) return;
165
449
  uploading = true;
450
+ uploadProgress = 0;
451
+ uploadLoadedBytes = 0;
452
+ uploadTotalBytes = 0;
453
+ uploadFileCount = fileList.length;
166
454
  uploadStatus = 'Uploading ' + fileList.length + ' file(s)…';
455
+ uploadSuccessMessage = '';
456
+ if (uploadSuccessTimer) clearTimeout(uploadSuccessTimer);
167
457
  if (typeof m !== 'undefined' && m.redraw) m.redraw();
168
458
 
169
459
  var fd = new FormData();
@@ -173,31 +463,515 @@ function generateFileManagerComponent(options = {}) {
173
463
 
174
464
  var uploadUrl = (window.__ADMIN_PATH__ || '/_admin') + '/api' + API + '/upload?path=' + encodeURIComponent(currentPath);
175
465
 
176
- var fetchFn = (typeof window !== 'undefined' && window.Admin && window.Admin.fetch) ? window.Admin.fetch : fetch;
177
- fetchFn(uploadUrl, {
178
- method: 'POST',
179
- body: fd,
180
- credentials: 'include',
181
- timeout: 60000,
182
- }).then(function(r) { return r.json(); }).then(function(res) {
466
+ var xhr = new XMLHttpRequest();
467
+ activeXhr = xhr;
468
+
469
+ xhr.upload.onprogress = function(evt) {
470
+ if (evt.lengthComputable) {
471
+ uploadLoadedBytes = evt.loaded;
472
+ uploadTotalBytes = evt.total;
473
+ uploadProgress = Math.min(100, Math.round((evt.loaded / evt.total) * 100));
474
+ uploadStatus = 'Uploading ' + uploadFileCount + ' file(s) (' + formatBytes(evt.loaded) + ' / ' + formatBytes(evt.total) + ')';
475
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
476
+ }
477
+ };
478
+
479
+ xhr.onload = function() {
480
+ activeXhr = null;
183
481
  uploading = false;
184
- uploadStatus = '';
185
- if (res.success) {
186
- loadDirectory();
187
- } else {
188
- alert('Upload error: ' + (res.error || 'Unknown error'));
482
+ uploadProgress = 100;
483
+ try {
484
+ var res = JSON.parse(xhr.responseText || '{}');
485
+ if (xhr.status >= 200 && xhr.status < 300 && res.success) {
486
+ uploadSuccessMessage = (res.files && res.files.length ? res.files.length : uploadFileCount) + ' file(s) successfully uploaded!';
487
+ uploadSuccessTimer = setTimeout(function() {
488
+ uploadSuccessMessage = '';
489
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
490
+ }, 4000);
491
+ loadDirectory();
492
+ } else {
493
+ alert('Upload error: ' + (res.error || ('HTTP ' + xhr.status)));
494
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
495
+ }
496
+ } catch (e) {
497
+ alert('Upload failed: ' + (e.message || String(e)));
189
498
  if (typeof m !== 'undefined' && m.redraw) m.redraw();
190
499
  }
191
- }).catch(function(err) {
500
+ };
501
+
502
+ xhr.onerror = function() {
503
+ activeXhr = null;
192
504
  uploading = false;
193
- uploadStatus = '';
194
- alert('Upload error: ' + (err.message || String(err)));
505
+ uploadProgress = 0;
506
+ alert('Network error while uploading');
195
507
  if (typeof m !== 'undefined' && m.redraw) m.redraw();
196
- });
508
+ };
509
+
510
+ xhr.onabort = function() {
511
+ activeXhr = null;
512
+ uploading = false;
513
+ uploadProgress = 0;
514
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
515
+ };
516
+
517
+ xhr.open('POST', uploadUrl, true);
518
+ xhr.withCredentials = true;
519
+ xhr.send(fd);
520
+ }
521
+
522
+ if (typeof window !== 'undefined') {
523
+ window.__fmDropzoneTest = {
524
+ setDrag: function(active) {
525
+ isDraggingOver = !!active;
526
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
527
+ },
528
+ setProgress: function(pct, loaded, total, status) {
529
+ uploading = true;
530
+ uploadProgress = pct;
531
+ uploadLoadedBytes = loaded || 2450000;
532
+ uploadTotalBytes = total || 5000000;
533
+ uploadStatus = status || ('Uploading 3 file(s) (' + formatBytes(uploadLoadedBytes) + ' / ' + formatBytes(uploadTotalBytes) + ')');
534
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
535
+ },
536
+ setSuccess: function(msg) {
537
+ uploading = false;
538
+ uploadSuccessMessage = msg || '3 file(s) successfully uploaded!';
539
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
540
+ },
541
+ selectFirst: function(n) {
542
+ selectedPaths = {};
543
+ for (var i = 0; i < Math.min(n || 2, items.length); i++) {
544
+ selectedPaths[items[i].path] = items[i];
545
+ }
546
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
547
+ },
548
+ reset: function() {
549
+ uploading = false;
550
+ isDraggingOver = false;
551
+ uploadSuccessMessage = '';
552
+ selectedPaths = {};
553
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
554
+ },
555
+ setFocusedIndex: function(idx) {
556
+ focusedIndex = idx;
557
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
558
+ },
559
+ getFocusedIndex: function() { return focusedIndex; },
560
+ toggleShortcutsModal: function(show) {
561
+ showShortcutsModal = typeof show === 'boolean' ? show : !showShortcutsModal;
562
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
563
+ },
564
+ isShortcutsModalOpen: function() { return showShortcutsModal; },
565
+ setStorageStats: function(stats) {
566
+ storageStats = stats;
567
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
568
+ },
569
+ getStorageStats: function() { return storageStats; },
570
+ toggleStorageDetails: function(show) {
571
+ showStorageDetails = typeof show === 'boolean' ? show : !showStorageDetails;
572
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
573
+ },
574
+ isStorageDetailsOpen: function() { return showStorageDetails; }
575
+ };
576
+ }
577
+
578
+ function isEditableActive() {
579
+ var el = document.activeElement;
580
+ if (!el) return false;
581
+ var tag = el.tagName ? el.tagName.toLowerCase() : '';
582
+ return tag === 'input' || tag === 'textarea' || tag === 'select' || el.isContentEditable;
583
+ }
584
+
585
+ function handleGlobalKeydown(e) {
586
+ if (!e) return;
587
+
588
+ // Ignore when typing inside editable inputs
589
+ if (isEditableActive()) {
590
+ if (e.key === 'Escape') {
591
+ if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
592
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
593
+ }
594
+ return;
595
+ }
596
+
597
+ // If text editor or image editor is active
598
+ if (textEditMode) {
599
+ if (e.key === 'Escape') {
600
+ if (textEditorState.content !== textEditorState.initialContent) {
601
+ if (!confirm('You have unsaved changes. Discard them?')) return;
602
+ }
603
+ textEditMode = false;
604
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
605
+ }
606
+ return;
607
+ }
608
+ if (editMode) {
609
+ if (e.key === 'Escape') {
610
+ editMode = false;
611
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
612
+ }
613
+ return;
614
+ }
615
+
616
+ // Close shortcuts modal if open
617
+ if (showShortcutsModal) {
618
+ if (e.key === 'Escape' || e.key === '?' || e.key === 'h' || e.key === 'H') {
619
+ e.preventDefault();
620
+ showShortcutsModal = false;
621
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
622
+ }
623
+ return;
624
+ }
625
+
626
+ // Handle Escape: close open modal or clear selection
627
+ if (e.key === 'Escape') {
628
+ if (modalState) {
629
+ e.preventDefault();
630
+ modalState = null;
631
+ selectedItem = null;
632
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
633
+ return;
634
+ }
635
+ var curSelected = getSelectedPathsList();
636
+ if (curSelected.length > 0) {
637
+ e.preventDefault();
638
+ selectedPaths = {};
639
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
640
+ return;
641
+ }
642
+ }
643
+
644
+ // If other dialogs (rename, delete, newFolder) are open, let modal handle it
645
+ if (modalState && modalState !== 'preview') {
646
+ return;
647
+ }
648
+
649
+ // Toggle shortcuts cheatsheet with '?' or 'h'
650
+ if (e.key === '?' || ((e.key === 'h' || e.key === 'H') && !e.ctrlKey && !e.metaKey)) {
651
+ e.preventDefault();
652
+ showShortcutsModal = true;
653
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
654
+ return;
655
+ }
656
+
657
+ // Focus search input with '/'
658
+ if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
659
+ e.preventDefault();
660
+ var searchEl = document.getElementById('fm-search-input');
661
+ if (searchEl) {
662
+ searchEl.focus();
663
+ searchEl.select();
664
+ }
665
+ return;
666
+ }
667
+
668
+ // Select All with Cmd+A or Ctrl+A
669
+ if ((e.metaKey || e.ctrlKey) && (e.key === 'a' || e.key === 'A')) {
670
+ e.preventDefault();
671
+ toggleSelectAll();
672
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
673
+ return;
674
+ }
675
+
676
+ // Space: Finder-style Quick Look
677
+ if (e.key === ' ' || e.code === 'Space') {
678
+ if (items.length > 0 && focusedIndex < items.length) {
679
+ e.preventDefault();
680
+ if (modalState === 'preview') {
681
+ modalState = null;
682
+ selectedItem = null;
683
+ } else {
684
+ selectedItem = items[focusedIndex];
685
+ modalState = 'preview';
686
+ }
687
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
688
+ }
689
+ return;
690
+ }
691
+
692
+ // If preview modal is open, arrows cycle through items
693
+ if (modalState === 'preview') {
694
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
695
+ e.preventDefault();
696
+ if (items.length > 0) {
697
+ focusedIndex = (focusedIndex + 1) % items.length;
698
+ selectedItem = items[focusedIndex];
699
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
700
+ }
701
+ return;
702
+ }
703
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
704
+ e.preventDefault();
705
+ if (items.length > 0) {
706
+ focusedIndex = (focusedIndex - 1 + items.length) % items.length;
707
+ selectedItem = items[focusedIndex];
708
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
709
+ }
710
+ return;
711
+ }
712
+ return;
713
+ }
714
+
715
+ // Arrow navigation in items list
716
+ if (e.key === 'ArrowRight') {
717
+ if (items.length > 0) {
718
+ e.preventDefault();
719
+ focusedIndex = (focusedIndex + 1) % items.length;
720
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
721
+ }
722
+ return;
723
+ }
724
+ if (e.key === 'ArrowLeft') {
725
+ if (items.length > 0) {
726
+ e.preventDefault();
727
+ focusedIndex = (focusedIndex - 1 + items.length) % items.length;
728
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
729
+ }
730
+ return;
731
+ }
732
+ if (e.key === 'ArrowDown') {
733
+ if (items.length > 0) {
734
+ e.preventDefault();
735
+ var step = viewMode === 'grid' ? 4 : 1;
736
+ focusedIndex = Math.min(items.length - 1, focusedIndex + step);
737
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
738
+ }
739
+ return;
740
+ }
741
+ if (e.key === 'ArrowUp') {
742
+ if (items.length > 0) {
743
+ e.preventDefault();
744
+ var stepUp = viewMode === 'grid' ? 4 : 1;
745
+ focusedIndex = Math.max(0, focusedIndex - stepUp);
746
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
747
+ }
748
+ return;
749
+ }
750
+
751
+ // Enter: Open folder or view file
752
+ if (e.key === 'Enter') {
753
+ if (items.length > 0 && focusedIndex < items.length) {
754
+ e.preventDefault();
755
+ var itm = items[focusedIndex];
756
+ if (itm.isDir) {
757
+ loadDirectory(itm.path);
758
+ focusedIndex = 0;
759
+ } else if (isPickerMode && onSelectCallback) {
760
+ onSelectCallback(itm.publicUrl, itm);
761
+ } else {
762
+ selectedItem = itm;
763
+ modalState = 'preview';
764
+ }
765
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
766
+ }
767
+ return;
768
+ }
769
+
770
+ // Parent folder: Alt+Up or Cmd/Ctrl+Up
771
+ if ((e.altKey && e.key === 'ArrowUp') || ((e.metaKey || e.ctrlKey) && e.key === 'ArrowUp')) {
772
+ if (breadcrumbs.length > 1) {
773
+ e.preventDefault();
774
+ var parentBc = breadcrumbs[breadcrumbs.length - 2];
775
+ loadDirectory(parentBc.path);
776
+ focusedIndex = 0;
777
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
778
+ }
779
+ return;
780
+ }
781
+
782
+ // Toggle selection with 'x'
783
+ if ((e.key === 'x' || e.key === 'X') && !e.ctrlKey && !e.metaKey) {
784
+ if (items.length > 0 && focusedIndex < items.length) {
785
+ e.preventDefault();
786
+ var toggleItem = items[focusedIndex];
787
+ if (selectedPaths[toggleItem.path]) {
788
+ delete selectedPaths[toggleItem.path];
789
+ } else {
790
+ selectedPaths[toggleItem.path] = toggleItem;
791
+ }
792
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
793
+ }
794
+ return;
795
+ }
796
+
797
+ // Quick edit with 'e'
798
+ if ((e.key === 'e' || e.key === 'E') && !e.ctrlKey && !e.metaKey) {
799
+ if (items.length > 0 && focusedIndex < items.length) {
800
+ var editItm = items[focusedIndex];
801
+ if (isTextFile(editItm)) {
802
+ e.preventDefault();
803
+ openTextEditor(editItm);
804
+ } else if (editItm.type === 'image' && editItm.publicUrl && !editItm.name.endsWith('.svg')) {
805
+ e.preventDefault();
806
+ selectedItem = editItm;
807
+ openImageEditor(editItm);
808
+ }
809
+ }
810
+ return;
811
+ }
812
+
813
+ // Rename with 'r'
814
+ if ((e.key === 'r' || e.key === 'R') && !e.ctrlKey && !e.metaKey) {
815
+ if (items.length > 0 && focusedIndex < items.length) {
816
+ e.preventDefault();
817
+ selectedItem = items[focusedIndex];
818
+ inputRenameValue = selectedItem.name;
819
+ modalState = 'rename';
820
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
821
+ }
822
+ return;
823
+ }
824
+
825
+ // Delete with Delete or Cmd+Backspace
826
+ if (e.key === 'Delete' || (e.key === 'Backspace' && (e.metaKey || e.ctrlKey))) {
827
+ e.preventDefault();
828
+ var selPaths = getSelectedPathsList();
829
+ if (selPaths.length > 0) {
830
+ deleteBatchSelected();
831
+ } else if (items.length > 0 && focusedIndex < items.length) {
832
+ selectedItem = items[focusedIndex];
833
+ modalState = 'delete';
834
+ }
835
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
836
+ return;
837
+ }
838
+ }
839
+
840
+ function openImageEditor(item) {
841
+ if (!item || item.type !== 'image' || !item.publicUrl) return;
842
+ editMode = true;
843
+ editorState.loaded = false;
844
+ editorState.rotation = 0;
845
+ editorState.flipH = false;
846
+ editorState.flipV = false;
847
+ editorState.aspectRatio = 'free';
848
+ editorState.cropX = 0;
849
+ editorState.cropY = 0;
850
+ editorState.cropW = 100;
851
+ editorState.cropH = 100;
852
+ editorState.saving = false;
853
+ editorState.saveStatus = '';
854
+ editorState.format = 'original';
855
+
856
+ var img = new Image();
857
+ img.crossOrigin = 'anonymous';
858
+ img.onload = function() {
859
+ editorState.loaded = true;
860
+ editorState.naturalWidth = img.naturalWidth;
861
+ editorState.naturalHeight = img.naturalHeight;
862
+ editorState.targetWidth = img.naturalWidth;
863
+ editorState.targetHeight = img.naturalHeight;
864
+ loadedImageMeta[item.path] = { width: img.naturalWidth, height: img.naturalHeight };
865
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
866
+ };
867
+ img.onerror = function() {
868
+ alert('Could not load image for editing.');
869
+ editMode = false;
870
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
871
+ };
872
+ img.src = item.publicUrl;
873
+ }
874
+
875
+ function setAspectRatio(ratio) {
876
+ editorState.aspectRatio = ratio;
877
+ if (ratio === '1:1') {
878
+ var minSide = Math.min(editorState.cropW, editorState.cropH);
879
+ editorState.cropW = minSide;
880
+ editorState.cropH = minSide;
881
+ } else if (ratio === '16:9') {
882
+ editorState.cropW = 100;
883
+ editorState.cropH = Math.min(100, Math.round((100 * (editorState.naturalWidth * 9)) / (editorState.naturalHeight * 16)));
884
+ } else if (ratio === '4:3') {
885
+ editorState.cropW = 100;
886
+ editorState.cropH = Math.min(100, Math.round((100 * (editorState.naturalWidth * 3)) / (editorState.naturalHeight * 4)));
887
+ }
888
+ }
889
+
890
+ function saveTransformedImage(overwrite) {
891
+ if (!selectedItem || editorState.saving) return;
892
+ editorState.saving = true;
893
+ editorState.saveStatus = 'Applying transformations…';
894
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
895
+
896
+ var img = new Image();
897
+ img.crossOrigin = 'anonymous';
898
+ img.onload = function() {
899
+ try {
900
+ var canvas = document.createElement('canvas');
901
+ var ctx = canvas.getContext('2d');
902
+
903
+ var sx = (editorState.cropX / 100) * img.naturalWidth;
904
+ var sy = (editorState.cropY / 100) * img.naturalHeight;
905
+ var sw = (editorState.cropW / 100) * img.naturalWidth;
906
+ var sh = (editorState.cropH / 100) * img.naturalHeight;
907
+
908
+ var isRotated90 = (editorState.rotation % 180 !== 0);
909
+ var outW = editorState.targetWidth || Math.round(sw);
910
+ var outH = editorState.targetHeight || Math.round(sh);
911
+
912
+ canvas.width = isRotated90 ? outH : outW;
913
+ canvas.height = isRotated90 ? outW : outH;
914
+
915
+ ctx.save();
916
+ ctx.translate(canvas.width / 2, canvas.height / 2);
917
+ ctx.rotate((editorState.rotation * Math.PI) / 180);
918
+ ctx.scale(editorState.flipH ? -1 : 1, editorState.flipV ? -1 : 1);
919
+
920
+ var drawW = isRotated90 ? canvas.height : canvas.width;
921
+ var drawH = isRotated90 ? canvas.width : canvas.height;
922
+ ctx.drawImage(img, sx, sy, sw, sh, -drawW / 2, -drawH / 2, drawW, drawH);
923
+ ctx.restore();
924
+
925
+ var mime = 'image/png';
926
+ if (editorState.format && editorState.format !== 'original') {
927
+ mime = editorState.format;
928
+ } else if (selectedItem.ext === 'jpg' || selectedItem.ext === 'jpeg') {
929
+ mime = 'image/jpeg';
930
+ } else if (selectedItem.ext === 'webp') {
931
+ mime = 'image/webp';
932
+ }
933
+
934
+ var dataUrl = canvas.toDataURL(mime, 0.92);
935
+
936
+ api.post(API + '/save-image', {
937
+ path: selectedItem.path,
938
+ dataUrl: dataUrl,
939
+ overwrite: !!overwrite
940
+ }).then(function(res) {
941
+ editorState.saving = false;
942
+ editorState.saveStatus = '';
943
+ editMode = false;
944
+ if (res && res.file) {
945
+ selectedItem = res.file;
946
+ }
947
+ loadDirectory();
948
+ }).catch(function(err) {
949
+ editorState.saving = false;
950
+ editorState.saveStatus = '';
951
+ alert('Failed to save image: ' + (err.message || String(err)));
952
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
953
+ });
954
+ } catch (e) {
955
+ editorState.saving = false;
956
+ editorState.saveStatus = '';
957
+ alert('Canvas processing error: ' + (e.message || String(e)));
958
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
959
+ }
960
+ };
961
+ img.onerror = function() {
962
+ editorState.saving = false;
963
+ editorState.saveStatus = '';
964
+ alert('Could not reload image for canvas export.');
965
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
966
+ };
967
+ img.src = selectedItem.publicUrl + (selectedItem.publicUrl.indexOf('?') === -1 ? '?t=' : '&t=') + Date.now();
197
968
  }
198
969
 
199
970
  return {
200
971
  oninit: function() {
972
+ if (typeof window !== 'undefined') {
973
+ window.addEventListener('keydown', handleGlobalKeydown);
974
+ }
201
975
  if (isPickerMode && pickerAccept) {
202
976
  if (pickerAccept.indexOf('image') !== -1) typeFilter = 'image';
203
977
  else if (pickerAccept.indexOf('video') !== -1) typeFilter = 'video';
@@ -207,33 +981,49 @@ function generateFileManagerComponent(options = {}) {
207
981
  loadDirectory('');
208
982
  },
209
983
 
984
+ onremove: function() {
985
+ if (typeof window !== 'undefined') {
986
+ window.removeEventListener('keydown', handleGlobalKeydown);
987
+ }
988
+ },
989
+
210
990
  view: function() {
211
991
  return m('div.space-y-4', [
212
992
  // Toolbar
213
993
  m('div.flex.flex-col.md:flex-row.md:items-center.justify-between.gap-3.bg-white.dark:bg-slate-800.p-4.rounded-xl.border.border-gray-200.dark:border-slate-700.shadow-sm', [
214
- // Left: Breadcrumbs
215
- m('div.flex.items-center.gap-2.flex-wrap.text-sm', [
216
- m('span.text-gray-400.dark:text-slate-500', 'πŸ“'),
217
- breadcrumbs.map(function(bc, idx) {
218
- var isLast = idx === breadcrumbs.length - 1;
219
- return m('span.flex.items-center.gap-2', [
220
- isLast
221
- ? m('span.font-semibold.text-gray-900.dark:text-slate-100', bc.name)
222
- : m('button.text-indigo-600.dark:text-indigo-400.hover:underline', {
223
- onclick: function() { loadDirectory(bc.path); }
224
- }, bc.name),
225
- !isLast ? m('span.text-gray-400.dark:text-slate-600', '/') : null
226
- ]);
227
- })
994
+ // Left: Breadcrumbs & Select All
995
+ m('div.flex.items-center.gap-3.flex-wrap.text-sm', [
996
+ !loading && !error && items.length > 0 ? m('label.flex.items-center.gap-1.5.cursor-pointer.px-2.5.py-1.rounded-lg.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.text-xs.font-medium.text-gray-700.dark:text-slate-300.hover:bg-gray-100.dark:hover:bg-slate-800.transition.select-none', [
997
+ m('input[type=checkbox].rounded.border-gray-300.text-indigo-600.focus:ring-indigo-500.cursor-pointer', {
998
+ checked: isAllSelected(),
999
+ onchange: toggleSelectAll
1000
+ }),
1001
+ m('span', isAllSelected() ? 'Unselect All' : 'Select All')
1002
+ ]) : null,
1003
+ m('div.flex.items-center.gap-2.flex-wrap', [
1004
+ m('span.text-gray-400.dark:text-slate-500', 'πŸ“'),
1005
+ breadcrumbs.map(function(bc, idx) {
1006
+ var isLast = idx === breadcrumbs.length - 1;
1007
+ return m('span.flex.items-center.gap-2', [
1008
+ isLast
1009
+ ? m('span.font-semibold.text-gray-900.dark:text-slate-100', bc.name)
1010
+ : m('button.text-indigo-600.dark:text-indigo-400.hover:underline', {
1011
+ onclick: function() { loadDirectory(bc.path); }
1012
+ }, bc.name),
1013
+ !isLast ? m('span.text-gray-400.dark:text-slate-600', '/') : null
1014
+ ]);
1015
+ })
1016
+ ])
228
1017
  ]),
229
1018
 
230
1019
  // Right: Actions & Tools
231
1020
  m('div.flex.items-center.gap-2.flex-wrap', [
232
1021
  // Search input
233
1022
  m('div.relative', [
234
- m('input.text-xs.pl-8.pr-3.py-1.5.border.border-gray-200.dark:border-slate-700.rounded-lg.bg-gray-50.dark:bg-slate-900.text-gray-900.dark:text-slate-100.placeholder-gray-400', {
1023
+ m('input#fm-search-input.text-xs.pl-8.pr-3.py-1.5.border.border-gray-200.dark:border-slate-700.rounded-lg.bg-gray-50.dark:bg-slate-900.text-gray-900.dark:text-slate-100.placeholder-gray-400', {
1024
+ id: 'fm-search-input',
235
1025
  type: 'text',
236
- placeholder: 'Search files…',
1026
+ placeholder: 'Search files… (/ to focus)',
237
1027
  value: searchQuery,
238
1028
  oninput: function(e) { searchQuery = e.target.value; },
239
1029
  onkeydown: function(e) { if (e.key === 'Enter') loadDirectory(); }
@@ -285,6 +1075,15 @@ function generateFileManagerComponent(options = {}) {
285
1075
  })
286
1076
  ]),
287
1077
 
1078
+ // Shortcuts Cheatsheet
1079
+ m('button.px-2.5.py-1.5.text-xs.font-medium.bg-gray-100.dark:bg-slate-700.hover:bg-gray-200.dark:hover:bg-slate-600.text-gray-700.dark:text-slate-300.rounded-lg.flex.items-center.gap-1.transition', {
1080
+ title: 'Keyboard Shortcuts (Press ? or h)',
1081
+ onclick: function() { showShortcutsModal = true; }
1082
+ }, [
1083
+ m('span', '⌨️'),
1084
+ m('span.hidden.sm:inline', 'Shortcuts')
1085
+ ]),
1086
+
288
1087
  // Refresh
289
1088
  m('button.p-1.5.text-xs.text-gray-500.hover:text-gray-900.dark:hover:text-slate-100.rounded-lg', {
290
1089
  title: 'Refresh',
@@ -293,23 +1092,163 @@ function generateFileManagerComponent(options = {}) {
293
1092
  ])
294
1093
  ]),
295
1094
 
296
- // Uploading Banner
297
- uploading ? m('div.p-3.bg-indigo-50.dark:bg-indigo-950/40.border.border-indigo-200.dark:border-indigo-800.rounded-lg.flex.items-center.gap-3.text-xs.text-indigo-700.dark:text-indigo-300.animate-pulse', [
298
- m('span.animate-spin', '⏳'),
299
- m('span.font-medium', uploadStatus)
1095
+ // Upload Progress Banner / Card
1096
+ uploading ? m('div.p-4.bg-white.dark:bg-slate-800.border.border-indigo-200.dark:border-indigo-800.rounded-xl.shadow-sm.flex.flex-col.gap-2.animate-in.fade-in', [
1097
+ m('div.flex.items-center.justify-between.text-xs', [
1098
+ m('div.flex.items-center.gap-2', [
1099
+ m('span.w-2.h-2.rounded-full.bg-indigo-600.animate-ping'),
1100
+ m('span.font-semibold.text-gray-900.dark:text-slate-100', uploadStatus || 'Uploading files…'),
1101
+ ]),
1102
+ m('div.flex.items-center.gap-3', [
1103
+ m('span.font-mono.font-bold.text-indigo-600.dark:text-indigo-400', uploadProgress + '%'),
1104
+ m('button.text-xs.text-red-500.hover:text-red-700.dark:hover:text-red-400.font-medium.px-2.py-0.5.rounded.border.border-red-200.dark:border-red-900/40.hover:bg-red-50.dark:hover:bg-red-950/30.transition', {
1105
+ onclick: cancelUpload
1106
+ }, 'Cancel')
1107
+ ])
1108
+ ]),
1109
+ m('div.w-full.h-2.bg-gray-100.dark:bg-slate-700.rounded-full.overflow-hidden', [
1110
+ m('div.h-full.bg-gradient-to-r.from-indigo-500.to-violet-600.rounded-full.transition-all.duration-200', {
1111
+ style: { width: uploadProgress + '%' }
1112
+ })
1113
+ ]),
1114
+ uploadTotalBytes > 0 ? m('div.flex.items-center.justify-between.text-[11px].text-gray-400.dark:text-slate-400', [
1115
+ m('span', formatBytes(uploadLoadedBytes) + ' uploaded'),
1116
+ m('span', 'Total ' + formatBytes(uploadTotalBytes))
1117
+ ]) : null
1118
+ ]) : null,
1119
+
1120
+ // Upload Success Banner
1121
+ uploadSuccessMessage ? m('div.p-3.bg-emerald-50.dark:bg-emerald-950/40.border.border-emerald-200.dark:border-emerald-800.rounded-xl.flex.items-center.justify-between.text-xs.text-emerald-800.dark:text-emerald-200.shadow-sm.animate-in.fade-in', [
1122
+ m('div.flex.items-center.gap-2', [
1123
+ m('span.text-base', 'βœ…'),
1124
+ m('span.font-medium', uploadSuccessMessage)
1125
+ ]),
1126
+ m('button.text-emerald-600.dark:text-emerald-400.hover:text-emerald-900.font-bold.px-1', {
1127
+ onclick: function() { uploadSuccessMessage = ''; }
1128
+ }, 'βœ•')
1129
+ ]) : null,
1130
+
1131
+ // STORAGE USAGE & QUOTA VISUALIZER (OPTION 6)
1132
+ storageStats ? m('div.bg-white.dark:bg-slate-800.border.border-gray-200.dark:border-slate-700.rounded-xl.p-3.5.shadow-sm.space-y-2.5', [
1133
+ // Top row: Usage stats & Controls
1134
+ m('div.flex.flex-col.sm:flex-row.sm:items-center.justify-between.gap-2.text-xs', [
1135
+ m('div.flex.items-center.gap-2.flex-wrap', [
1136
+ m('span.text-base', 'πŸ’Ύ'),
1137
+ m('span.font-semibold.text-gray-900.dark:text-slate-100', 'Storage:'),
1138
+ m('span.font-medium.text-gray-700.dark:text-slate-300', storageStats.formattedTotal + ' of ' + storageStats.formattedQuota),
1139
+ m('span.px-2.py-0.5.rounded-full.text-[11px].font-semibold', {
1140
+ class: storageStats.isOverLimit
1141
+ ? 'bg-red-100 dark:bg-red-950/60 text-red-700 dark:text-red-300 border border-red-200 dark:border-red-800'
1142
+ : storageStats.isNearLimit
1143
+ ? 'bg-amber-100 dark:bg-amber-950/60 text-amber-800 dark:text-amber-300 border border-amber-200 dark:border-amber-800'
1144
+ : 'bg-emerald-50 dark:bg-emerald-950/50 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800'
1145
+ }, storageStats.usagePercent + '% used' + (storageStats.isOverLimit ? ' (Over Quota!)' : storageStats.isNearLimit ? ' (Near Quota)' : '')),
1146
+ ]),
1147
+
1148
+ m('div.flex.items-center.gap-3.text-gray-500.dark:text-slate-400', [
1149
+ m('span', storageStats.fileCount + ' files Β· ' + storageStats.folderCount + ' folders'),
1150
+ m('button.text-indigo-600.dark:text-indigo-400.hover:underline.font-medium.flex.items-center.gap-1', {
1151
+ onclick: function() { showStorageDetails = !showStorageDetails; }
1152
+ }, [
1153
+ m('span', showStorageDetails ? 'Hide Breakdown' : 'Breakdown'),
1154
+ m('span.text-[10px]', showStorageDetails ? 'β–²' : 'β–Ό')
1155
+ ]),
1156
+ m('button.p-1.text-gray-400.hover:text-gray-700.dark:hover:text-slate-200.rounded', {
1157
+ title: 'Refresh Storage Stats',
1158
+ onclick: function() { loadStorageStats(true); }
1159
+ }, 'πŸ”„')
1160
+ ])
1161
+ ]),
1162
+
1163
+ // Segmented Progress Bar
1164
+ m('div.w-full.h-2.5.bg-gray-100.dark:bg-slate-700/60.rounded-full.overflow-hidden.flex.shadow-inner', [
1165
+ storageStats.categories.images && storageStats.categories.images.bytes > 0
1166
+ ? m('div.h-full.bg-emerald-500.transition-all', {
1167
+ style: { width: Math.max(1, storageStats.categories.images.percent) + '%' },
1168
+ title: 'Images: ' + storageStats.categories.images.formattedBytes + ' (' + storageStats.categories.images.percent + '%)'
1169
+ }) : null,
1170
+ storageStats.categories.documents && storageStats.categories.documents.bytes > 0
1171
+ ? m('div.h-full.bg-indigo-500.transition-all', {
1172
+ style: { width: Math.max(1, storageStats.categories.documents.percent) + '%' },
1173
+ title: 'Documents: ' + storageStats.categories.documents.formattedBytes + ' (' + storageStats.categories.documents.percent + '%)'
1174
+ }) : null,
1175
+ storageStats.categories.code && storageStats.categories.code.bytes > 0
1176
+ ? m('div.h-full.bg-amber-500.transition-all', {
1177
+ style: { width: Math.max(1, storageStats.categories.code.percent) + '%' },
1178
+ title: 'Code & Text: ' + storageStats.categories.code.formattedBytes + ' (' + storageStats.categories.code.percent + '%)'
1179
+ }) : null,
1180
+ storageStats.categories.media && storageStats.categories.media.bytes > 0
1181
+ ? m('div.h-full.bg-pink-500.transition-all', {
1182
+ style: { width: Math.max(1, storageStats.categories.media.percent) + '%' },
1183
+ title: 'Media: ' + storageStats.categories.media.formattedBytes + ' (' + storageStats.categories.media.percent + '%)'
1184
+ }) : null,
1185
+ storageStats.categories.archives && storageStats.categories.archives.bytes > 0
1186
+ ? m('div.h-full.bg-purple-500.transition-all', {
1187
+ style: { width: Math.max(1, storageStats.categories.archives.percent) + '%' },
1188
+ title: 'Archives: ' + storageStats.categories.archives.formattedBytes + ' (' + storageStats.categories.archives.percent + '%)'
1189
+ }) : null,
1190
+ storageStats.categories.other && storageStats.categories.other.bytes > 0
1191
+ ? m('div.h-full.bg-slate-400.transition-all', {
1192
+ style: { width: Math.max(1, storageStats.categories.other.percent) + '%' },
1193
+ title: 'Other: ' + storageStats.categories.other.formattedBytes + ' (' + storageStats.categories.other.percent + '%)'
1194
+ }) : null
1195
+ ]),
1196
+
1197
+ // Expanded Breakdown Details
1198
+ showStorageDetails ? m('div.pt-2.border-t.border-gray-100.dark:border-slate-700/60.grid.grid-cols-2.sm:grid-cols-3.md:grid-cols-6.gap-2.text-xs.animate-in.fade-in', [
1199
+ renderCategoryCard('Images', 'πŸ–ΌοΈ', storageStats.categories.images, 'text-emerald-600 dark:text-emerald-400', 'bg-emerald-500'),
1200
+ renderCategoryCard('Documents', 'πŸ“„', storageStats.categories.documents, 'text-indigo-600 dark:text-indigo-400', 'bg-indigo-500'),
1201
+ renderCategoryCard('Code & Text', 'πŸ“', storageStats.categories.code, 'text-amber-600 dark:text-amber-400', 'bg-amber-500'),
1202
+ renderCategoryCard('Media', '🎬', storageStats.categories.media, 'text-pink-600 dark:text-pink-400', 'bg-pink-500'),
1203
+ renderCategoryCard('Archives', 'πŸ“¦', storageStats.categories.archives, 'text-purple-600 dark:text-purple-400', 'bg-purple-500'),
1204
+ renderCategoryCard('Other', 'πŸ—‚οΈ', storageStats.categories.other, 'text-slate-600 dark:text-slate-400', 'bg-slate-400'),
1205
+ ]) : null
300
1206
  ]) : null,
301
1207
 
302
1208
  // Main Content / Drop Zone
303
- m('div.min-h-[360px].relative', {
304
- ondragover: function(e) { e.preventDefault(); e.stopPropagation(); },
1209
+ m('div.min-h-[420px].relative', {
1210
+ ondragenter: function(e) {
1211
+ e.preventDefault();
1212
+ e.stopPropagation();
1213
+ dragCounter++;
1214
+ isDraggingOver = true;
1215
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
1216
+ },
1217
+ ondragleave: function(e) {
1218
+ e.preventDefault();
1219
+ e.stopPropagation();
1220
+ dragCounter--;
1221
+ if (dragCounter <= 0) {
1222
+ dragCounter = 0;
1223
+ isDraggingOver = false;
1224
+ }
1225
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
1226
+ },
1227
+ ondragover: function(e) {
1228
+ e.preventDefault();
1229
+ e.stopPropagation();
1230
+ if (e.dataTransfer) {
1231
+ e.dataTransfer.dropEffect = 'copy';
1232
+ }
1233
+ },
305
1234
  ondrop: function(e) {
306
1235
  e.preventDefault();
307
1236
  e.stopPropagation();
308
- if (e.dataTransfer && e.dataTransfer.files) {
1237
+ dragCounter = 0;
1238
+ isDraggingOver = false;
1239
+ if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
309
1240
  handleFileUpload(e.dataTransfer.files);
310
1241
  }
1242
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
311
1243
  }
312
1244
  }, [
1245
+ // FULL SURFACE DROPZONE OVERLAY
1246
+ isDraggingOver ? m('div.absolute.inset-0.z-30.bg-indigo-600/15.dark:bg-indigo-500/25.backdrop-blur-sm.border-2.border-dashed.border-indigo-500.rounded-2xl.flex.flex-col.items-center.justify-center.p-8.pointer-events-none.transition-all', [
1247
+ m('div.w-20.h-20.rounded-2xl.bg-indigo-600.text-white.flex.items-center.justify-center.text-3xl.shadow-xl.shadow-indigo-500/30.mb-4.animate-bounce', 'πŸ“₯'),
1248
+ m('h3.text-lg.font-bold.text-indigo-950.dark:text-indigo-100', 'Drop files here to upload'),
1249
+ m('p.text-sm.text-indigo-700.dark:text-indigo-300.mt-1', 'Target Directory: ' + (currentPath ? '/' + currentPath : '/ (Root)')),
1250
+ m('div.mt-4.px-3.py-1.rounded-full.bg-white/80.dark:bg-slate-900/80.text-xs.font-semibold.text-indigo-600.dark:text-indigo-400.shadow-sm', 'Release to automatically upload')
1251
+ ]) : null,
313
1252
  loading ? m('div.flex.flex-col.items-center.justify-center.py-20.text-gray-400', [
314
1253
  m('div.w-8.h-8.border-2.border-indigo-600.border-t-transparent.rounded-full.animate-spin.mb-2'),
315
1254
  m('p.text-xs', 'Loading files…')
@@ -324,10 +1263,14 @@ function generateFileManagerComponent(options = {}) {
324
1263
  ]) : null,
325
1264
 
326
1265
  // GRID VIEW
327
- !loading && !error && items.length > 0 && viewMode === 'grid' ? m('div.grid.grid-cols-2.sm:grid-cols-3.md:grid-cols-4.lg:grid-cols-6.gap-4', items.map(function(item) {
1266
+ !loading && !error && items.length > 0 && viewMode === 'grid' ? m('div.grid.grid-cols-2.sm:grid-cols-3.md:grid-cols-4.lg:grid-cols-6.gap-4', items.map(function(item, idx) {
328
1267
  var isImg = item.type === 'image' && item.publicUrl;
329
- return m('div.group.relative.flex.flex-col.bg-white.dark:bg-slate-800.border.border-gray-200.dark:border-slate-700.rounded-xl.p-3.hover:shadow-md.transition.cursor-pointer', {
1268
+ var selected = isSelected(item);
1269
+ var isFocused = (focusedIndex === idx);
1270
+ return m('div.group.relative.flex.flex-col.bg-white.dark:bg-slate-800.border.rounded-xl.p-3.hover:shadow-md.transition.cursor-pointer', {
1271
+ class: (selected ? 'border-indigo-500 ring-2 ring-indigo-500/50 bg-indigo-50/40 dark:bg-indigo-950/20 ' : 'border-gray-200 dark:border-slate-700 ') + (isFocused ? 'ring-2 ring-offset-2 ring-indigo-600 dark:ring-offset-slate-900 shadow-md ' : ''),
330
1272
  onclick: function() {
1273
+ focusedIndex = idx;
331
1274
  if (item.isDir) {
332
1275
  loadDirectory(item.path);
333
1276
  } else if (isPickerMode && onSelectCallback) {
@@ -338,6 +1281,15 @@ function generateFileManagerComponent(options = {}) {
338
1281
  }
339
1282
  }
340
1283
  }, [
1284
+ // Selection Checkbox
1285
+ m('div.absolute.top-2.left-2.z-10', [
1286
+ m('input[type=checkbox].rounded.border-gray-300.text-indigo-600.focus:ring-indigo-500.cursor-pointer.w-4.h-4.shadow-sm', {
1287
+ checked: selected,
1288
+ onclick: function(e) { if (e && e.stopPropagation) e.stopPropagation(); },
1289
+ onchange: function(e) { toggleSelect(item, e); }
1290
+ })
1291
+ ]),
1292
+
341
1293
  // Thumbnail / Icon
342
1294
  m('div.aspect-square.w-full.rounded-lg.bg-gray-50.dark:bg-slate-900.flex.items-center.justify-center.overflow-hidden.mb-2.relative', [
343
1295
  isImg
@@ -352,6 +1304,21 @@ function generateFileManagerComponent(options = {}) {
352
1304
  m('span', item.isDir ? 'Folder' : formatBytes(item.size)),
353
1305
  // Menu dots
354
1306
  m('div.relative.opacity-0.group-hover:opacity-100.transition', [
1307
+ isTextFile(item) ? m('button.hover:text-indigo-600.px-1', {
1308
+ title: 'Edit Code / Text',
1309
+ onclick: function(e) {
1310
+ e.stopPropagation();
1311
+ openTextEditor(item);
1312
+ }
1313
+ }, 'πŸ“') : null,
1314
+ item.type === 'image' && item.publicUrl && !item.name.endsWith('.svg') ? m('button.hover:text-indigo-600.px-1', {
1315
+ title: 'Edit Image',
1316
+ onclick: function(e) {
1317
+ e.stopPropagation();
1318
+ selectedItem = item;
1319
+ openImageEditor(item);
1320
+ }
1321
+ }, 'βœ‚οΈ') : null,
355
1322
  m('button.hover:text-gray-700.dark:hover:text-slate-200.px-1', {
356
1323
  onclick: function(e) {
357
1324
  e.stopPropagation();
@@ -377,6 +1344,12 @@ function generateFileManagerComponent(options = {}) {
377
1344
  m('table.min-w-full.text-left.text-xs', [
378
1345
  m('thead.bg-gray-50.dark:bg-slate-900.border-b.border-gray-200.dark:border-slate-700.text-gray-500', [
379
1346
  m('tr', [
1347
+ m('th.p-3.w-8', [
1348
+ m('input[type=checkbox].rounded.border-gray-300.text-indigo-600.focus:ring-indigo-500.cursor-pointer', {
1349
+ checked: isAllSelected(),
1350
+ onchange: toggleSelectAll
1351
+ })
1352
+ ]),
380
1353
  m('th.p-3', 'Name'),
381
1354
  m('th.p-3', 'Type'),
382
1355
  m('th.p-3', 'Size'),
@@ -384,9 +1357,13 @@ function generateFileManagerComponent(options = {}) {
384
1357
  m('th.p-3.text-right', 'Actions'),
385
1358
  ])
386
1359
  ]),
387
- m('tbody.divide-y.divide-gray-100.dark:divide-slate-700/50', items.map(function(item) {
388
- return m('tr.hover:bg-gray-50/80.dark:hover:bg-slate-700/30.transition.cursor-pointer', {
1360
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700/50', items.map(function(item, idx) {
1361
+ var selected = isSelected(item);
1362
+ var isFocused = (focusedIndex === idx);
1363
+ return m('tr.transition.cursor-pointer', {
1364
+ class: (selected ? 'bg-indigo-50/70 dark:bg-indigo-950/40 ' : 'hover:bg-gray-50/80 dark:hover:bg-slate-700/30 ') + (isFocused ? 'ring-2 ring-inset ring-indigo-500 font-semibold bg-indigo-50/40 dark:bg-indigo-950/30 ' : ''),
389
1365
  onclick: function() {
1366
+ focusedIndex = idx;
390
1367
  if (item.isDir) {
391
1368
  loadDirectory(item.path);
392
1369
  } else if (isPickerMode && onSelectCallback) {
@@ -397,6 +1374,13 @@ function generateFileManagerComponent(options = {}) {
397
1374
  }
398
1375
  }
399
1376
  }, [
1377
+ m('td.p-3.w-8', [
1378
+ m('input[type=checkbox].rounded.border-gray-300.text-indigo-600.focus:ring-indigo-500.cursor-pointer', {
1379
+ checked: selected,
1380
+ onclick: function(e) { if (e && e.stopPropagation) e.stopPropagation(); },
1381
+ onchange: function(e) { toggleSelect(item, e); }
1382
+ })
1383
+ ]),
400
1384
  m('td.p-3.flex.items-center.gap-2.font-medium.text-gray-900.dark:text-slate-100', [
401
1385
  m('span', renderFileIcon(item)),
402
1386
  m('span.truncate.max-w-xs.md:max-w-md', item.name)
@@ -405,6 +1389,19 @@ function generateFileManagerComponent(options = {}) {
405
1389
  m('td.p-3.text-gray-500.dark:text-slate-400', item.isDir ? 'β€”' : formatBytes(item.size)),
406
1390
  m('td.p-3.text-gray-500.dark:text-slate-400', formatDate(item.mtime)),
407
1391
  m('td.p-3.text-right.space-x-2', [
1392
+ isTextFile(item) ? m('button.text-indigo-600.dark:text-indigo-400.hover:underline.font-medium', {
1393
+ onclick: function(e) {
1394
+ e.stopPropagation();
1395
+ openTextEditor(item);
1396
+ }
1397
+ }, 'Edit') : null,
1398
+ item.type === 'image' && item.publicUrl && !item.name.endsWith('.svg') ? m('button.text-indigo-600.dark:text-indigo-400.hover:underline.font-medium', {
1399
+ onclick: function(e) {
1400
+ e.stopPropagation();
1401
+ selectedItem = item;
1402
+ openImageEditor(item);
1403
+ }
1404
+ }, 'Edit') : null,
408
1405
  !item.isDir && item.publicUrl ? m('button.text-indigo-600.dark:text-indigo-400.hover:underline', {
409
1406
  onclick: function(e) {
410
1407
  e.stopPropagation();
@@ -434,6 +1431,50 @@ function generateFileManagerComponent(options = {}) {
434
1431
  ]) : null
435
1432
  ]),
436
1433
 
1434
+ // STICKY FLOATING BULK ACTION BAR
1435
+ getSelectedPathsList().length > 0 ? m('div.fixed.bottom-6.left-1/2.-translate-x-1/2.z-50.bg-slate-900/95.dark:bg-slate-800/95.text-white.px-5.py-3.rounded-2xl.shadow-2xl.backdrop-blur-md.border.border-slate-700.flex.items-center.gap-4.animate-in.fade-in', [
1436
+ m('div.flex.items-center.gap-2.border-r.border-slate-700.pr-4', [
1437
+ m('span.w-2.h-2.rounded-full.bg-indigo-400.animate-ping'),
1438
+ m('span.text-xs.font-semibold', getSelectedPathsList().length + ' selected')
1439
+ ]),
1440
+
1441
+ // Action Buttons
1442
+ m('div.flex.items-center.gap-2', [
1443
+ // Download Zip
1444
+ m('button.px-3.py-1.5.bg-indigo-600.hover:bg-indigo-500.text-white.text-xs.font-medium.rounded-lg.flex.items-center.gap-1.5.transition.shadow-sm', {
1445
+ disabled: batchActionLoading,
1446
+ onclick: downloadBatchZip
1447
+ }, [
1448
+ m('span', 'πŸ“¦'),
1449
+ m('span', 'Download ZIP')
1450
+ ]),
1451
+
1452
+ // Move
1453
+ m('button.px-3.py-1.5.bg-slate-800.dark:bg-slate-700.hover:bg-slate-700.dark:hover:bg-slate-600.text-white.text-xs.font-medium.rounded-lg.flex.items-center.gap-1.5.transition', {
1454
+ disabled: batchActionLoading,
1455
+ onclick: moveBatchSelected
1456
+ }, [
1457
+ m('span', 'πŸ“'),
1458
+ m('span', 'Move…')
1459
+ ]),
1460
+
1461
+ // Delete
1462
+ m('button.px-3.py-1.5.bg-red-600/90.hover:bg-red-600.text-white.text-xs.font-medium.rounded-lg.flex.items-center.gap-1.5.transition.shadow-sm', {
1463
+ disabled: batchActionLoading,
1464
+ onclick: deleteBatchSelected
1465
+ }, [
1466
+ m('span', 'πŸ—‘οΈ'),
1467
+ m('span', 'Delete (' + getSelectedPathsList().length + ')')
1468
+ ]),
1469
+
1470
+ // Deselect
1471
+ m('button.p-1.5.text-slate-400.hover:text-white.rounded-lg.text-xs.transition.ml-1', {
1472
+ title: 'Clear Selection',
1473
+ onclick: clearSelection
1474
+ }, 'βœ•')
1475
+ ])
1476
+ ]) : null,
1477
+
437
1478
  // MODALS
438
1479
  // 1. New Folder Modal
439
1480
  modalState === 'newFolder' ? m('div.fixed.inset-0.bg-black/50.flex.items-center.justify-center.z-50.p-4', [
@@ -498,52 +1539,101 @@ function generateFileManagerComponent(options = {}) {
498
1539
  ]) : null,
499
1540
 
500
1541
  // 4. Preview Modal
501
- modalState === 'preview' && selectedItem ? m('div.fixed.inset-0.bg-black/60.flex.items-center.justify-center.z-50.p-4.backdrop-blur-sm', {
1542
+ modalState === 'preview' && selectedItem && !editMode ? m('div.fixed.inset-0.bg-black/60.flex.items-center.justify-center.z-50.p-4.backdrop-blur-sm', {
502
1543
  onclick: function() { modalState = null; }
503
1544
  }, [
504
- m('div.bg-white.dark:bg-slate-800.rounded-2xl.shadow-2xl.max-w-2xl.w-full.overflow-hidden.border.border-gray-200.dark:border-slate-700', {
1545
+ m('div.bg-white.dark:bg-slate-800.rounded-2xl.shadow-2xl.w-full.max-w-3xl.max-h-[85vh].flex.flex-col.overflow-hidden.border.border-gray-200.dark:border-slate-700.transition-all', {
505
1546
  onclick: function(e) { e.stopPropagation(); }
506
1547
  }, [
507
1548
  // Header
508
- m('div.flex.items-center.justify-between.p-4.border-b.border-gray-200.dark:border-slate-700', [
509
- m('h3.text-sm.font-bold.text-gray-900.dark:text-slate-100.truncate.max-w-lg', selectedItem.name),
510
- m('button.text-gray-400.hover:text-gray-600.dark:hover:text-slate-200.text-lg', {
511
- onclick: function() { modalState = null; }
512
- }, 'βœ•')
1549
+ m('div.flex.items-center.justify-between.px-5.py-3.5.border-b.border-gray-200.dark:border-slate-700.bg-gray-50/70.dark:bg-slate-800/80', [
1550
+ m('div.flex.items-center.gap-2.truncate.max-w-md', [
1551
+ m('span.text-xs.font-semibold.uppercase.px-2.py-0.5.rounded.bg-indigo-100.dark:bg-indigo-900/50.text-indigo-700.dark:text-indigo-300', selectedItem.ext || selectedItem.type),
1552
+ m('h3.text-sm.font-bold.text-gray-900.dark:text-slate-100.truncate', selectedItem.name),
1553
+ ]),
1554
+ m('div.flex.items-center.gap-2', [
1555
+ isTextFile(selectedItem) ? m('button.px-3.py-1.5.text-xs.font-medium.rounded-lg.bg-indigo-50.dark:bg-indigo-950/40.text-indigo-600.dark:text-indigo-400.hover:bg-indigo-100.dark:hover:bg-indigo-900/60.border.border-indigo-200.dark:border-indigo-800.flex.items-center.gap-1.5.transition', {
1556
+ onclick: function() { openTextEditor(selectedItem); }
1557
+ }, [
1558
+ m('span', 'πŸ“'),
1559
+ m('span', 'Edit Text / Code')
1560
+ ]) : null,
1561
+ selectedItem.type === 'image' && selectedItem.publicUrl && !selectedItem.name.endsWith('.svg') ? m('button.px-3.py-1.5.text-xs.font-medium.rounded-lg.bg-indigo-50.dark:bg-indigo-950/40.text-indigo-600.dark:text-indigo-400.hover:bg-indigo-100.dark:hover:bg-indigo-900/60.border.border-indigo-200.dark:border-indigo-800.flex.items-center.gap-1.5.transition', {
1562
+ onclick: function() { openImageEditor(selectedItem); }
1563
+ }, [
1564
+ m('span', 'βœ‚οΈ'),
1565
+ m('span', 'Edit Image')
1566
+ ]) : null,
1567
+ m('button.text-gray-400.hover:text-gray-600.dark:hover:text-slate-200.text-lg.p-1.rounded-md', {
1568
+ onclick: function() { modalState = null; }
1569
+ }, 'βœ•')
1570
+ ])
513
1571
  ]),
514
1572
  // Media Viewer Area
515
- m('div.p-6.bg-gray-50.dark:bg-slate-900/50.flex.items-center.justify-center.min-h-[240px]', [
1573
+ m('div.flex-1.p-6.flex.items-center.justify-center.min-h-[220px].max-h-[420px].overflow-hidden', {
1574
+ style: selectedItem.type === 'image' ? 'background-image: radial-gradient(rgba(148, 163, 184, 0.25) 1px, transparent 1px); background-size: 16px 16px; background-color: rgba(248, 250, 252, 0.6);' : ''
1575
+ }, [
516
1576
  selectedItem.type === 'image' && selectedItem.publicUrl
517
- ? m('img.max-h-96.rounded-lg.object-contain.shadow-sm', { src: selectedItem.publicUrl })
1577
+ ? m('img.max-h-[360px].max-w-full.w-auto.h-auto.rounded-lg.object-contain.shadow-sm.border.border-gray-200/50.dark:border-slate-700/50', {
1578
+ src: selectedItem.publicUrl,
1579
+ onload: function(e) {
1580
+ loadedImageMeta[selectedItem.path] = { width: e.target.naturalWidth, height: e.target.naturalHeight };
1581
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
1582
+ }
1583
+ })
518
1584
  : (selectedItem.type === 'video' && selectedItem.publicUrl
519
- ? m('video.max-h-96.rounded-lg', { src: selectedItem.publicUrl, controls: true })
1585
+ ? m('video.max-h-[360px].max-w-full.rounded-lg', { src: selectedItem.publicUrl, controls: true })
520
1586
  : (selectedItem.type === 'audio' && selectedItem.publicUrl
521
- ? m('audio.w-full', { src: selectedItem.publicUrl, controls: true })
522
- : m('div.text-center.p-8', [
523
- renderFileIcon(selectedItem),
524
- m('p.text-xs.text-gray-500.mt-2', 'Preview not available')
525
- ])))
1587
+ ? m('audio.w-full.max-w-md', { src: selectedItem.publicUrl, controls: true })
1588
+ : (isTextFile(selectedItem)
1589
+ ? m('div.flex.flex-col.items-center.justify-center.p-8.text-center', [
1590
+ m('div.w-16.h-16.rounded-2xl.bg-indigo-50.dark:bg-slate-700/60.flex.items-center.justify-center.mb-3', renderFileIcon(selectedItem)),
1591
+ m('p.text-sm.font-bold.text-gray-900.dark:text-slate-100', selectedItem.name),
1592
+ m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', 'Text / Configuration / Code File (' + (selectedItem.ext || 'text').toUpperCase() + ')'),
1593
+ m('button.mt-4.px-4.py-2.bg-indigo-600.hover:bg-indigo-700.text-white.text-xs.font-semibold.rounded-xl.shadow-sm.flex.items-center.gap-2.transition', {
1594
+ onclick: function() { openTextEditor(selectedItem); }
1595
+ }, [
1596
+ m('span', 'πŸ“'),
1597
+ m('span', 'Open in Code Editor')
1598
+ ])
1599
+ ])
1600
+ : m('div.text-center.p-8', [
1601
+ renderFileIcon(selectedItem),
1602
+ m('p.text-xs.text-gray-500.mt-2', 'Preview not available')
1603
+ ]))))
526
1604
  ]),
527
1605
  // File Info Pane
528
- m('div.p-4.space-y-2.text-xs.text-gray-600.dark:text-slate-300.border-t.border-gray-200.dark:border-slate-700', [
529
- m('div.grid.grid-cols-2.gap-2', [
530
- m('p', [m('span.text-gray-400', 'Size: '), formatBytes(selectedItem.size)]),
531
- m('p', [m('span.text-gray-400', 'Type: '), selectedItem.mimeType || selectedItem.ext]),
532
- m('p', [m('span.text-gray-400', 'Date: '), formatDate(selectedItem.mtime)]),
533
- m('p.truncate', [m('span.text-gray-400', 'Path: '), selectedItem.path]),
1606
+ m('div.p-4.space-y-3.text-xs.text-gray-600.dark:text-slate-300.border-t.border-gray-200.dark:border-slate-700.bg-white.dark:bg-slate-800', [
1607
+ m('div.grid.grid-cols-2.md:grid-cols-4.gap-2.text-[11px]', [
1608
+ m('div.bg-gray-50.dark:bg-slate-900.p-2.rounded-lg.border.border-gray-100.dark:border-slate-700', [
1609
+ m('span.text-gray-400.block', 'File Size'),
1610
+ m('strong.text-gray-800.dark:text-slate-200', formatBytes(selectedItem.size))
1611
+ ]),
1612
+ m('div.bg-gray-50.dark:bg-slate-900.p-2.rounded-lg.border.border-gray-100.dark:border-slate-700', [
1613
+ m('span.text-gray-400.block', 'Dimensions'),
1614
+ m('strong.text-gray-800.dark:text-slate-200', loadedImageMeta[selectedItem.path] ? loadedImageMeta[selectedItem.path].width + ' Γ— ' + loadedImageMeta[selectedItem.path].height + ' px' : 'β€”')
1615
+ ]),
1616
+ m('div.bg-gray-50.dark:bg-slate-900.p-2.rounded-lg.border.border-gray-100.dark:border-slate-700', [
1617
+ m('span.text-gray-400.block', 'Format'),
1618
+ m('strong.text-gray-800.dark:text-slate-200', selectedItem.mimeType || selectedItem.ext.toUpperCase())
1619
+ ]),
1620
+ m('div.bg-gray-50.dark:bg-slate-900.p-2.rounded-lg.border.border-gray-100.dark:border-slate-700', [
1621
+ m('span.text-gray-400.block', 'Last Modified'),
1622
+ m('strong.text-gray-800.dark:text-slate-200', formatDate(selectedItem.mtime))
1623
+ ]),
534
1624
  ]),
535
- selectedItem.publicUrl ? m('div.flex.items-center.gap-2.pt-2', [
536
- m('input.flex-1.px-2.py-1.text-xs.border.rounded.bg-gray-50.dark:bg-slate-900.text-gray-700.dark:text-slate-300', {
1625
+ selectedItem.publicUrl ? m('div.flex.items-center.gap-2.pt-1', [
1626
+ m('input.flex-1.px-2.5.py-1.5.text-xs.border.border-gray-200.dark:border-slate-700.rounded-lg.bg-gray-50.dark:bg-slate-900.text-gray-700.dark:text-slate-300', {
537
1627
  readonly: true,
538
1628
  value: selectedItem.publicUrl
539
1629
  }),
540
- m('button.px-3.py-1.bg-indigo-600.text-white.rounded.font-medium.hover:bg-indigo-700', {
1630
+ m('button.px-3.5.py-1.5.bg-indigo-600.text-white.rounded-lg.font-medium.hover:bg-indigo-700.transition', {
541
1631
  onclick: function() {
542
1632
  copyToClipboard(selectedItem.publicUrl);
543
- alert('URL copied!');
1633
+ alert('URL copied to clipboard!');
544
1634
  }
545
- }, 'Copy'),
546
- isPickerMode && onSelectCallback ? m('button.px-3.py-1.bg-emerald-600.text-white.rounded.font-medium.hover:bg-emerald-700', {
1635
+ }, 'Copy URL'),
1636
+ isPickerMode && onSelectCallback ? m('button.px-3.5.py-1.5.bg-emerald-600.text-white.rounded-lg.font-medium.hover:bg-emerald-700.transition', {
547
1637
  onclick: function() {
548
1638
  onSelectCallback(selectedItem.publicUrl);
549
1639
  modalState = null;
@@ -552,6 +1642,392 @@ function generateFileManagerComponent(options = {}) {
552
1642
  ]) : null
553
1643
  ])
554
1644
  ])
1645
+ ]) : null,
1646
+
1647
+ // 5. Image Editor Modal
1648
+ editMode && selectedItem ? m('div.fixed.inset-0.bg-black/75.flex.items-center.justify-center.z-50.p-4.backdrop-blur-sm', {
1649
+ onclick: function() { if (!editorState.saving) editMode = false; }
1650
+ }, [
1651
+ m('div.bg-white.dark:bg-slate-800.rounded-2xl.shadow-2xl.w-full.max-w-5xl.max-h-[90vh].flex.flex-col.overflow-hidden.border.border-gray-200.dark:border-slate-700', {
1652
+ onclick: function(e) { e.stopPropagation(); }
1653
+ }, [
1654
+ // Header
1655
+ m('div.flex.items-center.justify-between.px-6.py-3.5.border-b.border-gray-200.dark:border-slate-700.bg-gray-50/70.dark:bg-slate-800', [
1656
+ m('div.flex.items-center.gap-2.truncate', [
1657
+ m('span.text-base', '🎨'),
1658
+ m('h3.text-sm.font-bold.text-gray-900.dark:text-slate-100.truncate', 'Edit Image: ' + selectedItem.name),
1659
+ editorState.naturalWidth ? m('span.text-xs.text-gray-500.dark:text-slate-400', '(' + editorState.naturalWidth + ' Γ— ' + editorState.naturalHeight + ' px)') : null,
1660
+ ]),
1661
+ m('button.text-gray-400.hover:text-gray-600.dark:hover:text-slate-200.text-lg', {
1662
+ disabled: editorState.saving,
1663
+ onclick: function() { editMode = false; }
1664
+ }, 'βœ•')
1665
+ ]),
1666
+
1667
+ // Body: 2 Columns (Canvas/Preview on Left, Controls on Right)
1668
+ m('div.flex-1.grid.grid-cols-1.lg:grid-cols-3.overflow-y-auto', [
1669
+ // Left Column: Interactive Canvas & Preview
1670
+ m('div.lg:col-span-2.p-6.flex.flex-col.items-center.justify-center.border-b.lg:border-b-0.lg:border-r.border-gray-200.dark:border-slate-700', {
1671
+ style: 'background-image: radial-gradient(rgba(148, 163, 184, 0.3) 1px, transparent 1px); background-size: 16px 16px; background-color: rgba(248, 250, 252, 0.5);'
1672
+ }, [
1673
+ m('div.relative.max-w-full.flex.items-center.justify-center.overflow-hidden.rounded-xl.shadow-md.border.border-gray-200.dark:border-slate-700.bg-white.dark:bg-slate-900', {
1674
+ style: 'max-height: 420px;'
1675
+ }, [
1676
+ m('img.max-h-[380px].max-w-full.w-auto.h-auto.object-contain.transition-all.duration-200', {
1677
+ src: selectedItem.publicUrl,
1678
+ style: 'transform: rotate(' + editorState.rotation + 'deg) scale(' + (editorState.flipH ? -1 : 1) + ', ' + (editorState.flipV ? -1 : 1) + ');'
1679
+ }),
1680
+ // Crop frame overlay if aspect ratio active
1681
+ editorState.aspectRatio !== 'free' ? m('div.absolute.inset-4.border-2.border-indigo-500.border-dashed.rounded-lg.pointer-events-none.flex.items-center.justify-center.bg-indigo-500/10', [
1682
+ m('span.text-xs.font-bold.px-2.py-1.rounded.bg-indigo-600.text-white.shadow', editorState.aspectRatio)
1683
+ ]) : null
1684
+ ]),
1685
+ m('div.mt-3.text-xs.text-gray-500.dark:text-slate-400.text-center', [
1686
+ m('span', 'Rotation: ' + editorState.rotation + 'Β° | Flip: ' + (editorState.flipH ? 'H ' : '') + (editorState.flipV ? 'V' : (editorState.flipH ? '' : 'None'))),
1687
+ editorState.aspectRatio !== 'free' ? m('span.ml-2.font-semibold.text-indigo-600', '| Crop: ' + editorState.aspectRatio) : null
1688
+ ])
1689
+ ]),
1690
+
1691
+ // Right Column: Tools & Actions
1692
+ m('div.p-6.space-y-5.bg-white.dark:bg-slate-800/80', [
1693
+ // 1. Aspect Ratio Presets
1694
+ m('div.space-y-2', [
1695
+ m('label.text-xs.font-bold.uppercase.tracking-wider.text-gray-500.dark:text-slate-400.block', 'Aspect Ratio (KΔ±rpma)'),
1696
+ m('div.grid.grid-cols-2.gap-1.5', [
1697
+ ['free', 'Free / Serbest'],
1698
+ ['1:1', '1:1 Square'],
1699
+ ['16:9', '16:9 Banner'],
1700
+ ['4:3', '4:3 Standard']
1701
+ ].map(function(item) {
1702
+ var active = editorState.aspectRatio === item[0];
1703
+ return m('button.px-2.py-1.5.text-xs.rounded-lg.border.font-medium.transition', {
1704
+ class: active
1705
+ ? 'bg-indigo-600 text-white border-indigo-600'
1706
+ : 'bg-gray-50 dark:bg-slate-900 text-gray-700 dark:text-slate-300 border-gray-200 dark:border-slate-700 hover:bg-gray-100',
1707
+ onclick: function() { setAspectRatio(item[0]); }
1708
+ }, item[1]);
1709
+ }))
1710
+ ]),
1711
+
1712
+ // 2. Rotate & Flip Controls
1713
+ m('div.space-y-2', [
1714
+ m('label.text-xs.font-bold.uppercase.tracking-wider.text-gray-500.dark:text-slate-400.block', 'Transform (DΓΆndΓΌr & Γ‡evir)'),
1715
+ m('div.grid.grid-cols-2.gap-2', [
1716
+ m('button.px-3.py-2.text-xs.rounded-lg.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.hover:bg-gray-100.dark:hover:bg-slate-800.text-gray-800.dark:text-slate-200.flex.items-center.justify-center.gap-1.5', {
1717
+ onclick: function() { editorState.rotation = (editorState.rotation - 90 + 360) % 360; }
1718
+ }, [m('span', 'β†Ί'), m('span', '-90Β°')]),
1719
+ m('button.px-3.py-2.text-xs.rounded-lg.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.hover:bg-gray-100.dark:hover:bg-slate-800.text-gray-800.dark:text-slate-200.flex.items-center.justify-center.gap-1.5', {
1720
+ onclick: function() { editorState.rotation = (editorState.rotation + 90) % 360; }
1721
+ }, [m('span', '↻'), m('span', '+90Β°')]),
1722
+ m('button.px-3.py-2.text-xs.rounded-lg.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.hover:bg-gray-100.dark:hover:bg-slate-800.text-gray-800.dark:text-slate-200.flex.items-center.justify-center.gap-1.5', {
1723
+ class: editorState.flipH ? 'border-indigo-500 text-indigo-600 bg-indigo-50/50' : '',
1724
+ onclick: function() { editorState.flipH = !editorState.flipH; }
1725
+ }, [m('span', '⇄'), m('span', 'Flip Horiz')]),
1726
+ m('button.px-3.py-2.text-xs.rounded-lg.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.hover:bg-gray-100.dark:hover:bg-slate-800.text-gray-800.dark:text-slate-200.flex.items-center.justify-center.gap-1.5', {
1727
+ class: editorState.flipV ? 'border-indigo-500 text-indigo-600 bg-indigo-50/50' : '',
1728
+ onclick: function() { editorState.flipV = !editorState.flipV; }
1729
+ }, [m('span', 'β‡…'), m('span', 'Flip Vert')])
1730
+ ])
1731
+ ]),
1732
+
1733
+ // 3. Resize Controls
1734
+ m('div.space-y-2', [
1735
+ m('label.text-xs.font-bold.uppercase.tracking-wider.text-gray-500.dark:text-slate-400.block', 'Resize (Yeniden BoyutlandΔ±r)'),
1736
+ m('div.grid.grid-cols-2.gap-2', [
1737
+ m('div', [
1738
+ m('span.text-[10px].text-gray-400.block.mb-1', 'Width (px)'),
1739
+ m('input.w-full.px-2.5.py-1.5.text-xs.border.border-gray-200.dark:border-slate-700.rounded-lg.bg-gray-50.dark:bg-slate-900.text-gray-800.dark:text-slate-200', {
1740
+ type: 'number',
1741
+ value: editorState.targetWidth,
1742
+ oninput: function(e) {
1743
+ var w = parseInt(e.target.value, 10) || 1;
1744
+ editorState.targetWidth = w;
1745
+ if (editorState.lockRatio && editorState.naturalWidth) {
1746
+ editorState.targetHeight = Math.round((w * editorState.naturalHeight) / editorState.naturalWidth);
1747
+ }
1748
+ }
1749
+ })
1750
+ ]),
1751
+ m('div', [
1752
+ m('span.text-[10px].text-gray-400.block.mb-1', 'Height (px)'),
1753
+ m('input.w-full.px-2.5.py-1.5.text-xs.border.border-gray-200.dark:border-slate-700.rounded-lg.bg-gray-50.dark:bg-slate-900.text-gray-800.dark:text-slate-200', {
1754
+ type: 'number',
1755
+ value: editorState.targetHeight,
1756
+ oninput: function(e) {
1757
+ var h = parseInt(e.target.value, 10) || 1;
1758
+ editorState.targetHeight = h;
1759
+ if (editorState.lockRatio && editorState.naturalHeight) {
1760
+ editorState.targetWidth = Math.round((h * editorState.naturalWidth) / editorState.naturalHeight);
1761
+ }
1762
+ }
1763
+ })
1764
+ ])
1765
+ ])
1766
+ ]),
1767
+
1768
+ // 4. Output Format
1769
+ m('div.space-y-2', [
1770
+ m('label.text-xs.font-bold.uppercase.tracking-wider.text-gray-500.dark:text-slate-400.block', 'Output Format'),
1771
+ m('select.w-full.px-2.5.py-1.5.text-xs.border.border-gray-200.dark:border-slate-700.rounded-lg.bg-gray-50.dark:bg-slate-900.text-gray-800.dark:text-slate-200', {
1772
+ value: editorState.format,
1773
+ onchange: function(e) { editorState.format = e.target.value; }
1774
+ }, [
1775
+ m('option', { value: 'original' }, 'Keep Original (' + selectedItem.ext.toUpperCase() + ')'),
1776
+ m('option', { value: 'image/webp' }, 'Convert to WebP (Recommended)'),
1777
+ m('option', { value: 'image/png' }, 'PNG'),
1778
+ m('option', { value: 'image/jpeg' }, 'JPEG')
1779
+ ])
1780
+ ]),
1781
+
1782
+ // 5. Actions: Save as Copy vs Overwrite
1783
+ m('div.pt-3.border-t.border-gray-200.dark:border-slate-700.space-y-2', [
1784
+ editorState.saveStatus ? m('p.text-xs.text-indigo-600.font-medium.animate-pulse', editorState.saveStatus) : null,
1785
+ m('div.flex.flex-col.gap-2', [
1786
+ m('button.w-full.py-2.px-4.bg-indigo-600.hover:bg-indigo-700.text-white.text-xs.font-semibold.rounded-xl.shadow-sm.transition.flex.items-center.justify-center.gap-1.5', {
1787
+ disabled: editorState.saving,
1788
+ onclick: function() { saveTransformedImage(false); }
1789
+ }, [
1790
+ m('span', 'πŸ’Ύ'),
1791
+ m('span', 'Save as New File')
1792
+ ]),
1793
+ m('button.w-full.py-2.px-4.bg-amber-600.hover:bg-amber-700.text-white.text-xs.font-semibold.rounded-xl.shadow-sm.transition.flex.items-center.justify-center.gap-1.5', {
1794
+ disabled: editorState.saving,
1795
+ onclick: function() {
1796
+ if (confirm('Are you sure you want to overwrite "' + selectedItem.name + '"?')) {
1797
+ saveTransformedImage(true);
1798
+ }
1799
+ }
1800
+ }, [
1801
+ m('span', '⚑'),
1802
+ m('span', 'Overwrite Original')
1803
+ ]),
1804
+ m('button.w-full.py-1.5.px-4.border.border-gray-300.dark:border-slate-600.text-gray-700.dark:text-slate-300.text-xs.rounded-xl.hover:bg-gray-100.dark:hover:bg-slate-700.transition', {
1805
+ disabled: editorState.saving,
1806
+ onclick: function() { editMode = false; }
1807
+ }, 'Cancel')
1808
+ ])
1809
+ ])
1810
+ ])
1811
+ ])
1812
+ ])
1813
+ ]) : null,
1814
+
1815
+ // 6. Text / Code Editor Modal
1816
+ textEditMode && textEditorState.file ? m('div.fixed.inset-0.bg-black/75.flex.items-center.justify-center.z-50.p-4.backdrop-blur-sm', {
1817
+ onclick: function() {
1818
+ if (textEditorState.saving) return;
1819
+ if (textEditorState.content !== textEditorState.initialContent) {
1820
+ if (!confirm('You have unsaved changes. Discard them?')) return;
1821
+ }
1822
+ textEditMode = false;
1823
+ }
1824
+ }, [
1825
+ m('div.bg-slate-900.text-slate-100.rounded-2xl.shadow-2xl.w-full.max-w-5xl.max-h-[90vh].flex.flex-col.overflow-hidden.border.border-slate-700', {
1826
+ onclick: function(e) { e.stopPropagation(); }
1827
+ }, [
1828
+ // Header
1829
+ m('div.flex.items-center.justify-between.px-6.py-3.border-b.border-slate-800.bg-slate-950/80', [
1830
+ m('div.flex.items-center.gap-2.5.truncate', [
1831
+ m('span.text-base', textEditorState.isJson ? 'βš™οΈ' : 'πŸ“'),
1832
+ m('h3.text-sm.font-bold.text-slate-100.truncate', textEditorState.file.name),
1833
+ m('span.text-[10px].uppercase.font-bold.px-2.py-0.5.rounded.bg-indigo-900/60.text-indigo-300.border.border-indigo-700/50', textEditorState.file.ext || 'TEXT'),
1834
+ textEditorState.content !== textEditorState.initialContent
1835
+ ? m('span.text-[10px].font-semibold.px-2.py-0.5.rounded-full.bg-amber-500/20.text-amber-400.border.border-amber-500/40.animate-pulse', '● Unsaved')
1836
+ : m('span.text-[10px].font-semibold.px-2.py-0.5.rounded-full.bg-emerald-500/10.text-emerald-400', 'Saved'),
1837
+ textEditorState.isJson
1838
+ ? (textEditorState.jsonError
1839
+ ? m('span.text-[10px].font-semibold.px-2.py-0.5.rounded.bg-red-500/20.text-red-400.truncate.max-w-xs', { title: textEditorState.jsonError }, '⚠️ Invalid JSON')
1840
+ : m('span.text-[10px].font-semibold.px-2.py-0.5.rounded.bg-emerald-500/20.text-emerald-400', 'βœ“ Valid JSON'))
1841
+ : null
1842
+ ]),
1843
+ // Header Actions
1844
+ m('div.flex.items-center.gap-2', [
1845
+ textEditorState.isJson ? m('button.px-2.5.py-1.text-xs.rounded-lg.bg-indigo-600/30.hover:bg-indigo-600/50.text-indigo-200.border.border-indigo-500/40.flex.items-center.gap-1.transition', {
1846
+ onclick: formatJsonContent,
1847
+ title: 'Beautify / Format JSON with 2-space indentation'
1848
+ }, [
1849
+ m('span', '✨'),
1850
+ m('span', 'Format JSON')
1851
+ ]) : null,
1852
+ m('button.px-2.py-1.text-xs.rounded-lg.bg-slate-800.hover:bg-slate-700.text-slate-300.border.border-slate-700.transition', {
1853
+ onclick: function() { textEditorState.wordWrap = !textEditorState.wordWrap; },
1854
+ title: 'Toggle Word Wrap'
1855
+ }, textEditorState.wordWrap ? 'Wrap: On' : 'Wrap: Off'),
1856
+ m('div.flex.items-center.border.border-slate-700.rounded-lg.overflow-hidden', [
1857
+ m('button.px-2.py-1.text-xs.bg-slate-800.hover:bg-slate-700.text-slate-300', {
1858
+ onclick: function() { textEditorState.fontSize = Math.max(10, textEditorState.fontSize - 1); },
1859
+ title: 'Decrease font size'
1860
+ }, 'A-'),
1861
+ m('span.px-2.py-1.text-[10px].bg-slate-900.text-slate-400', textEditorState.fontSize + 'px'),
1862
+ m('button.px-2.py-1.text-xs.bg-slate-800.hover:bg-slate-700.text-slate-300', {
1863
+ onclick: function() { textEditorState.fontSize = Math.min(22, textEditorState.fontSize + 1); },
1864
+ title: 'Increase font size'
1865
+ }, 'A+'),
1866
+ ]),
1867
+ m('button.text-slate-400.hover:text-slate-100.text-lg.p-1.rounded-md', {
1868
+ disabled: textEditorState.saving,
1869
+ onclick: function() {
1870
+ if (textEditorState.content !== textEditorState.initialContent) {
1871
+ if (!confirm('You have unsaved changes. Discard them?')) return;
1872
+ }
1873
+ textEditMode = false;
1874
+ }
1875
+ }, 'βœ•')
1876
+ ])
1877
+ ]),
1878
+
1879
+ // Editor Body: Loading / Error / Textarea
1880
+ m('div.flex-1.relative.flex.overflow-hidden.bg-slate-950', [
1881
+ textEditorState.loading ? m('div.w-full.flex.flex-col.items-center.justify-center.py-24.text-slate-400', [
1882
+ m('div.w-8.h-8.border-2.border-indigo-500.border-t-transparent.rounded-full.animate-spin.mb-2'),
1883
+ m('p.text-xs', 'Loading file content…')
1884
+ ]) : (textEditorState.error ? m('div.w-full.p-8.text-center.text-red-400.text-sm', textEditorState.error) : [
1885
+ // Textarea with line numbers gutter
1886
+ m('div.flex.w-full.h-full.min-h-[420px]', [
1887
+ // Line numbers gutter
1888
+ m('div.py-3.px-3.bg-slate-900/60.text-slate-500.text-right.select-none.font-mono.border-r.border-slate-800/80', {
1889
+ style: 'font-size: ' + textEditorState.fontSize + 'px; line-height: 1.5; min-width: 44px;'
1890
+ }, (function() {
1891
+ var lines = [];
1892
+ var count = Math.max(1, textEditorState.lineCount);
1893
+ for (var i = 1; i <= count; i++) {
1894
+ lines.push(m('div', String(i)));
1895
+ }
1896
+ return lines;
1897
+ })()),
1898
+ // Real textarea
1899
+ m('textarea.w-full.h-full.p-3.bg-transparent.text-slate-100.font-mono.resize-none.outline-none.focus:ring-0.border-none', {
1900
+ value: textEditorState.content,
1901
+ spellcheck: false,
1902
+ style: 'font-size: ' + textEditorState.fontSize + 'px; line-height: 1.5; white-space: ' + (textEditorState.wordWrap ? 'pre-wrap' : 'pre') + '; tab-size: 2;',
1903
+ oninput: function(e) {
1904
+ textEditorState.content = e.target.value;
1905
+ updateTextStats();
1906
+ },
1907
+ onkeydown: function(e) {
1908
+ if (e.key === 'Tab') {
1909
+ e.preventDefault();
1910
+ var start = e.target.selectionStart;
1911
+ var end = e.target.selectionEnd;
1912
+ var val = e.target.value;
1913
+ e.target.value = val.substring(0, start) + ' ' + val.substring(end);
1914
+ e.target.selectionStart = e.target.selectionEnd = start + 2;
1915
+ textEditorState.content = e.target.value;
1916
+ updateTextStats();
1917
+ } else if ((e.ctrlKey || e.metaKey) && e.key === 's') {
1918
+ e.preventDefault();
1919
+ saveTextContent(true);
1920
+ }
1921
+ }
1922
+ })
1923
+ ])
1924
+ ])
1925
+ ]),
1926
+
1927
+ // Footer: Status Bar & Save Actions
1928
+ m('div.flex.items-center.justify-between.px-6.py-3.border-t.border-slate-800.bg-slate-900', [
1929
+ // Left stats
1930
+ m('div.flex.items-center.gap-4.text-xs.text-slate-400', [
1931
+ m('span', 'Lines: ' + textEditorState.lineCount),
1932
+ m('span', 'Chars: ' + textEditorState.charCount),
1933
+ m('span', 'Encoding: UTF-8'),
1934
+ textEditorState.saveStatus ? m('span.text-indigo-400.font-medium.animate-pulse', textEditorState.saveStatus) : null
1935
+ ]),
1936
+ // Right buttons
1937
+ m('div.flex.items-center.gap-2', [
1938
+ m('button.px-3.py-1.5.text-xs.font-medium.rounded-xl.border.border-slate-700.hover:bg-slate-800.text-slate-300.transition', {
1939
+ disabled: textEditorState.saving,
1940
+ onclick: function() {
1941
+ if (textEditorState.content !== textEditorState.initialContent) {
1942
+ if (!confirm('You have unsaved changes. Discard them?')) return;
1943
+ }
1944
+ textEditMode = false;
1945
+ }
1946
+ }, 'Close'),
1947
+ m('button.px-3.py-1.5.text-xs.font-semibold.rounded-xl.bg-slate-800.hover:bg-slate-700.text-slate-200.border.border-slate-600.flex.items-center.gap-1.5.transition', {
1948
+ disabled: textEditorState.saving || textEditorState.loading,
1949
+ onclick: function() { saveTextContent(false); }
1950
+ }, [
1951
+ m('span', 'πŸ“‹'),
1952
+ m('span', 'Save as Copy')
1953
+ ]),
1954
+ m('button.px-4.py-1.5.text-xs.font-semibold.rounded-xl.bg-indigo-600.hover:bg-indigo-500.text-white.shadow-sm.flex.items-center.gap-1.5.transition', {
1955
+ disabled: textEditorState.saving || textEditorState.loading,
1956
+ onclick: function() { saveTextContent(true); }
1957
+ }, [
1958
+ m('span', textEditorState.saving ? '⏳' : 'πŸ’Ύ'),
1959
+ m('span', textEditorState.saving ? 'Saving…' : 'Save Changes')
1960
+ ])
1961
+ ])
1962
+ ])
1963
+ ])
1964
+ ]) : null,
1965
+
1966
+ // KEYBOARD SHORTCUTS CHEATSHEET MODAL
1967
+ showShortcutsModal ? m('div.fixed.inset-0.bg-black/60.flex.items-center.justify-center.z-50.p-4.backdrop-blur-sm', {
1968
+ onclick: function() { showShortcutsModal = false; }
1969
+ }, [
1970
+ m('div.bg-white.dark:bg-slate-900.rounded-2xl.shadow-2xl.w-full.max-w-xl.overflow-hidden.border.border-gray-200.dark:border-slate-800.transition-all', {
1971
+ onclick: function(e) { e.stopPropagation(); }
1972
+ }, [
1973
+ // Header
1974
+ m('div.flex.items-center.justify-between.px-6.py-4.border-b.border-gray-100.dark:border-slate-800.bg-gray-50/60.dark:bg-slate-800/50', [
1975
+ m('div.flex.items-center.gap-2.5', [
1976
+ m('span.text-xl', '⌨️'),
1977
+ m('div', [
1978
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', 'Keyboard Shortcuts'),
1979
+ m('p.text-xs.text-gray-500.dark:text-slate-400', 'Navigate and manage files without touching your mouse')
1980
+ ])
1981
+ ]),
1982
+ m('button.text-gray-400.hover:text-gray-600.dark:hover:text-slate-200.p-1.5.rounded-lg.hover:bg-gray-100.dark:hover:bg-slate-800', {
1983
+ onclick: function() { showShortcutsModal = false; }
1984
+ }, 'βœ•')
1985
+ ]),
1986
+
1987
+ // Body: Shortcut Categories
1988
+ m('div.p-6.space-y-5.max-h-[70vh].overflow-y-auto', [
1989
+ // Group 1: Navigation
1990
+ m('div.space-y-2.5', [
1991
+ m('h4.text-xs.font-bold.text-indigo-600.dark:text-indigo-400.uppercase.tracking-wider', 'Navigation & Browsing'),
1992
+ m('div.grid.grid-cols-1.sm:grid-cols-2.gap-2', [
1993
+ renderShortcutItem('↑ ↓ ← β†’', 'Move focus between items'),
1994
+ renderShortcutItem('Enter', 'Open folder / preview file'),
1995
+ renderShortcutItem('Space', 'Finder Quick Look (preview)'),
1996
+ renderShortcutItem('Alt + ↑', 'Enclosing parent folder')
1997
+ ])
1998
+ ]),
1999
+
2000
+ // Group 2: Selection & Batch
2001
+ m('div.space-y-2.5', [
2002
+ m('h4.text-xs.font-bold.text-indigo-600.dark:text-indigo-400.uppercase.tracking-wider', 'Selection & Batch Actions'),
2003
+ m('div.grid.grid-cols-1.sm:grid-cols-2.gap-2', [
2004
+ renderShortcutItem('Cmd / Ctrl + A', 'Select all files/folders'),
2005
+ renderShortcutItem('X', 'Toggle select focused item'),
2006
+ renderShortcutItem('Esc', 'Clear selection / close modal'),
2007
+ renderShortcutItem('Delete', 'Delete selected / focused item')
2008
+ ])
2009
+ ]),
2010
+
2011
+ // Group 3: Quick Tools & Editors
2012
+ m('div.space-y-2.5', [
2013
+ m('h4.text-xs.font-bold.text-indigo-600.dark:text-indigo-400.uppercase.tracking-wider', 'Quick Tools & Actions'),
2014
+ m('div.grid.grid-cols-1.sm:grid-cols-2.gap-2', [
2015
+ renderShortcutItem('E', 'Edit code/text or image'),
2016
+ renderShortcutItem('R', 'Rename focused item'),
2017
+ renderShortcutItem('/', 'Focus search input'),
2018
+ renderShortcutItem('? or H', 'Toggle shortcuts help')
2019
+ ])
2020
+ ])
2021
+ ]),
2022
+
2023
+ // Footer
2024
+ m('div.flex.items-center.justify-between.px-6.py-3.border-t.border-gray-100.dark:border-slate-800.bg-gray-50.dark:bg-slate-800/70', [
2025
+ m('span.text-xs.text-gray-400.dark:text-slate-500', 'Press Esc to close at any time'),
2026
+ m('button.px-4.py-1.5.text-xs.font-semibold.rounded-xl.bg-indigo-600.hover:bg-indigo-500.text-white.shadow-sm.transition', {
2027
+ onclick: function() { showShortcutsModal = false; }
2028
+ }, 'Got It')
2029
+ ])
2030
+ ])
555
2031
  ]) : null
556
2032
  ]);
557
2033
  }