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