@masters-union/union-stack 0.1.7 → 0.1.8
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.
- package/dist/cdn/loader.v1.global.js +179 -3
- package/dist/cdn/loader.v1.global.js.map +1 -1
- package/dist/{chunk-5OCCDRQG.js → chunk-4ZN7SILU.js} +6 -3
- package/dist/chunk-4ZN7SILU.js.map +1 -0
- package/dist/{chunk-6AHBENOC.cjs → chunk-PCVJ4TVQ.cjs} +6 -3
- package/dist/chunk-PCVJ4TVQ.cjs.map +1 -0
- package/dist/{client-AMBRkgCm.d.cts → client-CrIecUWp.d.cts} +27 -8
- package/dist/{client-AMBRkgCm.d.ts → client-CrIecUWp.d.ts} +27 -8
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/picker.cjs +879 -52
- package/dist/picker.cjs.map +1 -1
- package/dist/picker.d.cts +17 -2
- package/dist/picker.d.ts +17 -2
- package/dist/picker.js +879 -52
- package/dist/picker.js.map +1 -1
- package/dist/react.cjs +2 -2
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5OCCDRQG.js.map +0 -1
- package/dist/chunk-6AHBENOC.cjs.map +0 -1
package/dist/picker.cjs
CHANGED
|
@@ -5,10 +5,10 @@ var STYLE_ID = "unionstack-picker-styles";
|
|
|
5
5
|
function ensureStyles() {
|
|
6
6
|
if (typeof document === "undefined") return;
|
|
7
7
|
if (document.getElementById(STYLE_ID)) return;
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
document.head.appendChild(
|
|
8
|
+
const el3 = document.createElement("style");
|
|
9
|
+
el3.id = STYLE_ID;
|
|
10
|
+
el3.textContent = BASE_CSS;
|
|
11
|
+
document.head.appendChild(el3);
|
|
12
12
|
}
|
|
13
13
|
function themeToCssVars(theme) {
|
|
14
14
|
const mode = theme?.mode || "light";
|
|
@@ -82,6 +82,7 @@ var BASE_CSS = `
|
|
|
82
82
|
width: 100%; max-width: 480px;
|
|
83
83
|
max-height: min(calc(100dvh - 32px), 680px);
|
|
84
84
|
display: flex; flex-direction: column;
|
|
85
|
+
position: relative;
|
|
85
86
|
box-shadow:
|
|
86
87
|
0 1px 1px rgba(0,0,0,0.04),
|
|
87
88
|
0 18px 40px -8px rgba(0,0,0,0.18),
|
|
@@ -318,6 +319,178 @@ var BASE_CSS = `
|
|
|
318
319
|
.us-footer a { color: var(--us-muted); text-decoration: none; font-weight: 500; }
|
|
319
320
|
.us-footer a:hover { color: var(--us-fg); }
|
|
320
321
|
|
|
322
|
+
/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 source tabs (Device / URL) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
323
|
+
.us-source-tabs {
|
|
324
|
+
display: inline-flex; gap: 2px; padding: 3px;
|
|
325
|
+
background: var(--us-subtle);
|
|
326
|
+
border: 1px solid var(--us-border);
|
|
327
|
+
border-radius: 10px;
|
|
328
|
+
margin-bottom: 14px;
|
|
329
|
+
}
|
|
330
|
+
.us-source-tab {
|
|
331
|
+
appearance: none; background: transparent; border: 0;
|
|
332
|
+
font: inherit; cursor: pointer;
|
|
333
|
+
padding: 6px 14px; min-height: 30px;
|
|
334
|
+
border-radius: 7px;
|
|
335
|
+
font-size: 12.5px; font-weight: 500; letter-spacing: -0.005em;
|
|
336
|
+
color: var(--us-muted);
|
|
337
|
+
transition: background 140ms, color 140ms;
|
|
338
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
339
|
+
}
|
|
340
|
+
.us-source-tab svg { width: 13px; height: 13px; }
|
|
341
|
+
.us-source-tab:hover { color: var(--us-fg); }
|
|
342
|
+
.us-source-tab[data-active="true"] {
|
|
343
|
+
background: var(--us-bg);
|
|
344
|
+
color: var(--us-fg);
|
|
345
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
|
346
|
+
}
|
|
347
|
+
.us-source-tab:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 2px; }
|
|
348
|
+
|
|
349
|
+
/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 URL source \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
350
|
+
.us-url-source { display: none; }
|
|
351
|
+
.us-url-source[data-active="true"] { display: block; }
|
|
352
|
+
.us-url-form {
|
|
353
|
+
display: flex; gap: 8px;
|
|
354
|
+
padding: 16px;
|
|
355
|
+
border: 1.5px dashed var(--us-border-strong);
|
|
356
|
+
border-radius: calc(var(--us-radius) - 2px);
|
|
357
|
+
background: color-mix(in srgb, var(--us-subtle) 60%, transparent);
|
|
358
|
+
}
|
|
359
|
+
.us-url-input {
|
|
360
|
+
appearance: none;
|
|
361
|
+
flex: 1; min-width: 0;
|
|
362
|
+
padding: 9px 12px; min-height: 36px;
|
|
363
|
+
border-radius: 8px; border: 1px solid var(--us-border);
|
|
364
|
+
background: var(--us-bg); color: var(--us-fg);
|
|
365
|
+
font: inherit; font-size: 13px;
|
|
366
|
+
transition: border-color 140ms, box-shadow 140ms;
|
|
367
|
+
}
|
|
368
|
+
.us-url-input::placeholder { color: var(--us-muted); }
|
|
369
|
+
.us-url-input:focus {
|
|
370
|
+
outline: none;
|
|
371
|
+
border-color: var(--us-primary);
|
|
372
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--us-primary) 18%, transparent);
|
|
373
|
+
}
|
|
374
|
+
.us-url-hint {
|
|
375
|
+
margin-top: 8px;
|
|
376
|
+
font-size: 11.5px; color: var(--us-muted);
|
|
377
|
+
}
|
|
378
|
+
.us-url-hint[data-error="true"] { color: var(--us-danger); }
|
|
379
|
+
|
|
380
|
+
/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 per-row edit button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
381
|
+
.us-file-actions { display: inline-flex; gap: 4px; }
|
|
382
|
+
.us-file-action {
|
|
383
|
+
appearance: none; background: transparent; border: 0; cursor: pointer;
|
|
384
|
+
width: 28px; height: 28px;
|
|
385
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
386
|
+
color: var(--us-muted); border-radius: 6px;
|
|
387
|
+
transition: background 140ms, color 140ms;
|
|
388
|
+
}
|
|
389
|
+
.us-file-action:hover { background: var(--us-subtle); color: var(--us-fg); }
|
|
390
|
+
.us-file-action:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 1px; }
|
|
391
|
+
.us-file-action svg { width: 15px; height: 15px; }
|
|
392
|
+
.us-file-action[data-edited="true"] { color: var(--us-primary); }
|
|
393
|
+
.us-file:not([data-state="queued"]) .us-file-action { display: none; }
|
|
394
|
+
|
|
395
|
+
/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 image editor overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
396
|
+
.us-editor {
|
|
397
|
+
position: absolute; inset: 0; z-index: 2;
|
|
398
|
+
display: flex; flex-direction: column;
|
|
399
|
+
background: var(--us-bg);
|
|
400
|
+
animation: us-fade 140ms ease-out;
|
|
401
|
+
}
|
|
402
|
+
.us-editor-header {
|
|
403
|
+
display: flex; align-items: center; gap: 12px;
|
|
404
|
+
padding: 14px 16px;
|
|
405
|
+
border-bottom: 1px solid var(--us-border);
|
|
406
|
+
}
|
|
407
|
+
.us-editor-title { font-weight: 600; font-size: 14px; flex: 1; letter-spacing: -0.01em; }
|
|
408
|
+
.us-editor-back {
|
|
409
|
+
appearance: none; background: transparent; border: 0; cursor: pointer;
|
|
410
|
+
width: 32px; height: 32px; border-radius: 8px;
|
|
411
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
412
|
+
color: var(--us-muted);
|
|
413
|
+
transition: background 140ms, color 140ms;
|
|
414
|
+
}
|
|
415
|
+
.us-editor-back:hover { background: var(--us-subtle); color: var(--us-fg); }
|
|
416
|
+
.us-editor-back svg { width: 16px; height: 16px; }
|
|
417
|
+
|
|
418
|
+
.us-editor-canvas-wrap {
|
|
419
|
+
flex: 1; min-height: 0;
|
|
420
|
+
background: var(--us-subtle);
|
|
421
|
+
display: flex; align-items: center; justify-content: center;
|
|
422
|
+
padding: 16px;
|
|
423
|
+
position: relative;
|
|
424
|
+
overflow: hidden;
|
|
425
|
+
}
|
|
426
|
+
.us-editor-canvas {
|
|
427
|
+
max-width: 100%; max-height: 100%;
|
|
428
|
+
border-radius: 6px;
|
|
429
|
+
display: block;
|
|
430
|
+
/* Checkerboard for transparency awareness (visible only in circle mode). */
|
|
431
|
+
background-image:
|
|
432
|
+
linear-gradient(45deg, var(--us-border) 25%, transparent 25%, transparent 75%, var(--us-border) 75%),
|
|
433
|
+
linear-gradient(45deg, var(--us-border) 25%, transparent 25%, transparent 75%, var(--us-border) 75%);
|
|
434
|
+
background-size: 12px 12px;
|
|
435
|
+
background-position: 0 0, 6px 6px;
|
|
436
|
+
}
|
|
437
|
+
.us-editor-overlay {
|
|
438
|
+
position: absolute; inset: 0;
|
|
439
|
+
pointer-events: none;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.us-editor-toolbar {
|
|
443
|
+
display: flex; gap: 6px; flex-wrap: wrap;
|
|
444
|
+
padding: 10px 16px;
|
|
445
|
+
border-top: 1px solid var(--us-border);
|
|
446
|
+
}
|
|
447
|
+
.us-tool {
|
|
448
|
+
appearance: none; background: transparent; border: 1px solid var(--us-border);
|
|
449
|
+
font: inherit; cursor: pointer;
|
|
450
|
+
padding: 6px 11px; min-height: 32px;
|
|
451
|
+
border-radius: 8px;
|
|
452
|
+
font-size: 12.5px; font-weight: 500;
|
|
453
|
+
color: var(--us-fg);
|
|
454
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
455
|
+
transition: background 140ms, border-color 140ms, color 140ms;
|
|
456
|
+
}
|
|
457
|
+
.us-tool:hover { background: var(--us-subtle); border-color: var(--us-border-strong); }
|
|
458
|
+
.us-tool[data-active="true"] {
|
|
459
|
+
background: color-mix(in srgb, var(--us-primary) 10%, var(--us-bg));
|
|
460
|
+
border-color: var(--us-primary);
|
|
461
|
+
color: var(--us-primary);
|
|
462
|
+
}
|
|
463
|
+
.us-tool[disabled] { opacity: 0.45; cursor: not-allowed; }
|
|
464
|
+
.us-tool svg { width: 13px; height: 13px; }
|
|
465
|
+
.us-tool-spacer { flex: 1; }
|
|
466
|
+
|
|
467
|
+
.us-editor-footer {
|
|
468
|
+
display: flex; gap: 8px; justify-content: flex-end;
|
|
469
|
+
padding: 12px 16px;
|
|
470
|
+
border-top: 1px solid var(--us-border);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Crop drag handles. Drawn inside .us-editor-canvas-wrap, positioned over canvas. */
|
|
474
|
+
.us-crop-box {
|
|
475
|
+
position: absolute;
|
|
476
|
+
border: 1.5px solid var(--us-primary);
|
|
477
|
+
box-shadow: 0 0 0 9999px rgba(0,0,0,0.45);
|
|
478
|
+
pointer-events: auto;
|
|
479
|
+
cursor: move;
|
|
480
|
+
}
|
|
481
|
+
.us-crop-handle {
|
|
482
|
+
position: absolute;
|
|
483
|
+
width: 12px; height: 12px;
|
|
484
|
+
background: var(--us-bg);
|
|
485
|
+
border: 1.5px solid var(--us-primary);
|
|
486
|
+
border-radius: 2px;
|
|
487
|
+
pointer-events: auto;
|
|
488
|
+
}
|
|
489
|
+
.us-crop-handle[data-pos="nw"] { top: -6px; left: -6px; cursor: nwse-resize; }
|
|
490
|
+
.us-crop-handle[data-pos="ne"] { top: -6px; right: -6px; cursor: nesw-resize; }
|
|
491
|
+
.us-crop-handle[data-pos="sw"] { bottom: -6px; left: -6px; cursor: nesw-resize; }
|
|
492
|
+
.us-crop-handle[data-pos="se"] { bottom: -6px; right: -6px; cursor: nwse-resize; }
|
|
493
|
+
|
|
321
494
|
/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
322
495
|
@media (prefers-reduced-motion: reduce) {
|
|
323
496
|
.us-picker-backdrop,
|
|
@@ -327,7 +500,10 @@ var BASE_CSS = `
|
|
|
327
500
|
.us-dropzone-icon,
|
|
328
501
|
.us-btn,
|
|
329
502
|
.us-file-status,
|
|
330
|
-
.us-file-progress-bar
|
|
503
|
+
.us-file-progress-bar,
|
|
504
|
+
.us-editor,
|
|
505
|
+
.us-source-tab,
|
|
506
|
+
.us-tool { animation: none !important; transition: none !important; }
|
|
331
507
|
.us-file[data-state="uploading"] .us-file-progress-bar::after { animation: none; opacity: 0; }
|
|
332
508
|
}
|
|
333
509
|
|
|
@@ -335,6 +511,409 @@ var BASE_CSS = `
|
|
|
335
511
|
.us-file-list:empty { display: none; }
|
|
336
512
|
`;
|
|
337
513
|
|
|
514
|
+
// src/picker/imageEditor.ts
|
|
515
|
+
var MAX_DISPLAY = 720;
|
|
516
|
+
var ICON = {
|
|
517
|
+
back: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>`,
|
|
518
|
+
crop: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2v14a2 2 0 0 0 2 2h14"/><path d="M18 22V8a2 2 0 0 0-2-2H2"/></svg>`,
|
|
519
|
+
circle: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/></svg>`,
|
|
520
|
+
rotate: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.82.93 6.58 2.46L21 8"/><path d="M21 3v5h-5"/></svg>`,
|
|
521
|
+
undo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>`,
|
|
522
|
+
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`
|
|
523
|
+
};
|
|
524
|
+
var ImageEditor = class {
|
|
525
|
+
constructor(opts) {
|
|
526
|
+
this.opts = opts;
|
|
527
|
+
this.cropBox = null;
|
|
528
|
+
// What's currently rendered to the user, sized to fit MAX_DISPLAY.
|
|
529
|
+
this.displayScale = 1;
|
|
530
|
+
// Tracks whether the working canvas has transparency (post-circle), so
|
|
531
|
+
// export() can pick PNG vs JPEG correctly.
|
|
532
|
+
this.hasAlpha = false;
|
|
533
|
+
this.mode = "none";
|
|
534
|
+
this.cropRect = null;
|
|
535
|
+
// display-canvas coords
|
|
536
|
+
// Drag state for crop interaction.
|
|
537
|
+
this.dragKind = null;
|
|
538
|
+
this.dragStart = null;
|
|
539
|
+
this.onPointerMove = (e) => {
|
|
540
|
+
if (!this.dragKind || !this.dragStart || !this.cropRect) return;
|
|
541
|
+
const dx = e.clientX - this.dragStart.px;
|
|
542
|
+
const dy = e.clientY - this.dragStart.py;
|
|
543
|
+
const start = this.dragStart.rect;
|
|
544
|
+
const maxW = this.displayCanvas.width;
|
|
545
|
+
const maxH = this.displayCanvas.height;
|
|
546
|
+
const min = 24;
|
|
547
|
+
let { x, y, w, h } = start;
|
|
548
|
+
switch (this.dragKind) {
|
|
549
|
+
case "move":
|
|
550
|
+
x = clamp(start.x + dx, 0, maxW - start.w);
|
|
551
|
+
y = clamp(start.y + dy, 0, maxH - start.h);
|
|
552
|
+
break;
|
|
553
|
+
case "nw": {
|
|
554
|
+
const nx = clamp(start.x + dx, 0, start.x + start.w - min);
|
|
555
|
+
const ny = clamp(start.y + dy, 0, start.y + start.h - min);
|
|
556
|
+
w = start.w + (start.x - nx);
|
|
557
|
+
h = start.h + (start.y - ny);
|
|
558
|
+
x = nx;
|
|
559
|
+
y = ny;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
case "ne": {
|
|
563
|
+
const nw = clamp(start.w + dx, min, maxW - start.x);
|
|
564
|
+
const ny = clamp(start.y + dy, 0, start.y + start.h - min);
|
|
565
|
+
h = start.h + (start.y - ny);
|
|
566
|
+
w = nw;
|
|
567
|
+
y = ny;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case "sw": {
|
|
571
|
+
const nx = clamp(start.x + dx, 0, start.x + start.w - min);
|
|
572
|
+
const nh = clamp(start.h + dy, min, maxH - start.y);
|
|
573
|
+
w = start.w + (start.x - nx);
|
|
574
|
+
h = nh;
|
|
575
|
+
x = nx;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case "se":
|
|
579
|
+
w = clamp(start.w + dx, min, maxW - start.x);
|
|
580
|
+
h = clamp(start.h + dy, min, maxH - start.y);
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
this.cropRect = { x, y, w, h };
|
|
584
|
+
this.updateCropBox();
|
|
585
|
+
};
|
|
586
|
+
this.onPointerUp = () => {
|
|
587
|
+
this.dragKind = null;
|
|
588
|
+
this.dragStart = null;
|
|
589
|
+
document.removeEventListener("pointermove", this.onPointerMove);
|
|
590
|
+
document.removeEventListener("pointerup", this.onPointerUp);
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
async open() {
|
|
594
|
+
this.mount();
|
|
595
|
+
try {
|
|
596
|
+
await this.loadOriginalIntoWorking();
|
|
597
|
+
if (this.opts.originalFile && this.opts.originalFile !== this.opts.file) {
|
|
598
|
+
this.markEdited(true);
|
|
599
|
+
}
|
|
600
|
+
this.draw();
|
|
601
|
+
} catch (err) {
|
|
602
|
+
this.canvasWrap.textContent = `Couldn't load image: ${err.message}`;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
close() {
|
|
606
|
+
document.removeEventListener("pointermove", this.onPointerMove);
|
|
607
|
+
document.removeEventListener("pointerup", this.onPointerUp);
|
|
608
|
+
this.root.remove();
|
|
609
|
+
}
|
|
610
|
+
// ---- mount / dom --------------------------------------------------------
|
|
611
|
+
mount() {
|
|
612
|
+
this.root = el("div", "us-editor");
|
|
613
|
+
const header = el("div", "us-editor-header");
|
|
614
|
+
const back = document.createElement("button");
|
|
615
|
+
back.type = "button";
|
|
616
|
+
back.className = "us-editor-back";
|
|
617
|
+
back.setAttribute("aria-label", "Back");
|
|
618
|
+
back.innerHTML = ICON.back;
|
|
619
|
+
back.onclick = () => {
|
|
620
|
+
this.opts.onCancel();
|
|
621
|
+
this.close();
|
|
622
|
+
};
|
|
623
|
+
header.appendChild(back);
|
|
624
|
+
header.appendChild(el("div", "us-editor-title", this.opts.title));
|
|
625
|
+
this.root.appendChild(header);
|
|
626
|
+
this.canvasWrap = el("div", "us-editor-canvas-wrap");
|
|
627
|
+
this.displayCanvas = document.createElement("canvas");
|
|
628
|
+
this.displayCanvas.className = "us-editor-canvas";
|
|
629
|
+
this.canvasWrap.appendChild(this.displayCanvas);
|
|
630
|
+
this.root.appendChild(this.canvasWrap);
|
|
631
|
+
const toolbar = el("div", "us-editor-toolbar");
|
|
632
|
+
this.cropTool = makeTool("Crop", ICON.crop);
|
|
633
|
+
this.cropTool.onclick = () => this.toggleCropMode();
|
|
634
|
+
this.circleTool = makeTool("Circle", ICON.circle);
|
|
635
|
+
this.circleTool.onclick = () => this.applyCircle();
|
|
636
|
+
this.rotateTool = makeTool("Rotate 90\xB0", ICON.rotate);
|
|
637
|
+
this.rotateTool.onclick = () => this.applyRotate();
|
|
638
|
+
this.revertTool = makeTool("Revert", ICON.undo);
|
|
639
|
+
this.revertTool.onclick = () => this.revert();
|
|
640
|
+
this.revertTool.disabled = true;
|
|
641
|
+
toolbar.appendChild(this.cropTool);
|
|
642
|
+
toolbar.appendChild(this.circleTool);
|
|
643
|
+
toolbar.appendChild(this.rotateTool);
|
|
644
|
+
toolbar.appendChild(el("div", "us-tool-spacer"));
|
|
645
|
+
toolbar.appendChild(this.revertTool);
|
|
646
|
+
this.root.appendChild(toolbar);
|
|
647
|
+
const footer = el("div", "us-editor-footer");
|
|
648
|
+
const cancel = document.createElement("button");
|
|
649
|
+
cancel.type = "button";
|
|
650
|
+
cancel.className = "us-btn";
|
|
651
|
+
cancel.textContent = "Cancel";
|
|
652
|
+
cancel.onclick = () => {
|
|
653
|
+
this.opts.onCancel();
|
|
654
|
+
this.close();
|
|
655
|
+
};
|
|
656
|
+
this.applyBtn = document.createElement("button");
|
|
657
|
+
this.applyBtn.type = "button";
|
|
658
|
+
this.applyBtn.className = "us-btn us-btn-primary";
|
|
659
|
+
this.applyBtn.innerHTML = `${ICON.check} <span>Apply</span>`;
|
|
660
|
+
this.applyBtn.onclick = () => this.applyAndClose();
|
|
661
|
+
footer.appendChild(cancel);
|
|
662
|
+
footer.appendChild(this.applyBtn);
|
|
663
|
+
this.root.appendChild(footer);
|
|
664
|
+
this.opts.host.appendChild(this.root);
|
|
665
|
+
}
|
|
666
|
+
// ---- image loading ------------------------------------------------------
|
|
667
|
+
async loadOriginalIntoWorking() {
|
|
668
|
+
const img = await loadImage(this.opts.file);
|
|
669
|
+
const c = document.createElement("canvas");
|
|
670
|
+
c.width = img.naturalWidth;
|
|
671
|
+
c.height = img.naturalHeight;
|
|
672
|
+
const ctx = c.getContext("2d");
|
|
673
|
+
if (!ctx) throw new Error("Canvas 2D context unavailable");
|
|
674
|
+
ctx.drawImage(img, 0, 0);
|
|
675
|
+
this.working = c;
|
|
676
|
+
this.hasAlpha = looksLikePngMime(this.opts.file.type);
|
|
677
|
+
this.markEdited(false);
|
|
678
|
+
}
|
|
679
|
+
// For Revert. Loads the true original (passed-in `originalFile`, or `file`
|
|
680
|
+
// if the caller didn't distinguish).
|
|
681
|
+
async loadTrueOriginalIntoWorking() {
|
|
682
|
+
const source = this.opts.originalFile ?? this.opts.file;
|
|
683
|
+
const img = await loadImage(source);
|
|
684
|
+
const c = document.createElement("canvas");
|
|
685
|
+
c.width = img.naturalWidth;
|
|
686
|
+
c.height = img.naturalHeight;
|
|
687
|
+
const ctx = c.getContext("2d");
|
|
688
|
+
if (!ctx) throw new Error("Canvas 2D context unavailable");
|
|
689
|
+
ctx.drawImage(img, 0, 0);
|
|
690
|
+
this.working = c;
|
|
691
|
+
this.hasAlpha = looksLikePngMime(source.type);
|
|
692
|
+
this.markEdited(false);
|
|
693
|
+
}
|
|
694
|
+
// ---- rendering ----------------------------------------------------------
|
|
695
|
+
draw() {
|
|
696
|
+
const { width: ww, height: wh } = this.working;
|
|
697
|
+
const scale = Math.min(1, MAX_DISPLAY / Math.max(ww, wh));
|
|
698
|
+
const dw = Math.max(1, Math.round(ww * scale));
|
|
699
|
+
const dh = Math.max(1, Math.round(wh * scale));
|
|
700
|
+
this.displayCanvas.width = dw;
|
|
701
|
+
this.displayCanvas.height = dh;
|
|
702
|
+
this.displayScale = scale;
|
|
703
|
+
const ctx = this.displayCanvas.getContext("2d");
|
|
704
|
+
if (!ctx) return;
|
|
705
|
+
ctx.clearRect(0, 0, dw, dh);
|
|
706
|
+
ctx.drawImage(this.working, 0, 0, dw, dh);
|
|
707
|
+
this.updateCropBox();
|
|
708
|
+
}
|
|
709
|
+
// ---- mode: crop ---------------------------------------------------------
|
|
710
|
+
toggleCropMode() {
|
|
711
|
+
if (this.mode === "crop") {
|
|
712
|
+
this.applyCrop();
|
|
713
|
+
} else {
|
|
714
|
+
this.enterCropMode();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
enterCropMode() {
|
|
718
|
+
this.mode = "crop";
|
|
719
|
+
this.cropTool.dataset.active = "true";
|
|
720
|
+
this.cropTool.innerHTML = `${ICON.check} <span>Apply crop</span>`;
|
|
721
|
+
const dw = this.displayCanvas.width;
|
|
722
|
+
const dh = this.displayCanvas.height;
|
|
723
|
+
const inset = Math.round(Math.min(dw, dh) * 0.1);
|
|
724
|
+
this.cropRect = { x: inset, y: inset, w: dw - inset * 2, h: dh - inset * 2 };
|
|
725
|
+
this.renderCropBox();
|
|
726
|
+
}
|
|
727
|
+
exitCropMode() {
|
|
728
|
+
this.mode = "none";
|
|
729
|
+
this.cropTool.removeAttribute("data-active");
|
|
730
|
+
this.cropTool.innerHTML = `${ICON.crop} <span>Crop</span>`;
|
|
731
|
+
this.cropRect = null;
|
|
732
|
+
if (this.cropBox) {
|
|
733
|
+
this.cropBox.remove();
|
|
734
|
+
this.cropBox = null;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
renderCropBox() {
|
|
738
|
+
if (!this.cropRect) return;
|
|
739
|
+
if (this.cropBox) this.cropBox.remove();
|
|
740
|
+
const box = el("div", "us-crop-box");
|
|
741
|
+
for (const pos of ["nw", "ne", "sw", "se"]) {
|
|
742
|
+
const h = el("div", "us-crop-handle");
|
|
743
|
+
h.dataset.pos = pos;
|
|
744
|
+
h.addEventListener("pointerdown", (e) => this.beginDrag(e, pos));
|
|
745
|
+
box.appendChild(h);
|
|
746
|
+
}
|
|
747
|
+
box.addEventListener("pointerdown", (e) => {
|
|
748
|
+
const target = e.target;
|
|
749
|
+
if (target.classList.contains("us-crop-handle")) return;
|
|
750
|
+
this.beginDrag(e, "move");
|
|
751
|
+
});
|
|
752
|
+
this.canvasWrap.appendChild(box);
|
|
753
|
+
this.cropBox = box;
|
|
754
|
+
this.updateCropBox();
|
|
755
|
+
}
|
|
756
|
+
updateCropBox() {
|
|
757
|
+
if (!this.cropBox || !this.cropRect) return;
|
|
758
|
+
const canvasRect = this.displayCanvas.getBoundingClientRect();
|
|
759
|
+
const wrapRect = this.canvasWrap.getBoundingClientRect();
|
|
760
|
+
const left = canvasRect.left - wrapRect.left + this.cropRect.x;
|
|
761
|
+
const top = canvasRect.top - wrapRect.top + this.cropRect.y;
|
|
762
|
+
this.cropBox.style.left = `${left}px`;
|
|
763
|
+
this.cropBox.style.top = `${top}px`;
|
|
764
|
+
this.cropBox.style.width = `${this.cropRect.w}px`;
|
|
765
|
+
this.cropBox.style.height = `${this.cropRect.h}px`;
|
|
766
|
+
}
|
|
767
|
+
// ---- drag handlers (crop) ----------------------------------------------
|
|
768
|
+
beginDrag(e, kind) {
|
|
769
|
+
if (!this.cropRect) return;
|
|
770
|
+
e.preventDefault();
|
|
771
|
+
this.dragKind = kind;
|
|
772
|
+
this.dragStart = { px: e.clientX, py: e.clientY, rect: { ...this.cropRect } };
|
|
773
|
+
document.addEventListener("pointermove", this.onPointerMove);
|
|
774
|
+
document.addEventListener("pointerup", this.onPointerUp);
|
|
775
|
+
}
|
|
776
|
+
// ---- operations (mutate working canvas) --------------------------------
|
|
777
|
+
applyCrop() {
|
|
778
|
+
if (!this.cropRect) {
|
|
779
|
+
this.exitCropMode();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const s = this.displayScale;
|
|
783
|
+
const sx = Math.round(this.cropRect.x / s);
|
|
784
|
+
const sy = Math.round(this.cropRect.y / s);
|
|
785
|
+
const sw = Math.round(this.cropRect.w / s);
|
|
786
|
+
const sh = Math.round(this.cropRect.h / s);
|
|
787
|
+
const next = document.createElement("canvas");
|
|
788
|
+
next.width = sw;
|
|
789
|
+
next.height = sh;
|
|
790
|
+
const ctx = next.getContext("2d");
|
|
791
|
+
if (!ctx) return;
|
|
792
|
+
ctx.drawImage(this.working, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
793
|
+
this.working = next;
|
|
794
|
+
this.exitCropMode();
|
|
795
|
+
this.markEdited(true);
|
|
796
|
+
this.draw();
|
|
797
|
+
}
|
|
798
|
+
applyRotate() {
|
|
799
|
+
const { width: w, height: h } = this.working;
|
|
800
|
+
const next = document.createElement("canvas");
|
|
801
|
+
next.width = h;
|
|
802
|
+
next.height = w;
|
|
803
|
+
const ctx = next.getContext("2d");
|
|
804
|
+
if (!ctx) return;
|
|
805
|
+
ctx.translate(h, 0);
|
|
806
|
+
ctx.rotate(Math.PI / 2);
|
|
807
|
+
ctx.drawImage(this.working, 0, 0);
|
|
808
|
+
this.working = next;
|
|
809
|
+
this.exitCropMode();
|
|
810
|
+
this.markEdited(true);
|
|
811
|
+
this.draw();
|
|
812
|
+
}
|
|
813
|
+
applyCircle() {
|
|
814
|
+
const { width: w, height: h } = this.working;
|
|
815
|
+
const size = Math.min(w, h);
|
|
816
|
+
const sx = Math.round((w - size) / 2);
|
|
817
|
+
const sy = Math.round((h - size) / 2);
|
|
818
|
+
const next = document.createElement("canvas");
|
|
819
|
+
next.width = size;
|
|
820
|
+
next.height = size;
|
|
821
|
+
const ctx = next.getContext("2d");
|
|
822
|
+
if (!ctx) return;
|
|
823
|
+
ctx.save();
|
|
824
|
+
ctx.beginPath();
|
|
825
|
+
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
|
|
826
|
+
ctx.closePath();
|
|
827
|
+
ctx.clip();
|
|
828
|
+
ctx.drawImage(this.working, sx, sy, size, size, 0, 0, size, size);
|
|
829
|
+
ctx.restore();
|
|
830
|
+
this.working = next;
|
|
831
|
+
this.hasAlpha = true;
|
|
832
|
+
this.exitCropMode();
|
|
833
|
+
this.markEdited(true);
|
|
834
|
+
this.draw();
|
|
835
|
+
}
|
|
836
|
+
async revert() {
|
|
837
|
+
await this.loadTrueOriginalIntoWorking();
|
|
838
|
+
this.exitCropMode();
|
|
839
|
+
this.draw();
|
|
840
|
+
}
|
|
841
|
+
markEdited(edited) {
|
|
842
|
+
this.revertTool.disabled = !edited;
|
|
843
|
+
}
|
|
844
|
+
// ---- apply --------------------------------------------------------------
|
|
845
|
+
async applyAndClose() {
|
|
846
|
+
this.applyBtn.disabled = true;
|
|
847
|
+
try {
|
|
848
|
+
const file = await this.exportFile();
|
|
849
|
+
this.opts.onApply(file);
|
|
850
|
+
this.close();
|
|
851
|
+
} catch (err) {
|
|
852
|
+
this.applyBtn.disabled = false;
|
|
853
|
+
console.error("[union-stack] image export failed", err);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
exportFile() {
|
|
857
|
+
return new Promise((resolve, reject) => {
|
|
858
|
+
const mime = this.hasAlpha ? "image/png" : "image/jpeg";
|
|
859
|
+
const quality = mime === "image/jpeg" ? 0.92 : void 0;
|
|
860
|
+
this.working.toBlob(
|
|
861
|
+
(blob) => {
|
|
862
|
+
if (!blob) return reject(new Error("toBlob returned null"));
|
|
863
|
+
const baseName = this.opts.file.name.replace(/\.[^.]+$/, "");
|
|
864
|
+
const ext = mime === "image/png" ? "png" : "jpg";
|
|
865
|
+
resolve(new File([blob], `${baseName}-edited.${ext}`, { type: mime }));
|
|
866
|
+
},
|
|
867
|
+
mime,
|
|
868
|
+
quality
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
function el(tag, className, text) {
|
|
874
|
+
const node = document.createElement(tag);
|
|
875
|
+
if (className) node.className = className;
|
|
876
|
+
if (text !== void 0) node.textContent = text;
|
|
877
|
+
return node;
|
|
878
|
+
}
|
|
879
|
+
function makeTool(label, iconHtml) {
|
|
880
|
+
const btn = document.createElement("button");
|
|
881
|
+
btn.type = "button";
|
|
882
|
+
btn.className = "us-tool";
|
|
883
|
+
btn.innerHTML = `${iconHtml} <span>${escapeText(label)}</span>`;
|
|
884
|
+
return btn;
|
|
885
|
+
}
|
|
886
|
+
function escapeText(s) {
|
|
887
|
+
return s.replace(/[&<>"']/g, (c) => ({
|
|
888
|
+
"&": "&",
|
|
889
|
+
"<": "<",
|
|
890
|
+
">": ">",
|
|
891
|
+
'"': """,
|
|
892
|
+
"'": "'"
|
|
893
|
+
})[c]);
|
|
894
|
+
}
|
|
895
|
+
function clamp(v, lo, hi) {
|
|
896
|
+
return Math.max(lo, Math.min(hi, v));
|
|
897
|
+
}
|
|
898
|
+
function loadImage(file) {
|
|
899
|
+
return new Promise((resolve, reject) => {
|
|
900
|
+
const url = URL.createObjectURL(file);
|
|
901
|
+
const img = new Image();
|
|
902
|
+
img.onload = () => {
|
|
903
|
+
URL.revokeObjectURL(url);
|
|
904
|
+
resolve(img);
|
|
905
|
+
};
|
|
906
|
+
img.onerror = () => {
|
|
907
|
+
URL.revokeObjectURL(url);
|
|
908
|
+
reject(new Error("decode failed"));
|
|
909
|
+
};
|
|
910
|
+
img.src = url;
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
function looksLikePngMime(mime) {
|
|
914
|
+
return /^image\/(png|webp|gif)$/.test(mime);
|
|
915
|
+
}
|
|
916
|
+
|
|
338
917
|
// src/picker/picker.ts
|
|
339
918
|
function mergeConfig(server, runtime) {
|
|
340
919
|
const merged = { ...runtime };
|
|
@@ -364,7 +943,7 @@ function mergeConfig(server, runtime) {
|
|
|
364
943
|
}
|
|
365
944
|
var DEFAULT_TITLE = "Upload files";
|
|
366
945
|
var FOOTER_LINK = "https://unionstack.link";
|
|
367
|
-
var
|
|
946
|
+
var ICON2 = {
|
|
368
947
|
upload: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
|
|
369
948
|
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
|
|
370
949
|
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
|
|
@@ -376,28 +955,39 @@ var ICON = {
|
|
|
376
955
|
pdf: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="13" y2="17"/></svg>`,
|
|
377
956
|
archive: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="5" rx="2"/><path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9"/><line x1="10" y1="13" x2="14" y2="13"/></svg>`,
|
|
378
957
|
file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
|
|
379
|
-
zap: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg
|
|
958
|
+
zap: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
|
|
959
|
+
device: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>`,
|
|
960
|
+
link: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72"/></svg>`,
|
|
961
|
+
pencil: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`
|
|
380
962
|
};
|
|
381
963
|
function iconForMime(mime) {
|
|
382
|
-
if (!mime) return
|
|
383
|
-
if (mime.startsWith("image/")) return
|
|
384
|
-
if (mime.startsWith("video/")) return
|
|
385
|
-
if (mime.startsWith("audio/")) return
|
|
386
|
-
if (mime === "application/pdf") return
|
|
387
|
-
if (mime.startsWith("application/zip") || mime.includes("compressed") || mime === "application/x-tar" || mime === "application/gzip") return
|
|
388
|
-
return
|
|
964
|
+
if (!mime) return ICON2.file;
|
|
965
|
+
if (mime.startsWith("image/")) return ICON2.image;
|
|
966
|
+
if (mime.startsWith("video/")) return ICON2.video;
|
|
967
|
+
if (mime.startsWith("audio/")) return ICON2.audio;
|
|
968
|
+
if (mime === "application/pdf") return ICON2.pdf;
|
|
969
|
+
if (mime.startsWith("application/zip") || mime.includes("compressed") || mime === "application/x-tar" || mime === "application/gzip") return ICON2.archive;
|
|
970
|
+
return ICON2.file;
|
|
389
971
|
}
|
|
390
972
|
var Picker = class {
|
|
391
973
|
constructor(client, opts) {
|
|
392
974
|
this.client = client;
|
|
393
975
|
this.opts = opts;
|
|
394
976
|
this.$backdrop = null;
|
|
977
|
+
this.$panel = null;
|
|
395
978
|
this.$list = null;
|
|
396
979
|
this.$confirm = null;
|
|
397
980
|
this.$cancel = null;
|
|
398
981
|
this.$closeBtn = null;
|
|
399
982
|
this.$input = null;
|
|
983
|
+
// Source-tab DOM (Device vs URL). Only created when both sources are enabled.
|
|
984
|
+
this.$deviceSource = null;
|
|
985
|
+
this.$urlSource = null;
|
|
986
|
+
this.$urlInput = null;
|
|
987
|
+
this.$urlHint = null;
|
|
988
|
+
this.$urlAddBtn = null;
|
|
400
989
|
this.items = [];
|
|
990
|
+
this.editor = null;
|
|
401
991
|
this.abortCtrl = new AbortController();
|
|
402
992
|
this.uploadStarted = false;
|
|
403
993
|
this.resolved = false;
|
|
@@ -440,38 +1030,50 @@ var Picker = class {
|
|
|
440
1030
|
root.addEventListener("click", (e) => {
|
|
441
1031
|
if (e.target === root && !this.uploadStarted) this.cancel();
|
|
442
1032
|
});
|
|
443
|
-
const panel =
|
|
444
|
-
const header =
|
|
445
|
-
const logoWrap =
|
|
1033
|
+
const panel = el2("div", "us-picker");
|
|
1034
|
+
const header = el2("div", "us-picker-header");
|
|
1035
|
+
const logoWrap = el2("div", "us-picker-header-logo");
|
|
446
1036
|
if (this.opts.branding?.logoUrl) {
|
|
447
1037
|
const logo = document.createElement("img");
|
|
448
1038
|
logo.src = this.opts.branding.logoUrl;
|
|
449
1039
|
logo.alt = "";
|
|
450
1040
|
logoWrap.appendChild(logo);
|
|
451
1041
|
} else {
|
|
452
|
-
logoWrap.innerHTML =
|
|
1042
|
+
logoWrap.innerHTML = ICON2.zap;
|
|
453
1043
|
}
|
|
454
1044
|
header.appendChild(logoWrap);
|
|
455
|
-
const title =
|
|
1045
|
+
const title = el2("div", "us-picker-title", this.opts.branding?.title ?? DEFAULT_TITLE);
|
|
456
1046
|
header.appendChild(title);
|
|
457
1047
|
this.$closeBtn = document.createElement("button");
|
|
458
1048
|
this.$closeBtn.type = "button";
|
|
459
1049
|
this.$closeBtn.className = "us-picker-close";
|
|
460
1050
|
this.$closeBtn.setAttribute("aria-label", "Close");
|
|
461
|
-
this.$closeBtn.innerHTML =
|
|
1051
|
+
this.$closeBtn.innerHTML = ICON2.close;
|
|
462
1052
|
this.$closeBtn.onclick = () => this.cancel();
|
|
463
1053
|
header.appendChild(this.$closeBtn);
|
|
464
1054
|
panel.appendChild(header);
|
|
465
|
-
const body =
|
|
466
|
-
|
|
467
|
-
|
|
1055
|
+
const body = el2("div", "us-picker-body");
|
|
1056
|
+
const sources = this.resolvedSources();
|
|
1057
|
+
if (sources.length > 1) {
|
|
1058
|
+
body.appendChild(this.renderSourceTabs(sources));
|
|
1059
|
+
}
|
|
1060
|
+
if (sources.includes("device")) {
|
|
1061
|
+
this.$deviceSource = this.renderDropzone();
|
|
1062
|
+
body.appendChild(this.$deviceSource);
|
|
1063
|
+
}
|
|
1064
|
+
if (sources.includes("url")) {
|
|
1065
|
+
this.$urlSource = this.renderUrlSource();
|
|
1066
|
+
body.appendChild(this.$urlSource);
|
|
1067
|
+
}
|
|
1068
|
+
this.activateSource(sources[0] ?? "device");
|
|
1069
|
+
this.$list = el2("div", "us-file-list");
|
|
468
1070
|
body.appendChild(this.$list);
|
|
469
1071
|
panel.appendChild(body);
|
|
470
|
-
const autoUpload = this.opts.autoUpload
|
|
471
|
-
const actions =
|
|
472
|
-
this.$summary =
|
|
1072
|
+
const autoUpload = this.opts.autoUpload === true;
|
|
1073
|
+
const actions = el2("div", "us-actions");
|
|
1074
|
+
this.$summary = el2("div", "us-actions-summary", "");
|
|
473
1075
|
actions.appendChild(this.$summary);
|
|
474
|
-
const buttons =
|
|
1076
|
+
const buttons = el2("div", "us-actions-buttons");
|
|
475
1077
|
this.$cancel = document.createElement("button");
|
|
476
1078
|
this.$cancel.type = "button";
|
|
477
1079
|
this.$cancel.className = "us-btn";
|
|
@@ -482,7 +1084,7 @@ var Picker = class {
|
|
|
482
1084
|
this.$confirm = document.createElement("button");
|
|
483
1085
|
this.$confirm.type = "button";
|
|
484
1086
|
this.$confirm.className = "us-btn us-btn-primary";
|
|
485
|
-
this.$confirm.innerHTML = `${
|
|
1087
|
+
this.$confirm.innerHTML = `${ICON2.upload} <span>Upload</span>`;
|
|
486
1088
|
this.$confirm.disabled = true;
|
|
487
1089
|
this.$confirm.onclick = () => this.startUpload();
|
|
488
1090
|
buttons.appendChild(this.$confirm);
|
|
@@ -490,24 +1092,123 @@ var Picker = class {
|
|
|
490
1092
|
actions.appendChild(buttons);
|
|
491
1093
|
panel.appendChild(actions);
|
|
492
1094
|
if (!this.opts.branding?.hideFooter) {
|
|
493
|
-
const footer =
|
|
494
|
-
footer.innerHTML = `${
|
|
1095
|
+
const footer = el2("div", "us-footer");
|
|
1096
|
+
footer.innerHTML = `${ICON2.zap} <span>Powered by <a href="${FOOTER_LINK}" target="_blank" rel="noopener">UnionStack</a></span>`;
|
|
495
1097
|
panel.appendChild(footer);
|
|
496
1098
|
}
|
|
497
1099
|
root.appendChild(panel);
|
|
498
1100
|
(this.opts.container ?? document.body).appendChild(root);
|
|
499
1101
|
this.$backdrop = root;
|
|
1102
|
+
this.$panel = panel;
|
|
1103
|
+
}
|
|
1104
|
+
// Resolves which sources to show, honoring opts.fromSources, defaulting to
|
|
1105
|
+
// both. Empty arrays fall back to ['device'] — we never want to leave a
|
|
1106
|
+
// picker with no way to add files.
|
|
1107
|
+
resolvedSources() {
|
|
1108
|
+
const raw = this.opts.fromSources;
|
|
1109
|
+
if (!raw || raw.length === 0) return ["device", "url"];
|
|
1110
|
+
return raw;
|
|
1111
|
+
}
|
|
1112
|
+
renderSourceTabs(sources) {
|
|
1113
|
+
const tabs = el2("div", "us-source-tabs");
|
|
1114
|
+
tabs.setAttribute("role", "tablist");
|
|
1115
|
+
for (const src of sources) {
|
|
1116
|
+
const btn = document.createElement("button");
|
|
1117
|
+
btn.type = "button";
|
|
1118
|
+
btn.className = "us-source-tab";
|
|
1119
|
+
btn.setAttribute("role", "tab");
|
|
1120
|
+
btn.dataset.source = src;
|
|
1121
|
+
btn.innerHTML = src === "device" ? `${ICON2.device} <span>My Device</span>` : `${ICON2.link} <span>Link</span>`;
|
|
1122
|
+
btn.onclick = () => this.activateSource(src);
|
|
1123
|
+
tabs.appendChild(btn);
|
|
1124
|
+
}
|
|
1125
|
+
return tabs;
|
|
1126
|
+
}
|
|
1127
|
+
activateSource(source) {
|
|
1128
|
+
if (this.$deviceSource) {
|
|
1129
|
+
const active = source === "device";
|
|
1130
|
+
this.$deviceSource.style.display = active ? "" : "none";
|
|
1131
|
+
}
|
|
1132
|
+
if (this.$urlSource) {
|
|
1133
|
+
this.$urlSource.dataset.active = source === "url" ? "true" : "false";
|
|
1134
|
+
}
|
|
1135
|
+
const tabs = this.$panel?.querySelectorAll(".us-source-tab");
|
|
1136
|
+
tabs?.forEach((t) => {
|
|
1137
|
+
t.dataset.active = t.dataset.source === source ? "true" : "false";
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
renderUrlSource() {
|
|
1141
|
+
const wrap = el2("div", "us-url-source");
|
|
1142
|
+
const form = el2("div", "us-url-form");
|
|
1143
|
+
const input = document.createElement("input");
|
|
1144
|
+
input.type = "url";
|
|
1145
|
+
input.className = "us-url-input";
|
|
1146
|
+
input.placeholder = "https://example.com/photo.jpg";
|
|
1147
|
+
input.setAttribute("aria-label", "File URL");
|
|
1148
|
+
input.onkeydown = (e) => {
|
|
1149
|
+
if (e.key === "Enter") {
|
|
1150
|
+
e.preventDefault();
|
|
1151
|
+
this.handleUrlAdd();
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
this.$urlInput = input;
|
|
1155
|
+
const add = document.createElement("button");
|
|
1156
|
+
add.type = "button";
|
|
1157
|
+
add.className = "us-btn us-btn-primary";
|
|
1158
|
+
add.textContent = "Add file";
|
|
1159
|
+
add.onclick = () => this.handleUrlAdd();
|
|
1160
|
+
this.$urlAddBtn = add;
|
|
1161
|
+
form.appendChild(input);
|
|
1162
|
+
form.appendChild(add);
|
|
1163
|
+
wrap.appendChild(form);
|
|
1164
|
+
const hint = el2("div", "us-url-hint", "Paste a direct file URL. The host must allow CORS so we can fetch it from the browser.");
|
|
1165
|
+
this.$urlHint = hint;
|
|
1166
|
+
wrap.appendChild(hint);
|
|
1167
|
+
return wrap;
|
|
1168
|
+
}
|
|
1169
|
+
async handleUrlAdd() {
|
|
1170
|
+
if (!this.$urlInput || !this.$urlAddBtn || !this.$urlHint) return;
|
|
1171
|
+
const url = this.$urlInput.value.trim();
|
|
1172
|
+
if (!url) {
|
|
1173
|
+
this.setUrlHint("Enter a URL first.", true);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
1177
|
+
this.setUrlHint("URL must start with http:// or https://", true);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
this.$urlAddBtn.disabled = true;
|
|
1181
|
+
const prevLabel = this.$urlAddBtn.textContent;
|
|
1182
|
+
this.$urlAddBtn.textContent = "Fetching\u2026";
|
|
1183
|
+
this.setUrlHint("Downloading\u2026", false);
|
|
1184
|
+
try {
|
|
1185
|
+
const file = await fetchUrlAsFile(url);
|
|
1186
|
+
this.addFiles([file]);
|
|
1187
|
+
this.$urlInput.value = "";
|
|
1188
|
+
this.setUrlHint("Paste a direct file URL. The host must allow CORS so we can fetch it from the browser.", false);
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
const msg = err.message || "Failed to fetch URL";
|
|
1191
|
+
this.setUrlHint(msg, true);
|
|
1192
|
+
} finally {
|
|
1193
|
+
this.$urlAddBtn.disabled = false;
|
|
1194
|
+
this.$urlAddBtn.textContent = prevLabel || "Add file";
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
setUrlHint(text, isError) {
|
|
1198
|
+
if (!this.$urlHint) return;
|
|
1199
|
+
this.$urlHint.textContent = text;
|
|
1200
|
+
this.$urlHint.dataset.error = isError ? "true" : "false";
|
|
500
1201
|
}
|
|
501
1202
|
renderDropzone() {
|
|
502
|
-
const dz =
|
|
1203
|
+
const dz = el2("div", "us-dropzone");
|
|
503
1204
|
dz.setAttribute("role", "button");
|
|
504
1205
|
dz.setAttribute("tabindex", "0");
|
|
505
1206
|
dz.setAttribute("aria-label", "Drop files here or click to browse");
|
|
506
|
-
const icon =
|
|
507
|
-
icon.innerHTML =
|
|
1207
|
+
const icon = el2("div", "us-dropzone-icon");
|
|
1208
|
+
icon.innerHTML = ICON2.upload;
|
|
508
1209
|
dz.appendChild(icon);
|
|
509
|
-
dz.appendChild(
|
|
510
|
-
dz.appendChild(
|
|
1210
|
+
dz.appendChild(el2("div", "us-dropzone-title", "Drop files to upload"));
|
|
1211
|
+
dz.appendChild(el2("div", "us-dropzone-hint", "or click to browse from your device"));
|
|
511
1212
|
const constraintBits = [];
|
|
512
1213
|
if (this.opts.maxFileSize) constraintBits.push(`max ${formatBytes(this.opts.maxFileSize)}`);
|
|
513
1214
|
if (this.opts.accept) {
|
|
@@ -517,7 +1218,7 @@ var Picker = class {
|
|
|
517
1218
|
}
|
|
518
1219
|
}
|
|
519
1220
|
if (constraintBits.length > 0) {
|
|
520
|
-
dz.appendChild(
|
|
1221
|
+
dz.appendChild(el2("div", "us-dropzone-constraints", constraintBits.join(" \xB7 ")));
|
|
521
1222
|
}
|
|
522
1223
|
const input = document.createElement("input");
|
|
523
1224
|
input.type = "file";
|
|
@@ -551,8 +1252,13 @@ var Picker = class {
|
|
|
551
1252
|
return dz;
|
|
552
1253
|
}
|
|
553
1254
|
unmount() {
|
|
1255
|
+
if (this.editor) {
|
|
1256
|
+
this.editor.close();
|
|
1257
|
+
this.editor = null;
|
|
1258
|
+
}
|
|
554
1259
|
if (this.$backdrop?.parentNode) this.$backdrop.parentNode.removeChild(this.$backdrop);
|
|
555
1260
|
this.$backdrop = null;
|
|
1261
|
+
this.$panel = null;
|
|
556
1262
|
for (const item of this.items) {
|
|
557
1263
|
if (item.objectUrl) URL.revokeObjectURL(item.objectUrl);
|
|
558
1264
|
}
|
|
@@ -569,6 +1275,8 @@ var Picker = class {
|
|
|
569
1275
|
const item2 = {
|
|
570
1276
|
uploadId: cryptoId(),
|
|
571
1277
|
file,
|
|
1278
|
+
originalFile: file,
|
|
1279
|
+
edited: false,
|
|
572
1280
|
state: "failed",
|
|
573
1281
|
progress: 0,
|
|
574
1282
|
error: `File exceeds ${formatBytes(this.opts.maxFileSize)} limit`
|
|
@@ -580,6 +1288,8 @@ var Picker = class {
|
|
|
580
1288
|
const item = {
|
|
581
1289
|
uploadId: cryptoId(),
|
|
582
1290
|
file,
|
|
1291
|
+
originalFile: file,
|
|
1292
|
+
edited: false,
|
|
583
1293
|
state: "queued",
|
|
584
1294
|
progress: 0
|
|
585
1295
|
};
|
|
@@ -587,7 +1297,7 @@ var Picker = class {
|
|
|
587
1297
|
this.renderItem(item);
|
|
588
1298
|
}
|
|
589
1299
|
this.refreshConfirm();
|
|
590
|
-
const autoUpload = this.opts.autoUpload
|
|
1300
|
+
const autoUpload = this.opts.autoUpload === true;
|
|
591
1301
|
const hasQueued = this.items.some((i) => i.state === "queued");
|
|
592
1302
|
if (autoUpload && hasQueued && !this.uploadStarted) {
|
|
593
1303
|
requestAnimationFrame(() => {
|
|
@@ -597,12 +1307,12 @@ var Picker = class {
|
|
|
597
1307
|
}
|
|
598
1308
|
renderItem(item) {
|
|
599
1309
|
if (!this.$list) return;
|
|
600
|
-
const row =
|
|
1310
|
+
const row = el2("div", "us-file");
|
|
601
1311
|
row.dataset.state = item.state;
|
|
602
1312
|
row.dataset.uploadId = item.uploadId;
|
|
603
1313
|
const idx = Math.min(this.items.length - 1, 8);
|
|
604
1314
|
row.style.animationDelay = `${idx * 35}ms`;
|
|
605
|
-
const thumb =
|
|
1315
|
+
const thumb = el2("div", "us-file-thumb");
|
|
606
1316
|
if (item.file.type.startsWith("image/") && typeof URL !== "undefined" && URL.createObjectURL) {
|
|
607
1317
|
try {
|
|
608
1318
|
const objectUrl = URL.createObjectURL(item.file);
|
|
@@ -616,18 +1326,32 @@ var Picker = class {
|
|
|
616
1326
|
thumb.innerHTML = iconForMime(item.file.type);
|
|
617
1327
|
}
|
|
618
1328
|
row.appendChild(thumb);
|
|
619
|
-
const main =
|
|
620
|
-
const row1 =
|
|
621
|
-
row1.appendChild(
|
|
622
|
-
const meta =
|
|
1329
|
+
const main = el2("div", "us-file-main");
|
|
1330
|
+
const row1 = el2("div", "us-file-row1");
|
|
1331
|
+
row1.appendChild(el2("div", "us-file-name", item.file.name));
|
|
1332
|
+
const meta = el2("div", "us-file-meta", formatBytes(item.file.size));
|
|
623
1333
|
row1.appendChild(meta);
|
|
624
1334
|
main.appendChild(row1);
|
|
625
|
-
const progress =
|
|
626
|
-
const bar =
|
|
1335
|
+
const progress = el2("div", "us-file-progress");
|
|
1336
|
+
const bar = el2("div", "us-file-progress-bar");
|
|
627
1337
|
progress.appendChild(bar);
|
|
628
1338
|
main.appendChild(progress);
|
|
629
1339
|
row.appendChild(main);
|
|
630
|
-
const
|
|
1340
|
+
const isImage = item.file.type.startsWith("image/");
|
|
1341
|
+
const editingEnabled = this.opts.imageEditing !== false;
|
|
1342
|
+
if (isImage && editingEnabled && item.state === "queued") {
|
|
1343
|
+
const edit = document.createElement("button");
|
|
1344
|
+
edit.type = "button";
|
|
1345
|
+
edit.className = "us-file-action";
|
|
1346
|
+
edit.setAttribute("aria-label", "Edit image");
|
|
1347
|
+
edit.title = "Edit image";
|
|
1348
|
+
edit.innerHTML = ICON2.pencil;
|
|
1349
|
+
edit.dataset.edited = item.edited ? "true" : "false";
|
|
1350
|
+
edit.onclick = () => this.openEditor(item);
|
|
1351
|
+
row.appendChild(edit);
|
|
1352
|
+
item.$edit = edit;
|
|
1353
|
+
}
|
|
1354
|
+
const status = el2("div", "us-file-status");
|
|
631
1355
|
status.setAttribute("aria-label", this.statusLabel(item));
|
|
632
1356
|
status.innerHTML = this.statusIcon(item.state);
|
|
633
1357
|
row.appendChild(status);
|
|
@@ -635,21 +1359,74 @@ var Picker = class {
|
|
|
635
1359
|
item.$bar = bar;
|
|
636
1360
|
item.$status = status;
|
|
637
1361
|
item.$meta = meta;
|
|
1362
|
+
item.$thumb = thumb;
|
|
638
1363
|
this.$list.appendChild(row);
|
|
639
1364
|
this.updateSummary();
|
|
640
1365
|
}
|
|
1366
|
+
// Swap a queued item's file with an edited version. Refreshes thumbnail and
|
|
1367
|
+
// meta in place so the row identity is preserved (uploadId, position).
|
|
1368
|
+
replaceItemFile(item, nextFile, edited) {
|
|
1369
|
+
item.file = nextFile;
|
|
1370
|
+
item.edited = edited;
|
|
1371
|
+
if (item.objectUrl) URL.revokeObjectURL(item.objectUrl);
|
|
1372
|
+
item.objectUrl = void 0;
|
|
1373
|
+
if (item.$thumb) {
|
|
1374
|
+
item.$thumb.style.backgroundImage = "";
|
|
1375
|
+
item.$thumb.removeAttribute("data-image");
|
|
1376
|
+
if (nextFile.type.startsWith("image/") && typeof URL !== "undefined" && URL.createObjectURL) {
|
|
1377
|
+
try {
|
|
1378
|
+
const objectUrl = URL.createObjectURL(nextFile);
|
|
1379
|
+
item.objectUrl = objectUrl;
|
|
1380
|
+
item.$thumb.style.backgroundImage = `url("${objectUrl}")`;
|
|
1381
|
+
item.$thumb.dataset.image = "true";
|
|
1382
|
+
item.$thumb.innerHTML = "";
|
|
1383
|
+
} catch {
|
|
1384
|
+
item.$thumb.innerHTML = iconForMime(nextFile.type);
|
|
1385
|
+
}
|
|
1386
|
+
} else {
|
|
1387
|
+
item.$thumb.innerHTML = iconForMime(nextFile.type);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (item.$meta) item.$meta.textContent = formatBytes(nextFile.size);
|
|
1391
|
+
if (item.$row) {
|
|
1392
|
+
const nameEl = item.$row.querySelector(".us-file-name");
|
|
1393
|
+
if (nameEl) nameEl.textContent = nextFile.name;
|
|
1394
|
+
}
|
|
1395
|
+
if (item.$edit) item.$edit.dataset.edited = edited ? "true" : "false";
|
|
1396
|
+
}
|
|
1397
|
+
// Mount the image editor overlay on top of the picker panel for the given
|
|
1398
|
+
// queued item. The editor calls back with either an edited File (Apply) or
|
|
1399
|
+
// nothing (Cancel/Back).
|
|
1400
|
+
openEditor(item) {
|
|
1401
|
+
if (!this.$panel || this.editor || item.state !== "queued") return;
|
|
1402
|
+
this.editor = new ImageEditor({
|
|
1403
|
+
host: this.$panel,
|
|
1404
|
+
file: item.file,
|
|
1405
|
+
originalFile: item.originalFile,
|
|
1406
|
+
title: item.originalFile.name,
|
|
1407
|
+
onApply: (edited) => {
|
|
1408
|
+
const wasEdited = edited !== item.originalFile;
|
|
1409
|
+
this.replaceItemFile(item, edited, wasEdited);
|
|
1410
|
+
this.editor = null;
|
|
1411
|
+
},
|
|
1412
|
+
onCancel: () => {
|
|
1413
|
+
this.editor = null;
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
this.editor.open();
|
|
1417
|
+
}
|
|
641
1418
|
statusIcon(state) {
|
|
642
1419
|
switch (state) {
|
|
643
1420
|
case "uploading":
|
|
644
|
-
return
|
|
1421
|
+
return ICON2.spinner;
|
|
645
1422
|
case "done":
|
|
646
|
-
return
|
|
1423
|
+
return ICON2.check;
|
|
647
1424
|
case "failed":
|
|
648
|
-
return
|
|
1425
|
+
return ICON2.alert;
|
|
649
1426
|
case "cancelled":
|
|
650
|
-
return
|
|
1427
|
+
return ICON2.alert;
|
|
651
1428
|
default:
|
|
652
|
-
return
|
|
1429
|
+
return ICON2.spinner;
|
|
653
1430
|
}
|
|
654
1431
|
}
|
|
655
1432
|
statusLabel(item) {
|
|
@@ -815,7 +1592,7 @@ var Picker = class {
|
|
|
815
1592
|
this.resolvePromise(fallback);
|
|
816
1593
|
}
|
|
817
1594
|
};
|
|
818
|
-
function
|
|
1595
|
+
function el2(tag, className, text) {
|
|
819
1596
|
const node = document.createElement(tag);
|
|
820
1597
|
if (className) node.className = className;
|
|
821
1598
|
if (text !== void 0) node.textContent = text;
|
|
@@ -833,6 +1610,56 @@ function cryptoId() {
|
|
|
833
1610
|
}
|
|
834
1611
|
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
835
1612
|
}
|
|
1613
|
+
async function fetchUrlAsFile(url) {
|
|
1614
|
+
let res;
|
|
1615
|
+
try {
|
|
1616
|
+
res = await fetch(url, { mode: "cors", credentials: "omit" });
|
|
1617
|
+
} catch {
|
|
1618
|
+
throw new Error(`Could not reach ${shortHost(url)} \u2014 check the URL or CORS.`);
|
|
1619
|
+
}
|
|
1620
|
+
if (!res.ok) throw new Error(`Server returned ${res.status} \u2014 file unavailable.`);
|
|
1621
|
+
const blob = await res.blob();
|
|
1622
|
+
const filename = filenameFromUrl(url) || "download";
|
|
1623
|
+
const type = blob.type || guessMimeFromExt(filename);
|
|
1624
|
+
return new File([blob], filename, { type });
|
|
1625
|
+
}
|
|
1626
|
+
function shortHost(url) {
|
|
1627
|
+
try {
|
|
1628
|
+
return new URL(url).host;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return "that host";
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function filenameFromUrl(url) {
|
|
1634
|
+
try {
|
|
1635
|
+
const u = new URL(url);
|
|
1636
|
+
const last = u.pathname.split("/").filter(Boolean).pop();
|
|
1637
|
+
return last ? decodeURIComponent(last) : "";
|
|
1638
|
+
} catch {
|
|
1639
|
+
return "";
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function guessMimeFromExt(filename) {
|
|
1643
|
+
const ext = filename.toLowerCase().split(".").pop() || "";
|
|
1644
|
+
const map = {
|
|
1645
|
+
jpg: "image/jpeg",
|
|
1646
|
+
jpeg: "image/jpeg",
|
|
1647
|
+
png: "image/png",
|
|
1648
|
+
gif: "image/gif",
|
|
1649
|
+
webp: "image/webp",
|
|
1650
|
+
svg: "image/svg+xml",
|
|
1651
|
+
pdf: "application/pdf",
|
|
1652
|
+
mp4: "video/mp4",
|
|
1653
|
+
mov: "video/quicktime",
|
|
1654
|
+
mp3: "audio/mpeg",
|
|
1655
|
+
wav: "audio/wav",
|
|
1656
|
+
zip: "application/zip",
|
|
1657
|
+
json: "application/json",
|
|
1658
|
+
txt: "text/plain",
|
|
1659
|
+
csv: "text/csv"
|
|
1660
|
+
};
|
|
1661
|
+
return map[ext] || "application/octet-stream";
|
|
1662
|
+
}
|
|
836
1663
|
function openPicker(client, opts) {
|
|
837
1664
|
const picker = new Picker(client, opts);
|
|
838
1665
|
return {
|