mbkbucket 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/index.js +7 -7
  2. package/{cli.js → lib/cli.js} +0 -1
  3. package/lib/public/mbkbucket-helper-demo.html +1177 -0
  4. package/lib/public/mbkbucket-helper.css +1235 -0
  5. package/lib/public/mbkbucket-helper.js +1550 -0
  6. package/{src → lib/src}/app.js +7 -0
  7. package/{src → lib/src}/config/index.js +1 -1
  8. package/{src → lib/src}/services/s3.service.js +3 -2
  9. package/package.json +13 -3
  10. package/.github/PACKAGE.md +0 -19
  11. package/.github/workflows/publish.yml +0 -88
  12. package/index.d.ts +0 -444
  13. package/lib/index.js +0 -17
  14. package/test/config-validation.test.js +0 -116
  15. /package/{public → lib/public}/bucketadmin.css +0 -0
  16. /package/{public → lib/public}/bucketadmin.js +0 -0
  17. /package/{src → lib/src}/cli/auth-helper.js +0 -0
  18. /package/{src → lib/src}/config/validation.js +0 -0
  19. /package/{src → lib/src}/controllers/api.controller.js +0 -0
  20. /package/{src → lib/src}/controllers/dashboard.controller.js +0 -0
  21. /package/{src → lib/src}/controllers/info.controller.js +0 -0
  22. /package/{src → lib/src}/controllers/view.controller.js +0 -0
  23. /package/{src → lib/src}/middleware/async-handler.js +0 -0
  24. /package/{src → lib/src}/middleware/bucket-resolver.js +0 -0
  25. /package/{src → lib/src}/middleware/error-handler.js +0 -0
  26. /package/{src → lib/src}/middleware/rate-limiter.js +0 -0
  27. /package/{src → lib/src}/middleware/security.js +0 -0
  28. /package/{src → lib/src}/routes/api.routes.js +0 -0
  29. /package/{src → lib/src}/routes/index.js +0 -0
  30. /package/{src → lib/src}/routes/info.routes.js +0 -0
  31. /package/{src → lib/src}/routes/view.routes.js +0 -0
  32. /package/{src → lib/src}/utils/errors.js +0 -0
  33. /package/{src → lib/src}/utils/helpers.js +0 -0
  34. /package/{src → lib/src}/utils/logger.js +0 -0
  35. /package/{views → lib/views}/bucket.handlebars +0 -0
  36. /package/{views → lib/views}/bucketadmin_alerts.handlebars +0 -0
  37. /package/{views → lib/views}/bucketadmin_delete_modal.handlebars +0 -0
  38. /package/{views → lib/views}/bucketadmin_filelist_skeleton.handlebars +0 -0
  39. /package/{views → lib/views}/bucketadmin_newfolder_modal.handlebars +0 -0
  40. /package/{views → lib/views}/bucketadmin_preview_modal.handlebars +0 -0
  41. /package/{views → lib/views}/bucketadmin_upload_modal.handlebars +0 -0
  42. /package/{views → lib/views}/bucketadmincontent.handlebars +0 -0
  43. /package/{views → lib/views}/bucketportal.handlebars +0 -0
  44. /package/{views → lib/views}/mbkbucket_info.handlebars +0 -0
@@ -0,0 +1,1177 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Helper Utils Demo — MBKBucket</title>
7
+ <meta name="description" content="Interactive demo of MBKBucket frontend helper utilities: gallery picker, file input, upload widget, multi-select, live preview binding, and API helpers.">
8
+
9
+ <!-- Fonts (same as bucket.handlebars) -->
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
13
+
14
+ <!-- MBKBucket design system + helper styles -->
15
+ <link rel="stylesheet" href="/mbkbucket/bucketadmin.css">
16
+ <link rel="stylesheet" href="/mbkbucket/mbkbucket-helper.css">
17
+
18
+ <style>
19
+ /* ── Page shell (mirrors bucket.handlebars body) ───────────── */
20
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
21
+
22
+ body {
23
+ font-family: var(--font);
24
+ font-size: 14px;
25
+ line-height: 1.6;
26
+ color: var(--text);
27
+ background:
28
+ radial-gradient(circle at 12% 14%, rgba(16, 185, 129, 0.1), transparent 44%),
29
+ radial-gradient(circle at 88% 8%, rgba(15, 118, 110, 0.08), transparent 40%),
30
+ linear-gradient(160deg, #eef7f2 0%, #f8fbf7 52%, #eaf2ef 100%);
31
+ min-height: 100vh;
32
+ overflow-x: hidden;
33
+ }
34
+
35
+ /* Ambient blobs */
36
+ body::before, body::after {
37
+ content: '';
38
+ position: fixed;
39
+ border-radius: 50%;
40
+ pointer-events: none;
41
+ z-index: 0;
42
+ }
43
+ body::before {
44
+ width: 280px; height: 280px;
45
+ top: 10vh; left: -80px;
46
+ background: radial-gradient(circle, rgba(15,118,110,0.14) 0, transparent 70%);
47
+ }
48
+ body::after {
49
+ width: 320px; height: 320px;
50
+ bottom: 8vh; right: -100px;
51
+ background: radial-gradient(circle, rgba(16,185,129,0.1) 0, transparent 70%);
52
+ }
53
+
54
+ /* ── Top nav ───────────────────────────────────────────────── */
55
+ .top-nav {
56
+ position: sticky;
57
+ top: 0;
58
+ z-index: 200;
59
+ background: rgba(255,255,255,0.85);
60
+ backdrop-filter: blur(12px);
61
+ -webkit-backdrop-filter: blur(12px);
62
+ border-bottom: 1px solid rgba(255,255,255,0.75);
63
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -1px rgba(0,0,0,0.03);
64
+ padding: 0 1.5rem;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: space-between;
68
+ height: 56px;
69
+ gap: 1rem;
70
+ }
71
+
72
+ .nav-logo {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 8px;
76
+ font-family: var(--font-headings);
77
+ font-size: 1.1rem;
78
+ font-weight: 700;
79
+ color: var(--primary-dark-color-ml);
80
+ text-decoration: none;
81
+ }
82
+
83
+ .nav-logo-icon {
84
+ width: 30px; height: 30px;
85
+ background: var(--primary-color-ml);
86
+ border-radius: 8px;
87
+ display: flex; align-items: center; justify-content: center;
88
+ color: #fff;
89
+ flex-shrink: 0;
90
+ }
91
+
92
+ .nav-badge {
93
+ font-family: var(--font);
94
+ font-size: 10.5px;
95
+ font-weight: 700;
96
+ letter-spacing: 0.04em;
97
+ padding: 2px 8px;
98
+ background: var(--primary-light-color-ml);
99
+ color: var(--primary-dark-color-ml);
100
+ border-radius: 20px;
101
+ border: 1px solid rgba(15,118,110,0.2);
102
+ }
103
+
104
+ .nav-links {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 2px;
108
+ }
109
+
110
+ .nav-link {
111
+ padding: 5px 12px;
112
+ border-radius: var(--radius-sm);
113
+ font-size: 13px;
114
+ font-weight: 500;
115
+ color: var(--text-muted);
116
+ text-decoration: none;
117
+ transition: all 0.15s ease;
118
+ }
119
+ .nav-link:hover { background: var(--primary-light-color-ml); color: var(--primary-dark-color-ml); }
120
+ .nav-link.active { background: var(--primary-light-color-ml); color: var(--primary-dark-color-ml); font-weight: 600; }
121
+
122
+ .nav-back {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ gap: 5px;
126
+ padding: 5px 12px;
127
+ border-radius: var(--radius-sm);
128
+ font-size: 13px;
129
+ font-weight: 500;
130
+ color: var(--primary-color-ml);
131
+ text-decoration: none;
132
+ border: 1px solid var(--border-color);
133
+ background: rgba(255,255,255,0.8);
134
+ transition: all 0.15s ease;
135
+ }
136
+ .nav-back:hover { background: var(--primary-light-color-ml); border-color: var(--primary-color-ml); }
137
+
138
+ /* ── Page layout ───────────────────────────────────────────── */
139
+ .page-wrap {
140
+ position: relative;
141
+ z-index: 1;
142
+ max-width: 1020px;
143
+ margin: 0 auto;
144
+ padding: 28px 24px 72px;
145
+ }
146
+
147
+ /* ── Page header ───────────────────────────────────────────── */
148
+ .demo-page-header {
149
+ display: flex;
150
+ align-items: flex-start;
151
+ justify-content: space-between;
152
+ gap: 16px;
153
+ margin-bottom: 28px;
154
+ padding: 18px 22px;
155
+ background: rgba(255,255,255,0.74);
156
+ border: 1px solid rgba(255,255,255,0.9);
157
+ border-radius: var(--radius-lg);
158
+ backdrop-filter: blur(10px);
159
+ box-shadow: 0 12px 34px rgba(15,23,42,0.05);
160
+ }
161
+
162
+ .demo-page-pretitle {
163
+ font-size: 11px;
164
+ font-weight: 700;
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.6px;
167
+ color: var(--primary-color-ml);
168
+ margin-bottom: 3px;
169
+ }
170
+
171
+ .demo-page-title {
172
+ font-family: var(--font-headings);
173
+ font-size: 1.5rem;
174
+ font-weight: 700;
175
+ color: var(--dark-color-ml);
176
+ line-height: 1.2;
177
+ margin: 0;
178
+ }
179
+
180
+ .demo-page-desc {
181
+ font-size: 13px;
182
+ color: var(--text-muted);
183
+ margin-top: 5px;
184
+ max-width: 540px;
185
+ }
186
+
187
+ .demo-include-snippet {
188
+ background: var(--light-color-ml);
189
+ border: 1px solid var(--border-color);
190
+ border-radius: var(--radius-sm);
191
+ padding: 10px 14px;
192
+ font-family: var(--font-mono);
193
+ font-size: 11.5px;
194
+ color: var(--primary-dark-color-ml);
195
+ white-space: nowrap;
196
+ flex-shrink: 0;
197
+ }
198
+ .demo-include-snippet .kw { color: var(--primary-color-ml); font-weight: 600; }
199
+ .demo-include-snippet .str { color: var(--purple); }
200
+ .demo-include-snippet .line { display: block; line-height: 1.8; }
201
+
202
+ /* ── Section spacing ───────────────────────────────────────── */
203
+ .demo-sections {
204
+ display: flex;
205
+ flex-direction: column;
206
+ gap: 28px;
207
+ }
208
+
209
+ /* ── Section card ──────────────────────────────────────────── */
210
+ .demo-section {
211
+ background: rgba(255,255,255,0.85);
212
+ border: 1px solid rgba(255,255,255,0.9);
213
+ border-radius: var(--radius-lg);
214
+ backdrop-filter: blur(14px);
215
+ box-shadow: 0 14px 38px rgba(15,23,42,0.05);
216
+ overflow: hidden;
217
+ }
218
+
219
+ .demo-section-header {
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 12px;
223
+ padding: 14px 20px;
224
+ border-bottom: 1px solid var(--border-color);
225
+ background: rgba(244,247,243,0.65);
226
+ }
227
+
228
+ .section-num {
229
+ width: 24px; height: 24px;
230
+ border-radius: 7px;
231
+ background: var(--primary-color-ml);
232
+ color: #fff;
233
+ font-size: 11.5px;
234
+ font-weight: 700;
235
+ display: flex; align-items: center; justify-content: center;
236
+ flex-shrink: 0;
237
+ }
238
+
239
+ .section-title {
240
+ font-family: var(--font-headings);
241
+ font-size: 14px;
242
+ font-weight: 700;
243
+ color: var(--dark-color-ml);
244
+ flex: 1;
245
+ }
246
+
247
+ .section-chips {
248
+ display: flex;
249
+ flex-wrap: wrap;
250
+ gap: 5px;
251
+ }
252
+
253
+ .chip {
254
+ padding: 2px 9px;
255
+ background: var(--primary-light-color-ml);
256
+ color: var(--primary-dark-color-ml);
257
+ border: 1px solid rgba(15,118,110,0.15);
258
+ border-radius: 20px;
259
+ font-size: 11px;
260
+ font-weight: 500;
261
+ }
262
+
263
+ /* ── Tab bar ───────────────────────────────────────────────── */
264
+ .demo-tabs {
265
+ display: flex;
266
+ border-bottom: 1px solid var(--border-color);
267
+ background: rgba(255,255,255,0.6);
268
+ }
269
+
270
+ .demo-tab {
271
+ padding: 9px 18px;
272
+ font-size: 12.5px;
273
+ font-weight: 600;
274
+ color: var(--text-muted);
275
+ cursor: pointer;
276
+ border-bottom: 2px solid transparent;
277
+ transition: all 0.14s ease;
278
+ user-select: none;
279
+ }
280
+ .demo-tab:hover { color: var(--primary-color-ml); }
281
+ .demo-tab.active {
282
+ color: var(--primary-dark-color-ml);
283
+ border-bottom-color: var(--primary-color-ml);
284
+ background: rgba(215,244,238,0.3);
285
+ }
286
+
287
+ /* ── Pane areas ────────────────────────────────────────────── */
288
+ .demo-pane { display: none; padding: 24px; }
289
+ .demo-pane.active { display: block; }
290
+
291
+ .code-pane { display: none; }
292
+ .code-pane.active { display: block; }
293
+
294
+ /* ── Code block ────────────────────────────────────────────── */
295
+ .code-block {
296
+ background: var(--dark-color-ml);
297
+ color: #d4e6e1;
298
+ font-family: var(--font-mono);
299
+ font-size: 12.5px;
300
+ line-height: 1.7;
301
+ padding: 20px 24px;
302
+ overflow-x: auto;
303
+ border-radius: 0;
304
+ margin: 0;
305
+ }
306
+ .code-block .cm { color: #667c79; font-style: italic; }
307
+ .code-block .kw { color: #4fc3a1; }
308
+ .code-block .fn { color: #a3d9cc; }
309
+ .code-block .str { color: #c3e88d; }
310
+ .code-block .num { color: #f78c6c; }
311
+ .code-block .tag { color: #86e1fc; }
312
+ .code-block .at { color: #c3e88d; }
313
+
314
+ /* ── Form helpers ──────────────────────────────────────────── */
315
+ .form-label {
316
+ font-size: 12.5px;
317
+ font-weight: 600;
318
+ color: var(--text-secondary);
319
+ margin-bottom: 6px;
320
+ display: flex;
321
+ align-items: center;
322
+ gap: 6px;
323
+ }
324
+
325
+ .form-hint {
326
+ font-size: 11.5px;
327
+ color: var(--text-muted);
328
+ margin-top: 5px;
329
+ }
330
+
331
+ .form-row { margin-bottom: 20px; }
332
+ .form-row:last-child { margin-bottom: 0; }
333
+
334
+ .inline-code {
335
+ font-family: var(--font-mono);
336
+ font-size: 11px;
337
+ background: var(--bg-surface-tertiary);
338
+ color: var(--primary-dark-color-ml);
339
+ padding: 1px 5px;
340
+ border-radius: 4px;
341
+ border: 1px solid var(--border-color);
342
+ }
343
+
344
+ /* ── Live Preview Box for Avatar ──────────────────────────── */
345
+ .avatar-preview-wrap {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 16px;
349
+ padding: 12px 16px;
350
+ background: var(--light-color-ml);
351
+ border: 1px dashed var(--border-color);
352
+ border-radius: var(--radius);
353
+ margin-bottom: 12px;
354
+ }
355
+ .avatar-preview-img {
356
+ width: 64px;
357
+ height: 64px;
358
+ border-radius: 50%;
359
+ object-fit: cover;
360
+ border: 2px solid var(--primary-color-ml);
361
+ background: #ffffff;
362
+ box-shadow: 0 4px 10px rgba(15,118,110,0.15);
363
+ }
364
+ .avatar-preview-text {
365
+ display: flex;
366
+ flex-direction: column;
367
+ gap: 2px;
368
+ }
369
+ .avatar-preview-title {
370
+ font-size: 12.5px;
371
+ font-weight: 600;
372
+ color: var(--dark-color-ml);
373
+ }
374
+ .avatar-preview-sub {
375
+ font-size: 11.5px;
376
+ color: var(--text-muted);
377
+ }
378
+
379
+ /* ── Output box ────────────────────────────────────────────── */
380
+ .output-box {
381
+ background: var(--light-color-ml);
382
+ border: 1px solid var(--border-color);
383
+ border-radius: var(--radius);
384
+ padding: 10px 14px;
385
+ font-family: var(--font-mono);
386
+ font-size: 12px;
387
+ color: var(--text-muted);
388
+ min-height: 42px;
389
+ margin-top: 14px;
390
+ word-break: break-all;
391
+ line-height: 1.5;
392
+ transition: border-color 0.15s ease;
393
+ }
394
+ .output-box .output-label {
395
+ font-size: 10px;
396
+ font-weight: 700;
397
+ text-transform: uppercase;
398
+ letter-spacing: 0.06em;
399
+ color: var(--primary-color-ml);
400
+ display: block;
401
+ margin-bottom: 3px;
402
+ }
403
+ .output-box.has-value {
404
+ border-color: rgba(22,163,74,0.3);
405
+ background: rgba(22,163,74,0.04);
406
+ color: var(--success-color-ml);
407
+ }
408
+
409
+ /* ── Divider ───────────────────────────────────────────────── */
410
+ .divider {
411
+ border: none;
412
+ border-top: 1px solid var(--border-color);
413
+ margin: 20px 0;
414
+ }
415
+
416
+ /* ── Text input (demo inputs) ──────────────────────────────── */
417
+ .demo-input {
418
+ width: 100%;
419
+ padding: 8px 12px;
420
+ background: var(--light-color-ml);
421
+ border: 1px solid var(--border-color);
422
+ border-radius: var(--radius-sm);
423
+ font-family: var(--font-mono);
424
+ font-size: 12.5px;
425
+ color: var(--dark-color-ml);
426
+ outline: none;
427
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
428
+ }
429
+ .demo-input:focus {
430
+ border-color: var(--primary-color-ml);
431
+ box-shadow: 0 0 0 3px rgba(15,118,110,0.1);
432
+ }
433
+
434
+ .flex-row {
435
+ display: flex;
436
+ align-items: center;
437
+ gap: 8px;
438
+ flex-wrap: wrap;
439
+ }
440
+
441
+ /* ── API result panel ──────────────────────────────────────── */
442
+ .api-result {
443
+ background: var(--dark-color-ml);
444
+ border-radius: var(--radius);
445
+ overflow: hidden;
446
+ margin-top: 14px;
447
+ }
448
+ .api-result-header {
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 8px;
452
+ padding: 8px 14px;
453
+ background: rgba(255,255,255,0.05);
454
+ border-bottom: 1px solid rgba(255,255,255,0.06);
455
+ font-family: var(--font-mono);
456
+ font-size: 11.5px;
457
+ color: #667c79;
458
+ }
459
+ .api-dot {
460
+ width: 7px; height: 7px;
461
+ border-radius: 50%;
462
+ background: var(--text-disabled);
463
+ flex-shrink: 0;
464
+ }
465
+ .api-dot.ok { background: var(--success-color-ml); box-shadow: 0 0 6px rgba(22,163,74,0.5); }
466
+ .api-dot.err { background: var(--danger-color-ml); box-shadow: 0 0 6px rgba(220,38,38,0.5); }
467
+ .api-result pre {
468
+ padding: 14px 16px;
469
+ font-family: var(--font-mono);
470
+ font-size: 11.5px;
471
+ color: #d4e6e1;
472
+ overflow-x: auto;
473
+ max-height: 240px;
474
+ white-space: pre-wrap;
475
+ word-break: break-word;
476
+ }
477
+
478
+ /* ── Toast demo buttons ────────────────────────────────────── */
479
+ .toast-grid {
480
+ display: grid;
481
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
482
+ gap: 8px;
483
+ }
484
+ .toast-btn {
485
+ padding: 9px 14px;
486
+ border-radius: var(--radius);
487
+ font-family: var(--font);
488
+ font-size: 12.5px;
489
+ font-weight: 600;
490
+ cursor: pointer;
491
+ border: 1px solid;
492
+ transition: all 0.15s ease;
493
+ display: flex;
494
+ align-items: center;
495
+ justify-content: center;
496
+ gap: 7px;
497
+ }
498
+ .toast-btn:active { transform: scale(0.97); }
499
+ .toast-btn:hover { transform: translateY(-1px); }
500
+
501
+ .tb-success { background: rgba(22,163,74,0.08); border-color: rgba(22,163,74,0.25); color: var(--success-color-ml); }
502
+ .tb-error { background: rgba(220,38,38,0.08); border-color: rgba(220,38,38,0.25); color: var(--danger-color-ml); }
503
+ .tb-warning { background: rgba(217,119,6,0.08); border-color: rgba(217,119,6,0.25); color: var(--warning-color-ml); }
504
+ .tb-info { background: rgba(6,111,209,0.08); border-color: rgba(6,111,209,0.25); color: var(--info-color-ml); }
505
+
506
+ .form-submit-row {
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 8px;
510
+ margin-top: 18px;
511
+ }
512
+
513
+ .demo-footer {
514
+ position: relative;
515
+ z-index: 1;
516
+ text-align: center;
517
+ padding: 20px 24px;
518
+ border-top: 1px solid var(--border-color);
519
+ background: rgba(255,255,255,0.6);
520
+ font-size: 12.5px;
521
+ color: var(--text-muted);
522
+ }
523
+ .demo-footer a {
524
+ color: var(--primary-color-ml);
525
+ text-decoration: none;
526
+ font-weight: 500;
527
+ }
528
+ .demo-footer a:hover { text-decoration: underline; }
529
+
530
+ @media (max-width: 640px) {
531
+ .page-wrap { padding: 16px 14px 60px; }
532
+ .top-nav { padding: 0 1rem; }
533
+ .nav-links { display: none; }
534
+ .demo-page-header { flex-direction: column; }
535
+ .demo-include-snippet { white-space: normal; }
536
+ }
537
+ </style>
538
+ </head>
539
+ <body>
540
+
541
+ <!-- ── Top navigation ──────────────────────────────────────────────── -->
542
+ <nav class="top-nav">
543
+ <a class="nav-logo" href="/mbkbucket">
544
+ <div class="nav-logo-icon">
545
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
546
+ <path d="M12 3C8 3 4 5 4 8v1c0 3 2 5 8 6 6-1 8-3 8-6V8c0-3-4-5-8-5z"/>
547
+ <path d="M4 9c0 3 2 5 8 6 6-1 8-3 8-6"/>
548
+ <path d="M4 13c0 3 2 5 8 6 6-1 8-3 8-6"/>
549
+ </svg>
550
+ </div>
551
+ MBKBucket
552
+ <span class="nav-badge">Helper Demo</span>
553
+ </a>
554
+
555
+ <div class="nav-links">
556
+ <a class="nav-link active" href="#gallery">Gallery</a>
557
+ <a class="nav-link" href="#picker">Live Preview Picker</a>
558
+ <a class="nav-link" href="#uploader">Upload Widget</a>
559
+ <a class="nav-link" href="#api">URL &amp; API</a>
560
+ <a class="nav-link" href="#toast">Toasts</a>
561
+ </div>
562
+
563
+ <a class="nav-back" href="/mbkbucket">
564
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
565
+ Admin Dashboard
566
+ </a>
567
+ </nav>
568
+
569
+ <!-- ── Page ────────────────────────────────────────────────────────── -->
570
+ <div class="page-wrap">
571
+
572
+ <!-- Page header -->
573
+ <div class="demo-page-header">
574
+ <div>
575
+ <div class="demo-page-pretitle">Developer Reference</div>
576
+ <h1 class="demo-page-title">MBKBucket Frontend Helpers</h1>
577
+ <p class="demo-page-desc">Single-line includes for any web page. In-modal upload, multi-file selection, live image preview bindings, and full S3/R2 utilities.</p>
578
+ </div>
579
+ <div class="demo-include-snippet">
580
+ <span class="line"><span class="kw">&lt;link</span> rel="stylesheet" href=<span class="str">"/mbkbucket/mbkbucket-helper.css"</span><span class="kw">&gt;</span></span>
581
+ <span class="line"><span class="kw">&lt;script</span> src=<span class="str">"/mbkbucket/mbkbucket-helper.js"</span><span class="kw">&gt;&lt;/script&gt;</span></span>
582
+ </div>
583
+ </div>
584
+
585
+ <div class="demo-sections">
586
+
587
+ <!-- ── 1. Gallery Browser (Single & Multi-Select with In-Modal Upload) ────── -->
588
+ <div class="demo-section" id="gallery">
589
+ <div class="demo-section-header">
590
+ <div class="section-num">1</div>
591
+ <div class="section-title">Gallery Browser Modal</div>
592
+ <div class="section-chips">
593
+ <span class="chip">In-Modal Upload</span>
594
+ <span class="chip">Multi-Select</span>
595
+ <span class="chip">Quick Filters</span>
596
+ <span class="chip">Drag &amp; Drop</span>
597
+ <span class="chip">Grid / List</span>
598
+ </div>
599
+ </div>
600
+
601
+ <div class="demo-tabs">
602
+ <div class="demo-tab active" data-tab="demo" data-group="gallery">Live Demo</div>
603
+ <div class="demo-tab" data-tab="js" data-group="gallery">JS API</div>
604
+ <div class="demo-tab" data-tab="attr" data-group="gallery">Data-attribute</div>
605
+ </div>
606
+
607
+ <!-- Demo -->
608
+ <div class="demo-pane active" data-pane="gallery-demo">
609
+ <div class="form-row">
610
+ <div class="form-label">Single file selection &amp; in-modal upload:</div>
611
+ <div class="flex-row">
612
+ <button class="btn btn-primary" id="demo-open-gallery">
613
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
614
+ Browse Single File
615
+ </button>
616
+ <button class="btn btn-outline" id="demo-open-gallery-multi">
617
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
618
+ Browse Multiple Files (<code style="font-family:var(--font-mono);font-size:11px">multiple: true</code>)
619
+ </button>
620
+ </div>
621
+ <div class="output-box" id="gallery-output">
622
+ <span class="output-label">Selected Result</span>
623
+ — nothing selected yet
624
+ </div>
625
+ </div>
626
+
627
+ <hr class="divider">
628
+
629
+ <div class="form-label">Data-attribute trigger (writes to target input):</div>
630
+ <div class="flex-row" style="margin-top:6px">
631
+ <button class="btn btn-outline btn-sm"
632
+ data-mbk-gallery-trigger
633
+ data-mbk-target="#gallery-attr-result">
634
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z"/></svg>
635
+ data-mbk-gallery-trigger
636
+ </button>
637
+ <input id="gallery-attr-result"
638
+ class="demo-input"
639
+ style="max-width:340px"
640
+ type="text"
641
+ readonly
642
+ placeholder="key will appear here…">
643
+ </div>
644
+ </div>
645
+
646
+ <!-- JS code -->
647
+ <div class="code-pane" data-pane="gallery-js">
648
+ <pre class="code-block"><span class="cm">// 1. Single selection mode (default)</span>
649
+ <span class="kw">const</span> file = <span class="kw">await</span> MBKBucket.<span class="fn">openGallery</span>({
650
+ prefix: <span class="str">'images/'</span>, <span class="cm">// start folder</span>
651
+ });
652
+ <span class="kw">if</span> (file) console.<span class="fn">log</span>(file.key, file.name, file.size, file.url);
653
+
654
+ <span class="cm">// 2. Multi-selection mode with In-Modal Upload</span>
655
+ <span class="kw">const</span> files = <span class="kw">await</span> MBKBucket.<span class="fn">openGallery</span>({
656
+ multiple: <span class="num">true</span>,
657
+ maxFiles: <span class="num">10</span>,
658
+ });
659
+ <span class="kw">if</span> (files) {
660
+ <span class="cm">// returns Array&lt;{ key, name, size, url }&gt;</span>
661
+ files.<span class="fn">forEach</span>(f =&gt; console.<span class="fn">log</span>(f.key));
662
+ }</pre>
663
+ </div>
664
+
665
+ <!-- Data-attr code -->
666
+ <div class="code-pane" data-pane="gallery-attr">
667
+ <pre class="code-block"><span class="cm">&lt;!-- Single file picker trigger --&gt;</span>
668
+ <span class="tag">&lt;button</span>
669
+ <span class="at">data-mbk-gallery-trigger</span>
670
+ <span class="at">data-mbk-target</span>=<span class="str">"#my-field"</span>
671
+ <span class="at">data-mbk-prefix</span>=<span class="str">"uploads/"</span>
672
+ <span class="tag">&gt;</span>Browse Bucket<span class="tag">&lt;/button&gt;</span>
673
+ <span class="tag">&lt;input</span> <span class="at">id</span>=<span class="str">"my-field"</span> <span class="at">type</span>=<span class="str">"text"</span> <span class="at">readonly</span><span class="tag">&gt;</span>
674
+
675
+ <span class="cm">&lt;!-- Multi-file picker trigger --&gt;</span>
676
+ <span class="tag">&lt;button</span>
677
+ <span class="at">data-mbk-gallery-trigger</span>
678
+ <span class="at">data-mbk-target</span>=<span class="str">"#multi-field"</span>
679
+ <span class="at">data-mbk-multiple</span>=<span class="str">"true"</span>
680
+ <span class="tag">&gt;</span>Browse Multiple<span class="tag">&lt;/button&gt;</span>
681
+ <span class="tag">&lt;input</span> <span class="at">id</span>=<span class="str">"multi-field"</span> <span class="at">type</span>=<span class="str">"text"</span> <span class="at">readonly</span><span class="tag">&gt;</span></pre>
682
+ </div>
683
+ </div>
684
+
685
+ <!-- ── 2. File Picker with Live Preview Binding ─────────────── -->
686
+ <div class="demo-section" id="picker">
687
+ <div class="demo-section-header">
688
+ <div class="section-num">2</div>
689
+ <div class="section-title">File Picker &amp; Live Image Preview Binding</div>
690
+ <div class="section-chips">
691
+ <span class="chip">Live &lt;img&gt; Preview</span>
692
+ <span class="chip">data-mbk-preview</span>
693
+ <span class="chip">Multi-select Input</span>
694
+ <span class="chip">Form Ready</span>
695
+ </div>
696
+ </div>
697
+
698
+ <div class="demo-tabs">
699
+ <div class="demo-tab active" data-tab="demo" data-group="picker">Live Demo</div>
700
+ <div class="demo-tab" data-tab="js" data-group="picker">JS API</div>
701
+ <div class="demo-tab" data-tab="attr" data-group="picker">Data-attribute</div>
702
+ </div>
703
+
704
+ <div class="demo-pane active" data-pane="picker-demo">
705
+ <form id="picker-form" onsubmit="return demoPickerSubmit(event)">
706
+
707
+ <!-- Live Preview Avatar Demo -->
708
+ <div class="form-row">
709
+ <label class="form-label">
710
+ User Avatar with Live Image Preview
711
+ <code class="inline-code">data-mbk-preview="#avatar-live-img"</code>
712
+ </label>
713
+ <div class="avatar-preview-wrap">
714
+ <img id="avatar-live-img"
715
+ class="avatar-preview-img"
716
+ src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=128&auto=format&fit=crop&q=80"
717
+ alt="Avatar Preview">
718
+ <div class="avatar-preview-text">
719
+ <span class="avatar-preview-title">Profile Avatar Preview</span>
720
+ <span class="avatar-preview-sub">Select an image from the bucket to instantly update this picture</span>
721
+ </div>
722
+ </div>
723
+ <input id="picker-field-1"
724
+ name="avatar"
725
+ data-mbk-picker
726
+ data-mbk-preview="#avatar-live-img"
727
+ data-mbk-prefix="images/"
728
+ data-mbk-placeholder="Pick an avatar image…">
729
+ </div>
730
+
731
+ <!-- Multi-file picker input -->
732
+ <div class="form-row">
733
+ <label class="form-label" for="picker-field-2">
734
+ Gallery Photos field (Multi-Select)
735
+ <code class="inline-code">data-mbk-multiple="true"</code>
736
+ </label>
737
+ <input id="picker-field-2"
738
+ name="gallery_photos"
739
+ data-mbk-picker
740
+ data-mbk-multiple="true"
741
+ data-mbk-prefix="images/"
742
+ data-mbk-placeholder="Pick multiple photos from bucket…">
743
+ </div>
744
+
745
+ <div class="form-submit-row">
746
+ <button type="submit" class="btn btn-primary btn-sm">Submit Form</button>
747
+ <span style="font-size:12px;color:var(--text-muted)">Logs FormData values below</span>
748
+ </div>
749
+ </form>
750
+
751
+ <div class="output-box" id="picker-output" style="display:none;margin-top:14px">
752
+ <span class="output-label">FormData Submitted</span>
753
+ <span id="picker-output-text" style="white-space:pre-wrap"></span>
754
+ </div>
755
+ </div>
756
+
757
+ <div class="code-pane" data-pane="picker-js">
758
+ <pre class="code-block"><span class="cm">// Wrap an input with Live Image Preview binding</span>
759
+ <span class="kw">const</span> picker = MBKBucket.<span class="fn">createPicker</span>(<span class="str">'#avatar-input'</span>, {
760
+ preview: <span class="str">'#avatar-live-img'</span>, <span class="cm">// Selector for &lt;img&gt; or background container</span>
761
+ prefix: <span class="str">'avatars/'</span>,
762
+ multiple: <span class="num">false</span>,
763
+ placeholder: <span class="str">'Choose an avatar…'</span>,
764
+ <span class="fn">onSelect</span>: result =&gt; {
765
+ console.<span class="fn">log</span>(<span class="str">'Selected:'</span>, result?.key, result?.url);
766
+ },
767
+ });</pre>
768
+ </div>
769
+
770
+ <div class="code-pane" data-pane="picker-attr">
771
+ <pre class="code-block"><span class="cm">&lt;!-- Live preview automatically binds to #avatar-img --&gt;</span>
772
+ <span class="tag">&lt;img</span> <span class="at">id</span>=<span class="str">"avatar-img"</span> <span class="at">src</span>=<span class="str">"/placeholder.png"</span> <span class="at">width</span>=<span class="str">"80"</span> <span class="at">height</span>=<span class="str">"80"</span><span class="tag">&gt;</span>
773
+
774
+ <span class="tag">&lt;input</span>
775
+ <span class="at">name</span>=<span class="str">"avatar"</span>
776
+ <span class="at">data-mbk-picker</span>
777
+ <span class="at">data-mbk-preview</span>=<span class="str">"#avatar-img"</span>
778
+ <span class="at">data-mbk-prefix</span>=<span class="str">"avatars/"</span>
779
+ <span class="at">data-mbk-placeholder</span>=<span class="str">"Select an avatar…"</span>
780
+ <span class="tag">&gt;</span></pre>
781
+ </div>
782
+ </div>
783
+
784
+ <!-- ── 3. Upload Widget ────────────────────────────────────── -->
785
+ <div class="demo-section" id="uploader">
786
+ <div class="demo-section-header">
787
+ <div class="section-num">3</div>
788
+ <div class="section-title">Upload Widget</div>
789
+ <div class="section-chips">
790
+ <span class="chip">Drag &amp; drop</span>
791
+ <span class="chip">Multi-file</span>
792
+ <span class="chip">Per-file progress</span>
793
+ <span class="chip">Auto multipart &gt;10 MB</span>
794
+ <span class="chip">8 MB chunks</span>
795
+ </div>
796
+ </div>
797
+
798
+ <div class="demo-tabs">
799
+ <div class="demo-tab active" data-tab="demo" data-group="uploader">Live Demo</div>
800
+ <div class="demo-tab" data-tab="js" data-group="uploader">JS API</div>
801
+ <div class="demo-tab" data-tab="attr" data-group="uploader">Data-attribute</div>
802
+ </div>
803
+
804
+ <div class="demo-pane active" data-pane="uploader-demo">
805
+ <div class="form-label" style="margin-bottom:10px">
806
+ Upload files to prefix <code class="inline-code">uploads/demo/</code>:
807
+ </div>
808
+ <div data-mbk-uploader
809
+ data-mbk-prefix="uploads/demo/"
810
+ data-mbk-multiple="true">
811
+ </div>
812
+ </div>
813
+
814
+ <div class="code-pane" data-pane="uploader-js">
815
+ <pre class="code-block"><span class="cm">// Mount in any container element</span>
816
+ <span class="kw">const</span> uploader = MBKBucket.<span class="fn">createUploader</span>(<span class="str">'#my-container'</span>, {
817
+ prefix: <span class="str">'uploads/'</span>,
818
+ bucket: <span class="str">''</span>,
819
+ accept: <span class="str">'image/*,.pdf'</span>,
820
+ multiple: <span class="num">true</span>,
821
+ <span class="fn">onSuccess</span>: ({ file, key }) =&gt; console.<span class="fn">log</span>(<span class="str">'Uploaded:'</span>, key),
822
+ <span class="fn">onError</span>: ({ file, error }) =&gt; console.<span class="fn">error</span>(error.message),
823
+ });
824
+
825
+ <span class="cm">// Programmatic injection</span>
826
+ uploader.<span class="fn">addFiles</span>(fileInput.files);
827
+ uploader.<span class="fn">clear</span>();</pre>
828
+ </div>
829
+
830
+ <div class="code-pane" data-pane="uploader-attr">
831
+ <pre class="code-block"><span class="cm">&lt;!-- Auto-inits on DOMContentLoaded --&gt;</span>
832
+ <span class="tag">&lt;div</span>
833
+ <span class="at">data-mbk-uploader</span>
834
+ <span class="at">data-mbk-prefix</span>=<span class="str">"uploads/"</span>
835
+ <span class="at">data-mbk-accept</span>=<span class="str">"image/*,application/pdf"</span>
836
+ <span class="at">data-mbk-multiple</span>=<span class="str">"true"</span>
837
+ <span class="tag">&gt;&lt;/div&gt;</span></pre>
838
+ </div>
839
+ </div>
840
+
841
+ <!-- ── 4. URL Builders & API Helpers ───────────────────────── -->
842
+ <div class="demo-section" id="api">
843
+ <div class="demo-section-header">
844
+ <div class="section-num">4</div>
845
+ <div class="section-title">URL Builders &amp; API Helpers</div>
846
+ <div class="section-chips">
847
+ <span class="chip">MBKBucket.url.*</span>
848
+ <span class="chip">listFiles</span>
849
+ <span class="chip">uploadFile</span>
850
+ <span class="chip">getSignedUrl</span>
851
+ </div>
852
+ </div>
853
+
854
+ <div class="demo-tabs">
855
+ <div class="demo-tab active" data-tab="urls" data-group="api">URL Builders</div>
856
+ <div class="demo-tab" data-tab="list" data-group="api">List Files</div>
857
+ <div class="demo-tab" data-tab="signed" data-group="api">Signed URL</div>
858
+ <div class="demo-tab" data-tab="code" data-group="api">Code Reference</div>
859
+ </div>
860
+
861
+ <!-- URL Builders -->
862
+ <div class="demo-pane active" data-pane="api-urls">
863
+ <div class="form-row">
864
+ <label class="form-label" for="url-helper-key">Enter any file key:</label>
865
+ <input id="url-helper-key"
866
+ class="demo-input"
867
+ type="text"
868
+ value="images/sample.jpg"
869
+ placeholder="e.g. images/photo.jpg"
870
+ oninput="updateUrlBuilderDemos()">
871
+ </div>
872
+ <div style="display:flex;flex-direction:column;gap:10px;margin-top:14px;">
873
+ <div class="output-box" style="margin-top:0">
874
+ <span class="output-label">MBKBucket.url.view(key) — Inline View</span>
875
+ <span id="url-demo-view">/mbkbucket/view/images/sample.jpg</span>
876
+ </div>
877
+ <div class="output-box" style="margin-top:0">
878
+ <span class="output-label">MBKBucket.url.public(key) — Public View (p_view)</span>
879
+ <span id="url-demo-public">/mbkbucket/p_view/images/sample.jpg</span>
880
+ </div>
881
+ <div class="output-box" style="margin-top:0">
882
+ <span class="output-label">MBKBucket.url.download(key) — Force Download</span>
883
+ <span id="url-demo-download">/mbkbucket/download/images/sample.jpg</span>
884
+ </div>
885
+ </div>
886
+ </div>
887
+
888
+ <!-- List Files -->
889
+ <div class="demo-pane" data-pane="api-list">
890
+ <div class="form-row">
891
+ <label class="form-label" for="api-prefix-input">Prefix to list:</label>
892
+ <div class="flex-row">
893
+ <input id="api-prefix-input"
894
+ class="demo-input"
895
+ type="text"
896
+ style="max-width:280px"
897
+ placeholder="e.g. images/ (leave empty for root)">
898
+ <button class="btn btn-primary btn-sm" onclick="demoListFiles()">
899
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.5"/></svg>
900
+ Fetch
901
+ </button>
902
+ </div>
903
+ <div class="form-hint">Calls <code class="inline-code">MBKBucket.api.listFiles(prefix, { recursive: false })</code></div>
904
+ </div>
905
+
906
+ <div class="api-result" id="api-list-result" style="display:none">
907
+ <div class="api-result-header">
908
+ <div class="api-dot" id="api-list-dot"></div>
909
+ <span id="api-list-status">—</span>
910
+ </div>
911
+ <pre id="api-list-pre"></pre>
912
+ </div>
913
+ </div>
914
+
915
+ <!-- Signed URL -->
916
+ <div class="demo-pane" data-pane="api-signed">
917
+ <div class="form-row">
918
+ <label class="form-label" for="api-signed-key">File key:</label>
919
+ <div class="flex-row">
920
+ <input id="api-signed-key"
921
+ class="demo-input"
922
+ type="text"
923
+ style="max-width:320px"
924
+ placeholder="e.g. images/photo.jpg">
925
+ <button class="btn btn-primary btn-sm" onclick="demoSignedUrl()">Get URL</button>
926
+ </div>
927
+ <div class="form-hint">Calls <code class="inline-code">MBKBucket.api.getSignedUrl(key, { expiresIn: 3600 })</code></div>
928
+ </div>
929
+
930
+ <div class="api-result" id="api-signed-result" style="display:none">
931
+ <div class="api-result-header">
932
+ <div class="api-dot" id="api-signed-dot"></div>
933
+ <span id="api-signed-status">—</span>
934
+ </div>
935
+ <pre id="api-signed-pre"></pre>
936
+ </div>
937
+ </div>
938
+
939
+ <!-- Code reference -->
940
+ <div class="code-pane" data-pane="api-code">
941
+ <pre class="code-block"><span class="cm">// URL helpers</span>
942
+ MBKBucket.url.<span class="fn">view</span>(<span class="str">'images/photo.jpg'</span>); <span class="cm">// /mbkbucket/view/images/photo.jpg</span>
943
+ MBKBucket.url.<span class="fn">public</span>(<span class="str">'images/photo.jpg'</span>); <span class="cm">// /mbkbucket/p_view/images/photo.jpg</span>
944
+ MBKBucket.url.<span class="fn">download</span>(<span class="str">'docs/file.pdf'</span>); <span class="cm">// /mbkbucket/download/docs/file.pdf</span>
945
+
946
+ <span class="cm">// List files</span>
947
+ <span class="kw">const</span> { files, folders } = <span class="kw">await</span> MBKBucket.api.<span class="fn">listFiles</span>(<span class="str">'images/'</span>);
948
+
949
+ <span class="cm">// Programmatic upload</span>
950
+ <span class="kw">await</span> MBKBucket.api.<span class="fn">uploadFile</span>(<span class="str">'uploads/'</span>, file, {
951
+ <span class="fn">onProgress</span>: pct =&gt; console.<span class="fn">log</span>(Math.<span class="fn">round</span>(pct * <span class="num">100</span>) + <span class="str">'%'</span>),
952
+ });</pre>
953
+ </div>
954
+ </div>
955
+
956
+ <!-- ── 5. Toast Notifications ──────────────────────────────── -->
957
+ <div class="demo-section" id="toast">
958
+ <div class="demo-section-header">
959
+ <div class="section-num">5</div>
960
+ <div class="section-title">Toast Notifications</div>
961
+ <div class="section-chips">
962
+ <span class="chip">4 types</span>
963
+ <span class="chip">Auto-dismiss</span>
964
+ <span class="chip">Custom duration</span>
965
+ </div>
966
+ </div>
967
+
968
+ <div class="demo-tabs">
969
+ <div class="demo-tab active" data-tab="demo" data-group="toast">Live Demo</div>
970
+ <div class="demo-tab" data-tab="code" data-group="toast">Code</div>
971
+ </div>
972
+
973
+ <div class="demo-pane active" data-pane="toast-demo">
974
+ <div class="form-label" style="margin-bottom:10px">Click to trigger each toast type:</div>
975
+ <div class="toast-grid">
976
+ <button class="toast-btn tb-success" onclick="MBKBucket.toast.success('File uploaded successfully!')">
977
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>
978
+ Success
979
+ </button>
980
+ <button class="toast-btn tb-error" onclick="MBKBucket.toast.error('Upload failed — access denied')">
981
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
982
+ Error
983
+ </button>
984
+ <button class="toast-btn tb-warning" onclick="MBKBucket.toast.warning('File already exists in bucket')">
985
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
986
+ Warning
987
+ </button>
988
+ <button class="toast-btn tb-info" onclick="MBKBucket.toast.info('Connecting to bucket…')">
989
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
990
+ Info
991
+ </button>
992
+ </div>
993
+ </div>
994
+
995
+ <div class="code-pane" data-pane="toast-code">
996
+ <pre class="code-block">MBKBucket.toast.<span class="fn">success</span>(<span class="str">'File uploaded!'</span>);
997
+ MBKBucket.toast.<span class="fn">error</span>(<span class="str">'Upload failed'</span>);
998
+ MBKBucket.toast.<span class="fn">warning</span>(<span class="str">'File already exists'</span>);
999
+ MBKBucket.toast.<span class="fn">info</span>(<span class="str">'Loading…'</span>);</pre>
1000
+ </div>
1001
+ </div>
1002
+
1003
+ </div><!-- /demo-sections -->
1004
+ </div><!-- /page-wrap -->
1005
+
1006
+ <!-- Footer -->
1007
+ <footer class="demo-footer">
1008
+ MBKBucket Helper v1.2.0 &nbsp;·&nbsp;
1009
+ <a href="/mbkbucket">Admin Dashboard</a> &nbsp;·&nbsp;
1010
+ <a href="https://github.com/MIbnEKhalid/mbkbucket" target="_blank" rel="noopener">GitHub</a>
1011
+ </footer>
1012
+
1013
+ <!-- ── Scripts ────────────────────────────────────────────────────── -->
1014
+ <script src="/mbkbucket/mbkbucket-helper.js"></script>
1015
+ <script>
1016
+ // ── Tab switching ────────────────────────────────────────────
1017
+ document.querySelectorAll('.demo-tab').forEach(tab => {
1018
+ tab.addEventListener('click', () => {
1019
+ const group = tab.dataset.group;
1020
+ const tabKey = tab.dataset.tab;
1021
+ const card = tab.closest('.demo-section');
1022
+
1023
+ card.querySelectorAll('.demo-tab').forEach(t => t.classList.toggle('active', t === tab));
1024
+
1025
+ card.querySelectorAll('.demo-pane, .code-pane').forEach(pane => {
1026
+ const key = pane.dataset.pane;
1027
+ const isDemo = tabKey === 'demo' && key === `${group}-demo`;
1028
+ const isUrls = tabKey === 'urls' && key === `${group}-urls`;
1029
+ const isList = tabKey === 'list' && key === `${group}-list`;
1030
+ const isSigned = tabKey === 'signed' && key === `${group}-signed`;
1031
+ const isCode = tabKey === 'code' && key === `${group}-code`;
1032
+ const isJs = tabKey === 'js' && key === `${group}-js`;
1033
+ const isAttr = tabKey === 'attr' && key === `${group}-attr`;
1034
+ pane.classList.toggle('active', isDemo || isUrls || isList || isSigned || isCode || isJs || isAttr);
1035
+ });
1036
+ });
1037
+ });
1038
+
1039
+ // ── Nav scroll highlighting ──────────────────────────────────
1040
+ const sections = document.querySelectorAll('.demo-section[id]');
1041
+ const navLinks = document.querySelectorAll('.nav-link[href^="#"]');
1042
+ const io = new IntersectionObserver(entries => {
1043
+ entries.forEach(e => {
1044
+ if (e.isIntersecting)
1045
+ navLinks.forEach(l => l.classList.toggle('active', l.getAttribute('href') === `#${e.target.id}`));
1046
+ });
1047
+ }, { threshold: 0.4 });
1048
+ sections.forEach(s => io.observe(s));
1049
+
1050
+ // ── 1. Single & Multi Gallery Demos ──────────────────────────
1051
+ document.getElementById('demo-open-gallery').addEventListener('click', async () => {
1052
+ const btn = document.getElementById('demo-open-gallery');
1053
+ btn.disabled = true;
1054
+ btn.textContent = 'Opening…';
1055
+
1056
+ const result = await MBKBucket.openGallery({ prefix: '', bucket: '', multiple: false });
1057
+
1058
+ btn.disabled = false;
1059
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Browse Single File`;
1060
+
1061
+ const out = document.getElementById('gallery-output');
1062
+ if (result) {
1063
+ out.innerHTML = `<span class="output-label">Selected Single File</span>${JSON.stringify(result, null, 2)}`;
1064
+ out.classList.add('has-value');
1065
+ } else {
1066
+ out.innerHTML = `<span class="output-label">Selected Result</span>— cancelled`;
1067
+ out.classList.remove('has-value');
1068
+ }
1069
+ });
1070
+
1071
+ document.getElementById('demo-open-gallery-multi').addEventListener('click', async () => {
1072
+ const btn = document.getElementById('demo-open-gallery-multi');
1073
+ btn.disabled = true;
1074
+ btn.textContent = 'Opening…';
1075
+
1076
+ const results = await MBKBucket.openGallery({ prefix: '', bucket: '', multiple: true });
1077
+
1078
+ btn.disabled = false;
1079
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg> Browse Multiple Files (multiple: true)`;
1080
+
1081
+ const out = document.getElementById('gallery-output');
1082
+ if (results && results.length) {
1083
+ out.innerHTML = `<span class="output-label">Selected Multiple (${results.length} files)</span>${JSON.stringify(results, null, 2)}`;
1084
+ out.classList.add('has-value');
1085
+ } else {
1086
+ out.innerHTML = `<span class="output-label">Selected Result</span>— cancelled`;
1087
+ out.classList.remove('has-value');
1088
+ }
1089
+ });
1090
+
1091
+ // ── 2. Picker form demo ──────────────────────────────────────
1092
+ function demoPickerSubmit(e) {
1093
+ e.preventDefault();
1094
+ const fd = new FormData(e.target);
1095
+ const entries = Object.fromEntries(fd.entries());
1096
+ const out = document.getElementById('picker-output');
1097
+ const txt = document.getElementById('picker-output-text');
1098
+ out.style.display = '';
1099
+ out.classList.add('has-value');
1100
+ txt.textContent = JSON.stringify(entries, null, 2);
1101
+ MBKBucket.toast.success('Form submitted! Check output below.');
1102
+ return false;
1103
+ }
1104
+
1105
+ // ── 4. URL Builder Demos ─────────────────────────────────────
1106
+ function updateUrlBuilderDemos() {
1107
+ const key = document.getElementById('url-helper-key').value.trim();
1108
+ document.getElementById('url-demo-view').textContent = MBKBucket.url.view(key);
1109
+ document.getElementById('url-demo-public').textContent = MBKBucket.url.public(key);
1110
+ document.getElementById('url-demo-download').textContent = MBKBucket.url.download(key);
1111
+ }
1112
+
1113
+ // ── 4. API: list files ───────────────────────────────────────
1114
+ async function demoListFiles() {
1115
+ const prefix = document.getElementById('api-prefix-input').value.trim();
1116
+ const result = document.getElementById('api-list-result');
1117
+ const dot = document.getElementById('api-list-dot');
1118
+ const status = document.getElementById('api-list-status');
1119
+ const pre = document.getElementById('api-list-pre');
1120
+
1121
+ result.style.display = '';
1122
+ dot.className = 'api-dot';
1123
+ status.textContent = 'Loading…';
1124
+ pre.textContent = '';
1125
+
1126
+ try {
1127
+ const data = await MBKBucket.api.listFiles(prefix, { recursive: false });
1128
+ dot.className = 'api-dot ok';
1129
+ const fc = (data.files || []).length;
1130
+ const fd = (data.folders || []).length;
1131
+ status.textContent = `200 OK — ${fc} file${fc !== 1 ? 's' : ''}, ${fd} folder${fd !== 1 ? 's' : ''}`;
1132
+ pre.textContent = JSON.stringify({
1133
+ totalFiles: data.totalFiles,
1134
+ hasMore: data.hasMore,
1135
+ folders: (data.folders || []).slice(0, 5),
1136
+ files: (data.files || []).slice(0, 5).map(f => ({
1137
+ key: f.Key,
1138
+ size: f.Size,
1139
+ lastModified: f.LastModified
1140
+ }))
1141
+ }, null, 2);
1142
+ } catch (err) {
1143
+ dot.className = 'api-dot err';
1144
+ status.textContent = `Error: ${err.message}`;
1145
+ pre.textContent = JSON.stringify({ error: err.message, status: err.status }, null, 2);
1146
+ }
1147
+ }
1148
+
1149
+ // ── 4. API: signed URL ───────────────────────────────────────
1150
+ async function demoSignedUrl() {
1151
+ const key = document.getElementById('api-signed-key').value.trim();
1152
+ if (!key) { MBKBucket.toast.warning('Enter a file key first'); return; }
1153
+
1154
+ const result = document.getElementById('api-signed-result');
1155
+ const dot = document.getElementById('api-signed-dot');
1156
+ const status = document.getElementById('api-signed-status');
1157
+ const pre = document.getElementById('api-signed-pre');
1158
+
1159
+ result.style.display = '';
1160
+ dot.className = 'api-dot';
1161
+ status.textContent = 'Loading…';
1162
+ pre.textContent = '';
1163
+
1164
+ try {
1165
+ const data = await MBKBucket.api.getSignedUrl(key, { expiresIn: 3600 });
1166
+ dot.className = 'api-dot ok';
1167
+ status.textContent = `200 OK — expires in ${data.expiresIn}s`;
1168
+ pre.textContent = JSON.stringify(data, null, 2);
1169
+ } catch (err) {
1170
+ dot.className = 'api-dot err';
1171
+ status.textContent = `Error: ${err.message}`;
1172
+ pre.textContent = JSON.stringify({ error: err.message }, null, 2);
1173
+ }
1174
+ }
1175
+ </script>
1176
+ </body>
1177
+ </html>