@masters-union/union-stack 0.1.7 → 0.1.9
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 +276 -42
- package/dist/cdn/loader.v1.global.js.map +1 -1
- package/dist/{chunk-5OCCDRQG.js → chunk-6Z46YEXF.js} +43 -19
- package/dist/chunk-6Z46YEXF.js.map +1 -0
- package/dist/{chunk-6AHBENOC.cjs → chunk-DETAGOYQ.cjs} +43 -19
- package/dist/chunk-DETAGOYQ.cjs.map +1 -0
- package/dist/{client-AMBRkgCm.d.cts → client-BKCHbspL.d.cts} +21 -19
- package/dist/{client-AMBRkgCm.d.ts → client-BKCHbspL.d.ts} +21 -19
- 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 +1058 -122
- 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 +1058 -122
- 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
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
// src/picker/styles.ts
|
|
2
2
|
var STYLE_ID = "unionstack-picker-styles";
|
|
3
|
+
var FONT_ID = "unionstack-picker-fonts";
|
|
4
|
+
var FONT_HREF = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap";
|
|
5
|
+
function ensureFonts() {
|
|
6
|
+
if (document.getElementById(FONT_ID)) return;
|
|
7
|
+
const preconnect = (href, cross) => {
|
|
8
|
+
const l = document.createElement("link");
|
|
9
|
+
l.rel = "preconnect";
|
|
10
|
+
l.href = href;
|
|
11
|
+
if (cross) l.crossOrigin = "anonymous";
|
|
12
|
+
document.head.appendChild(l);
|
|
13
|
+
};
|
|
14
|
+
preconnect("https://fonts.googleapis.com");
|
|
15
|
+
preconnect("https://fonts.gstatic.com", true);
|
|
16
|
+
const link = document.createElement("link");
|
|
17
|
+
link.id = FONT_ID;
|
|
18
|
+
link.rel = "stylesheet";
|
|
19
|
+
link.href = FONT_HREF;
|
|
20
|
+
document.head.appendChild(link);
|
|
21
|
+
}
|
|
3
22
|
function ensureStyles() {
|
|
4
23
|
if (typeof document === "undefined") return;
|
|
24
|
+
ensureFonts();
|
|
5
25
|
if (document.getElementById(STYLE_ID)) return;
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
document.head.appendChild(
|
|
26
|
+
const el3 = document.createElement("style");
|
|
27
|
+
el3.id = STYLE_ID;
|
|
28
|
+
el3.textContent = BASE_CSS;
|
|
29
|
+
document.head.appendChild(el3);
|
|
10
30
|
}
|
|
11
31
|
function themeToCssVars(theme) {
|
|
12
32
|
const mode = theme?.mode || "light";
|
|
13
33
|
const defaults = mode === "dark" ? DARK_DEFAULTS : LIGHT_DEFAULTS;
|
|
14
34
|
return {
|
|
15
35
|
"--us-primary": theme?.primary ?? defaults.primary,
|
|
36
|
+
"--us-on-primary": defaults.onPrimary,
|
|
16
37
|
"--us-bg": theme?.background ?? defaults.background,
|
|
17
38
|
"--us-fg": theme?.foreground ?? defaults.foreground,
|
|
18
39
|
"--us-muted": defaults.muted,
|
|
@@ -20,55 +41,84 @@ function themeToCssVars(theme) {
|
|
|
20
41
|
"--us-border": theme?.border ?? defaults.border,
|
|
21
42
|
"--us-border-strong": defaults.borderStrong,
|
|
22
43
|
"--us-elevated": defaults.elevated,
|
|
44
|
+
"--us-overlay": defaults.overlay,
|
|
45
|
+
"--us-raised": defaults.raised,
|
|
46
|
+
"--us-accent": defaults.accent,
|
|
23
47
|
"--us-success": defaults.success,
|
|
24
48
|
"--us-danger": defaults.danger,
|
|
25
49
|
"--us-radius": theme?.radius ?? "12px"
|
|
26
50
|
};
|
|
27
51
|
}
|
|
28
52
|
var LIGHT_DEFAULTS = {
|
|
29
|
-
primary: "#
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
//
|
|
53
|
+
primary: "#494bd6",
|
|
54
|
+
// inverse-primary of the dark scheme
|
|
55
|
+
onPrimary: "#ffffff",
|
|
56
|
+
background: "#fdfbff",
|
|
57
|
+
// surface — near-white with a violet hint
|
|
58
|
+
foreground: "#1b1b21",
|
|
59
|
+
// on-surface
|
|
60
|
+
muted: "#5e5c6e",
|
|
61
|
+
// on-surface-variant
|
|
62
|
+
subtle: "#f3f1fa",
|
|
63
|
+
// surface-container — chips, icon wells, tab rail
|
|
64
|
+
border: "#e4e1ee",
|
|
65
|
+
// outline-variant (soft)
|
|
66
|
+
borderStrong: "#c8c5d4",
|
|
67
|
+
// outline — drag-over emphasis
|
|
42
68
|
elevated: "#ffffff",
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
// cards, one step above the floor
|
|
70
|
+
overlay: "#eceaf4",
|
|
71
|
+
// instant hover state
|
|
72
|
+
raised: "#ffffff",
|
|
73
|
+
// active tab / popover layer
|
|
74
|
+
accent: "#b35a00",
|
|
75
|
+
// tertiary — "edited" markers
|
|
76
|
+
success: "#2c9a5b",
|
|
77
|
+
// desaturated green
|
|
78
|
+
danger: "#ba1a1a"
|
|
45
79
|
};
|
|
46
80
|
var DARK_DEFAULTS = {
|
|
47
|
-
primary: "#
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
primary: "#c0c1ff",
|
|
82
|
+
// primary
|
|
83
|
+
onPrimary: "#1000a9",
|
|
84
|
+
// on-primary — dark ink on periwinkle
|
|
85
|
+
background: "#0c1324",
|
|
86
|
+
// surface (floor)
|
|
87
|
+
foreground: "#dce1fb",
|
|
88
|
+
// on-surface
|
|
89
|
+
muted: "#908fa0",
|
|
90
|
+
// outline
|
|
91
|
+
subtle: "#191f31",
|
|
92
|
+
// surface-container
|
|
93
|
+
border: "#2e3447",
|
|
94
|
+
// surface-container-highest
|
|
95
|
+
borderStrong: "#464554",
|
|
96
|
+
// outline-variant
|
|
97
|
+
elevated: "#151b2d",
|
|
98
|
+
// surface-container-low — cards, one step up
|
|
99
|
+
overlay: "#23293c",
|
|
100
|
+
// surface-container-high — instant hover
|
|
101
|
+
raised: "#2e3447",
|
|
102
|
+
// active tab / popover layer
|
|
103
|
+
accent: "#ffb783",
|
|
104
|
+
// tertiary — "edited" markers
|
|
105
|
+
success: "#7ad08e",
|
|
106
|
+
// desaturated green
|
|
107
|
+
danger: "#ffb4ab"
|
|
108
|
+
// error
|
|
62
109
|
};
|
|
63
110
|
var BASE_CSS = `
|
|
64
111
|
.us-picker-backdrop {
|
|
112
|
+
--us-font: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
113
|
+
--us-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
65
114
|
position: fixed; inset: 0; z-index: 2147483000;
|
|
66
|
-
background:
|
|
115
|
+
background: rgba(2, 6, 23, 0.4);
|
|
67
116
|
-webkit-backdrop-filter: blur(8px);
|
|
68
117
|
backdrop-filter: blur(8px);
|
|
69
118
|
display: flex; align-items: center; justify-content: center;
|
|
70
119
|
padding: 16px;
|
|
71
|
-
font-family:
|
|
120
|
+
font-family: var(--us-font);
|
|
121
|
+
font-feature-settings: "cv02", "cv11";
|
|
72
122
|
animation: us-fade 140ms ease-out;
|
|
73
123
|
}
|
|
74
124
|
@keyframes us-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
@@ -80,10 +130,8 @@ var BASE_CSS = `
|
|
|
80
130
|
width: 100%; max-width: 480px;
|
|
81
131
|
max-height: min(calc(100dvh - 32px), 680px);
|
|
82
132
|
display: flex; flex-direction: column;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
0 18px 40px -8px rgba(0,0,0,0.18),
|
|
86
|
-
0 32px 80px -16px rgba(0,0,0,0.22);
|
|
133
|
+
position: relative;
|
|
134
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
|
87
135
|
overflow: hidden;
|
|
88
136
|
animation: us-rise 240ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
89
137
|
}
|
|
@@ -104,19 +152,20 @@ var BASE_CSS = `
|
|
|
104
152
|
width: 28px; height: 28px; flex-shrink: 0;
|
|
105
153
|
border-radius: 8px;
|
|
106
154
|
background: var(--us-subtle);
|
|
155
|
+
border: 1px solid var(--us-border);
|
|
107
156
|
color: var(--us-primary);
|
|
108
157
|
}
|
|
109
|
-
.us-picker-header-logo svg { width:
|
|
158
|
+
.us-picker-header-logo svg { width: 15px; height: 15px; }
|
|
110
159
|
.us-picker-header-logo img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
|
|
111
|
-
.us-picker-title { font-weight: 600; font-size:
|
|
160
|
+
.us-picker-title { font-weight: 600; font-size: 15px; letter-spacing: -0.02em; line-height: 1.2; flex: 1; }
|
|
112
161
|
.us-picker-close {
|
|
113
162
|
background: none; border: 0; cursor: pointer;
|
|
114
163
|
width: 32px; height: 32px;
|
|
115
164
|
display: inline-flex; align-items: center; justify-content: center;
|
|
116
165
|
color: var(--us-muted); border-radius: 8px;
|
|
117
|
-
transition: color
|
|
166
|
+
transition: color 100ms, background 0ms;
|
|
118
167
|
}
|
|
119
|
-
.us-picker-close:hover { background: var(--us-
|
|
168
|
+
.us-picker-close:hover { background: var(--us-overlay); color: var(--us-fg); }
|
|
120
169
|
.us-picker-close:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 1px; }
|
|
121
170
|
.us-picker-close svg { width: 16px; height: 16px; }
|
|
122
171
|
|
|
@@ -125,17 +174,17 @@ var BASE_CSS = `
|
|
|
125
174
|
|
|
126
175
|
.us-dropzone {
|
|
127
176
|
position: relative;
|
|
128
|
-
border:
|
|
129
|
-
border-radius:
|
|
177
|
+
border: 1px dashed var(--us-border-strong);
|
|
178
|
+
border-radius: 8px;
|
|
130
179
|
padding: 28px 20px;
|
|
131
180
|
text-align: center;
|
|
132
181
|
cursor: pointer;
|
|
133
|
-
transition: border-color
|
|
134
|
-
background:
|
|
182
|
+
transition: border-color 120ms, background 0ms;
|
|
183
|
+
background: var(--us-subtle);
|
|
135
184
|
}
|
|
136
185
|
.us-dropzone:hover {
|
|
137
186
|
border-color: var(--us-primary);
|
|
138
|
-
background: color-mix(in srgb, var(--us-primary)
|
|
187
|
+
background: color-mix(in srgb, var(--us-primary) 6%, var(--us-subtle));
|
|
139
188
|
}
|
|
140
189
|
.us-dropzone:focus-visible {
|
|
141
190
|
outline: 2px solid var(--us-primary); outline-offset: 2px;
|
|
@@ -143,13 +192,13 @@ var BASE_CSS = `
|
|
|
143
192
|
.us-dropzone[data-drag="over"] {
|
|
144
193
|
border-style: solid;
|
|
145
194
|
border-color: var(--us-primary);
|
|
146
|
-
background: color-mix(in srgb, var(--us-primary)
|
|
147
|
-
transform: scale(1.005);
|
|
195
|
+
background: color-mix(in srgb, var(--us-primary) 10%, var(--us-subtle));
|
|
148
196
|
}
|
|
149
197
|
.us-dropzone-icon {
|
|
150
198
|
width: 44px; height: 44px;
|
|
151
|
-
border-radius:
|
|
199
|
+
border-radius: 8px;
|
|
152
200
|
background: color-mix(in srgb, var(--us-primary) 12%, var(--us-bg));
|
|
201
|
+
border: 1px solid color-mix(in srgb, var(--us-primary) 24%, transparent);
|
|
153
202
|
color: var(--us-primary);
|
|
154
203
|
display: inline-flex; align-items: center; justify-content: center;
|
|
155
204
|
margin-bottom: 12px;
|
|
@@ -158,19 +207,18 @@ var BASE_CSS = `
|
|
|
158
207
|
.us-dropzone:hover .us-dropzone-icon { transform: translateY(-2px); }
|
|
159
208
|
.us-dropzone[data-drag="over"] .us-dropzone-icon {
|
|
160
209
|
transform: translateY(-3px) scale(1.06);
|
|
161
|
-
background: color-mix(in srgb, var(--us-primary) 18%, var(--us-bg));
|
|
162
210
|
}
|
|
163
211
|
.us-dropzone-icon svg { width: 22px; height: 22px; }
|
|
164
212
|
.us-dropzone-title {
|
|
165
|
-
font-size: 14px; font-weight: 600; letter-spacing: -0.
|
|
213
|
+
font-size: 14px; font-weight: 600; letter-spacing: -0.011em;
|
|
166
214
|
margin-bottom: 2px;
|
|
167
215
|
}
|
|
168
|
-
.us-dropzone-hint { color: var(--us-muted); font-size: 12.5px; }
|
|
216
|
+
.us-dropzone-hint { color: var(--us-muted); font-size: 12.5px; letter-spacing: -0.006em; }
|
|
169
217
|
.us-dropzone-constraints {
|
|
170
|
-
margin-top:
|
|
218
|
+
margin-top: 12px;
|
|
171
219
|
font-size: 11px; color: var(--us-muted);
|
|
172
|
-
font-family:
|
|
173
|
-
letter-spacing: 0
|
|
220
|
+
font-family: var(--us-mono);
|
|
221
|
+
letter-spacing: 0;
|
|
174
222
|
}
|
|
175
223
|
|
|
176
224
|
.us-dropzone--compact {
|
|
@@ -194,8 +242,8 @@ var BASE_CSS = `
|
|
|
194
242
|
padding: 10px 12px;
|
|
195
243
|
background: var(--us-elevated);
|
|
196
244
|
border: 1px solid var(--us-border);
|
|
197
|
-
border-radius:
|
|
198
|
-
transition: border-color
|
|
245
|
+
border-radius: 8px;
|
|
246
|
+
transition: border-color 120ms;
|
|
199
247
|
animation: us-row-in 280ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
|
|
200
248
|
position: relative;
|
|
201
249
|
}
|
|
@@ -203,19 +251,20 @@ var BASE_CSS = `
|
|
|
203
251
|
from { opacity: 0; transform: translateY(6px); }
|
|
204
252
|
to { opacity: 1; transform: translateY(0); }
|
|
205
253
|
}
|
|
206
|
-
.us-file[data-state="done"] { border-color: color-mix(in srgb, var(--us-success)
|
|
207
|
-
.us-file[data-state="failed"] { border-color: color-mix(in srgb, var(--us-danger)
|
|
254
|
+
.us-file[data-state="done"] { border-color: color-mix(in srgb, var(--us-success) 35%, var(--us-border)); }
|
|
255
|
+
.us-file[data-state="failed"] { border-color: color-mix(in srgb, var(--us-danger) 35%, var(--us-border)); }
|
|
208
256
|
|
|
209
257
|
.us-file-thumb {
|
|
210
258
|
width: 40px; height: 40px; flex-shrink: 0;
|
|
211
|
-
border-radius:
|
|
259
|
+
border-radius: 4px;
|
|
212
260
|
background: var(--us-subtle);
|
|
261
|
+
border: 1px solid var(--us-border);
|
|
213
262
|
background-size: cover; background-position: center;
|
|
214
263
|
display: inline-flex; align-items: center; justify-content: center;
|
|
215
264
|
color: var(--us-muted);
|
|
216
265
|
overflow: hidden;
|
|
217
266
|
}
|
|
218
|
-
.us-file-thumb[data-image="true"] { color: transparent; }
|
|
267
|
+
.us-file-thumb[data-image="true"] { color: transparent; border-color: transparent; }
|
|
219
268
|
.us-file-thumb svg { width: 18px; height: 18px; }
|
|
220
269
|
|
|
221
270
|
.us-file-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
@@ -224,9 +273,12 @@ var BASE_CSS = `
|
|
|
224
273
|
font-size: 13px; font-weight: 500;
|
|
225
274
|
flex: 1; min-width: 0;
|
|
226
275
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
227
|
-
letter-spacing: -0.
|
|
276
|
+
letter-spacing: -0.006em;
|
|
277
|
+
}
|
|
278
|
+
.us-file-meta {
|
|
279
|
+
color: var(--us-muted); font-size: 11px; flex-shrink: 0;
|
|
280
|
+
font-family: var(--us-mono); letter-spacing: 0;
|
|
228
281
|
}
|
|
229
|
-
.us-file-meta { color: var(--us-muted); font-size: 11.5px; flex-shrink: 0; font-variant-numeric: tabular-nums; }
|
|
230
282
|
|
|
231
283
|
.us-file-progress {
|
|
232
284
|
height: 3px; background: var(--us-border); border-radius: 999px; overflow: hidden;
|
|
@@ -282,27 +334,30 @@ var BASE_CSS = `
|
|
|
282
334
|
padding: 12px 16px;
|
|
283
335
|
border-top: 1px solid var(--us-border);
|
|
284
336
|
}
|
|
285
|
-
.us-actions-summary {
|
|
337
|
+
.us-actions-summary {
|
|
338
|
+
font-size: 11px; color: var(--us-muted);
|
|
339
|
+
font-family: var(--us-mono); letter-spacing: 0;
|
|
340
|
+
}
|
|
286
341
|
.us-actions-buttons { display: flex; gap: 8px; }
|
|
287
342
|
|
|
288
343
|
.us-btn {
|
|
289
344
|
appearance: none;
|
|
290
345
|
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
291
346
|
padding: 8px 14px; min-height: 36px;
|
|
292
|
-
border-radius: 8px; border: 1px solid var(--us-border);
|
|
347
|
+
border-radius: 8px; border: 1px solid var(--us-border-strong);
|
|
293
348
|
background: transparent; color: var(--us-fg);
|
|
294
349
|
cursor: pointer; font-size: 13px; font-weight: 500;
|
|
295
|
-
font-family: inherit; letter-spacing:
|
|
296
|
-
transition: background
|
|
350
|
+
font-family: inherit; letter-spacing: 0.01em;
|
|
351
|
+
transition: background 0ms, border-color 120ms, transform 80ms ease-out;
|
|
297
352
|
}
|
|
298
|
-
.us-btn:hover { background: var(--us-
|
|
353
|
+
.us-btn:hover { background: var(--us-overlay); }
|
|
299
354
|
.us-btn:active { transform: scale(0.98); }
|
|
300
355
|
.us-btn:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 2px; }
|
|
301
356
|
.us-btn-primary {
|
|
302
|
-
background: var(--us-primary); color:
|
|
357
|
+
background: var(--us-primary); color: var(--us-on-primary);
|
|
303
358
|
border-color: var(--us-primary);
|
|
304
359
|
}
|
|
305
|
-
.us-btn-primary:hover { filter: brightness(
|
|
360
|
+
.us-btn-primary:hover { filter: brightness(1.06); background: var(--us-primary); }
|
|
306
361
|
.us-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
307
362
|
.us-btn[disabled]:hover { transform: none; }
|
|
308
363
|
.us-btn svg { width: 14px; height: 14px; }
|
|
@@ -310,12 +365,235 @@ var BASE_CSS = `
|
|
|
310
365
|
.us-footer {
|
|
311
366
|
padding: 8px 16px 12px;
|
|
312
367
|
font-size: 11px; color: var(--us-muted); text-align: center;
|
|
368
|
+
letter-spacing: 0.02em;
|
|
313
369
|
display: flex; align-items: center; justify-content: center; gap: 6px;
|
|
314
370
|
}
|
|
315
371
|
.us-footer svg { width: 11px; height: 11px; opacity: 0.7; }
|
|
316
372
|
.us-footer a { color: var(--us-muted); text-decoration: none; font-weight: 500; }
|
|
317
373
|
.us-footer a:hover { color: var(--us-fg); }
|
|
318
374
|
|
|
375
|
+
/* \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 */
|
|
376
|
+
.us-source-tabs {
|
|
377
|
+
display: inline-flex; gap: 2px; padding: 3px;
|
|
378
|
+
background: var(--us-subtle);
|
|
379
|
+
border: 1px solid var(--us-border);
|
|
380
|
+
border-radius: 8px;
|
|
381
|
+
margin-bottom: 14px;
|
|
382
|
+
}
|
|
383
|
+
.us-source-tab {
|
|
384
|
+
appearance: none; background: transparent; border: 1px solid transparent;
|
|
385
|
+
font: inherit; cursor: pointer;
|
|
386
|
+
padding: 6px 14px; min-height: 30px;
|
|
387
|
+
border-radius: 6px;
|
|
388
|
+
font-size: 12px; font-weight: 500; letter-spacing: 0.02em;
|
|
389
|
+
color: var(--us-muted);
|
|
390
|
+
transition: color 100ms, background 0ms;
|
|
391
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
392
|
+
}
|
|
393
|
+
.us-source-tab svg { width: 13px; height: 13px; }
|
|
394
|
+
.us-source-tab:hover { color: var(--us-fg); }
|
|
395
|
+
.us-source-tab[data-active="true"] {
|
|
396
|
+
background: var(--us-raised);
|
|
397
|
+
border-color: var(--us-border);
|
|
398
|
+
color: var(--us-fg);
|
|
399
|
+
}
|
|
400
|
+
.us-source-tab:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 2px; }
|
|
401
|
+
|
|
402
|
+
/* \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 */
|
|
403
|
+
.us-url-source { display: none; }
|
|
404
|
+
.us-url-source[data-active="true"] { display: block; }
|
|
405
|
+
.us-url-form {
|
|
406
|
+
display: flex; gap: 8px;
|
|
407
|
+
padding: 16px;
|
|
408
|
+
border: 1px dashed var(--us-border-strong);
|
|
409
|
+
border-radius: 8px;
|
|
410
|
+
background: var(--us-subtle);
|
|
411
|
+
}
|
|
412
|
+
.us-url-input {
|
|
413
|
+
appearance: none;
|
|
414
|
+
flex: 1; min-width: 0;
|
|
415
|
+
padding: 9px 12px; min-height: 36px;
|
|
416
|
+
border-radius: 8px; border: 1px solid var(--us-border-strong);
|
|
417
|
+
background: var(--us-bg); color: var(--us-fg);
|
|
418
|
+
font: inherit; font-size: 13px; letter-spacing: -0.006em;
|
|
419
|
+
transition: border-color 120ms, box-shadow 120ms;
|
|
420
|
+
}
|
|
421
|
+
.us-url-input::placeholder { color: var(--us-muted); }
|
|
422
|
+
.us-url-input:focus {
|
|
423
|
+
outline: none;
|
|
424
|
+
border-color: var(--us-primary);
|
|
425
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--us-primary) 25%, transparent);
|
|
426
|
+
}
|
|
427
|
+
.us-url-hint {
|
|
428
|
+
margin-top: 8px;
|
|
429
|
+
font-size: 11.5px; color: var(--us-muted); letter-spacing: -0.006em;
|
|
430
|
+
}
|
|
431
|
+
.us-url-hint[data-error="true"] { color: var(--us-danger); }
|
|
432
|
+
|
|
433
|
+
/* \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 */
|
|
434
|
+
.us-file-actions { display: inline-flex; gap: 4px; }
|
|
435
|
+
.us-file-action {
|
|
436
|
+
appearance: none; background: transparent; border: 0; cursor: pointer;
|
|
437
|
+
width: 28px; height: 28px;
|
|
438
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
439
|
+
color: var(--us-muted); border-radius: 6px;
|
|
440
|
+
transition: color 100ms, background 0ms;
|
|
441
|
+
}
|
|
442
|
+
.us-file-action:hover { background: var(--us-overlay); color: var(--us-fg); }
|
|
443
|
+
.us-file-action:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 1px; }
|
|
444
|
+
.us-file-action svg { width: 15px; height: 15px; }
|
|
445
|
+
.us-file-action[data-edited="true"] { color: var(--us-accent); }
|
|
446
|
+
.us-file:not([data-state="queued"]) .us-file-action { display: none; }
|
|
447
|
+
|
|
448
|
+
/* \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 */
|
|
449
|
+
.us-editor {
|
|
450
|
+
position: absolute; inset: 0; z-index: 2;
|
|
451
|
+
display: flex; flex-direction: column;
|
|
452
|
+
background: var(--us-bg);
|
|
453
|
+
animation: us-fade 140ms ease-out;
|
|
454
|
+
}
|
|
455
|
+
.us-editor-header {
|
|
456
|
+
display: flex; align-items: center; gap: 12px;
|
|
457
|
+
padding: 14px 16px;
|
|
458
|
+
border-bottom: 1px solid var(--us-border);
|
|
459
|
+
}
|
|
460
|
+
.us-editor-title { font-weight: 600; font-size: 15px; flex: 1; letter-spacing: -0.02em; }
|
|
461
|
+
.us-editor-back {
|
|
462
|
+
appearance: none; background: transparent; border: 0; cursor: pointer;
|
|
463
|
+
width: 32px; height: 32px; border-radius: 8px;
|
|
464
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
465
|
+
color: var(--us-muted);
|
|
466
|
+
transition: color 100ms, background 0ms;
|
|
467
|
+
}
|
|
468
|
+
.us-editor-back:hover { background: var(--us-overlay); color: var(--us-fg); }
|
|
469
|
+
.us-editor-back svg { width: 16px; height: 16px; }
|
|
470
|
+
|
|
471
|
+
.us-editor-canvas-wrap {
|
|
472
|
+
flex: 1; min-height: 0;
|
|
473
|
+
background: var(--us-subtle);
|
|
474
|
+
display: flex; align-items: center; justify-content: center;
|
|
475
|
+
padding: 16px;
|
|
476
|
+
position: relative;
|
|
477
|
+
overflow: hidden;
|
|
478
|
+
}
|
|
479
|
+
.us-editor-canvas {
|
|
480
|
+
max-width: 100%; max-height: 100%;
|
|
481
|
+
border-radius: 4px;
|
|
482
|
+
display: block;
|
|
483
|
+
/* Checkerboard for transparency awareness (visible only in circle mode). */
|
|
484
|
+
background-image:
|
|
485
|
+
linear-gradient(45deg, var(--us-border) 25%, transparent 25%, transparent 75%, var(--us-border) 75%),
|
|
486
|
+
linear-gradient(45deg, var(--us-border) 25%, transparent 25%, transparent 75%, var(--us-border) 75%);
|
|
487
|
+
background-size: 12px 12px;
|
|
488
|
+
background-position: 0 0, 6px 6px;
|
|
489
|
+
}
|
|
490
|
+
.us-editor-overlay {
|
|
491
|
+
position: absolute; inset: 0;
|
|
492
|
+
pointer-events: none;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.us-editor-toolbar {
|
|
496
|
+
display: flex; gap: 6px; flex-wrap: wrap;
|
|
497
|
+
padding: 10px 16px;
|
|
498
|
+
border-top: 1px solid var(--us-border);
|
|
499
|
+
}
|
|
500
|
+
.us-tool {
|
|
501
|
+
appearance: none; background: transparent; border: 1px solid var(--us-border-strong);
|
|
502
|
+
font: inherit; cursor: pointer;
|
|
503
|
+
padding: 6px 11px; min-height: 32px;
|
|
504
|
+
border-radius: 8px;
|
|
505
|
+
font-size: 12px; font-weight: 500; letter-spacing: 0.02em;
|
|
506
|
+
color: var(--us-fg);
|
|
507
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
508
|
+
transition: border-color 120ms, color 100ms, background 0ms;
|
|
509
|
+
}
|
|
510
|
+
.us-tool:hover { background: var(--us-overlay); }
|
|
511
|
+
.us-tool[data-active="true"] {
|
|
512
|
+
background: color-mix(in srgb, var(--us-primary) 12%, var(--us-bg));
|
|
513
|
+
border-color: var(--us-primary);
|
|
514
|
+
color: var(--us-primary);
|
|
515
|
+
}
|
|
516
|
+
.us-tool[disabled] { opacity: 0.45; cursor: not-allowed; }
|
|
517
|
+
.us-tool svg { width: 13px; height: 13px; }
|
|
518
|
+
.us-tool-spacer { flex: 1; }
|
|
519
|
+
|
|
520
|
+
.us-editor-footer {
|
|
521
|
+
display: flex; gap: 8px; justify-content: flex-end;
|
|
522
|
+
padding: 12px 16px;
|
|
523
|
+
border-top: 1px solid var(--us-border);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/* Crop drag handles. Drawn inside .us-editor-canvas-wrap, positioned over canvas. */
|
|
527
|
+
.us-crop-box {
|
|
528
|
+
position: absolute;
|
|
529
|
+
border: 1.5px solid var(--us-primary);
|
|
530
|
+
box-shadow: 0 0 0 9999px rgba(0,0,0,0.45);
|
|
531
|
+
pointer-events: auto;
|
|
532
|
+
cursor: move;
|
|
533
|
+
}
|
|
534
|
+
.us-crop-handle {
|
|
535
|
+
position: absolute;
|
|
536
|
+
width: 12px; height: 12px;
|
|
537
|
+
background: var(--us-bg);
|
|
538
|
+
border: 1.5px solid var(--us-primary);
|
|
539
|
+
border-radius: 2px;
|
|
540
|
+
pointer-events: auto;
|
|
541
|
+
}
|
|
542
|
+
.us-crop-handle[data-pos="nw"] { top: -6px; left: -6px; cursor: nwse-resize; }
|
|
543
|
+
.us-crop-handle[data-pos="ne"] { top: -6px; right: -6px; cursor: nesw-resize; }
|
|
544
|
+
.us-crop-handle[data-pos="sw"] { bottom: -6px; left: -6px; cursor: nesw-resize; }
|
|
545
|
+
.us-crop-handle[data-pos="se"] { bottom: -6px; right: -6px; cursor: nwse-resize; }
|
|
546
|
+
|
|
547
|
+
/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 mobile (bottom sheet) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
548
|
+
@media (max-width: 600px) {
|
|
549
|
+
.us-picker-backdrop {
|
|
550
|
+
padding: 0;
|
|
551
|
+
align-items: flex-end;
|
|
552
|
+
}
|
|
553
|
+
.us-picker {
|
|
554
|
+
max-width: none;
|
|
555
|
+
max-height: calc(100dvh - 40px);
|
|
556
|
+
border-radius: 16px 16px 0 0;
|
|
557
|
+
border-left: 0; border-right: 0; border-bottom: 0;
|
|
558
|
+
animation: us-sheet-up 300ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
559
|
+
}
|
|
560
|
+
/* Grab handle */
|
|
561
|
+
.us-picker::before {
|
|
562
|
+
content: "";
|
|
563
|
+
flex-shrink: 0;
|
|
564
|
+
width: 36px; height: 4px;
|
|
565
|
+
border-radius: 999px;
|
|
566
|
+
background: var(--us-border-strong);
|
|
567
|
+
margin: 8px auto 0;
|
|
568
|
+
}
|
|
569
|
+
.us-picker-header { padding: 10px 16px 14px; }
|
|
570
|
+
.us-dropzone { padding: 24px 16px; }
|
|
571
|
+
.us-btn { min-height: 44px; padding: 10px 16px; }
|
|
572
|
+
.us-actions {
|
|
573
|
+
flex-direction: column; align-items: stretch; gap: 10px;
|
|
574
|
+
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
|
575
|
+
}
|
|
576
|
+
.us-actions-summary { text-align: center; order: 2; }
|
|
577
|
+
.us-actions-summary:empty { display: none; }
|
|
578
|
+
.us-actions-buttons { width: 100%; }
|
|
579
|
+
.us-actions-buttons .us-btn { flex: 1; }
|
|
580
|
+
.us-source-tabs { display: flex; width: 100%; }
|
|
581
|
+
.us-source-tab { flex: 1; justify-content: center; min-height: 38px; }
|
|
582
|
+
.us-url-form { flex-direction: column; }
|
|
583
|
+
.us-url-input { min-height: 44px; }
|
|
584
|
+
.us-file { padding: 12px; }
|
|
585
|
+
.us-file-action, .us-picker-close { width: 36px; height: 36px; }
|
|
586
|
+
.us-editor-toolbar { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
587
|
+
.us-tool { flex-shrink: 0; min-height: 40px; }
|
|
588
|
+
.us-editor-footer { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
|
|
589
|
+
.us-editor-footer .us-btn { flex: 1; }
|
|
590
|
+
.us-footer { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
|
|
591
|
+
}
|
|
592
|
+
@keyframes us-sheet-up {
|
|
593
|
+
from { opacity: 0.6; transform: translateY(32px); }
|
|
594
|
+
to { opacity: 1; transform: translateY(0); }
|
|
595
|
+
}
|
|
596
|
+
|
|
319
597
|
/* \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
598
|
@media (prefers-reduced-motion: reduce) {
|
|
321
599
|
.us-picker-backdrop,
|
|
@@ -325,7 +603,10 @@ var BASE_CSS = `
|
|
|
325
603
|
.us-dropzone-icon,
|
|
326
604
|
.us-btn,
|
|
327
605
|
.us-file-status,
|
|
328
|
-
.us-file-progress-bar
|
|
606
|
+
.us-file-progress-bar,
|
|
607
|
+
.us-editor,
|
|
608
|
+
.us-source-tab,
|
|
609
|
+
.us-tool { animation: none !important; transition: none !important; }
|
|
329
610
|
.us-file[data-state="uploading"] .us-file-progress-bar::after { animation: none; opacity: 0; }
|
|
330
611
|
}
|
|
331
612
|
|
|
@@ -333,6 +614,409 @@ var BASE_CSS = `
|
|
|
333
614
|
.us-file-list:empty { display: none; }
|
|
334
615
|
`;
|
|
335
616
|
|
|
617
|
+
// src/picker/imageEditor.ts
|
|
618
|
+
var MAX_DISPLAY = 720;
|
|
619
|
+
var ICON = {
|
|
620
|
+
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>`,
|
|
621
|
+
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>`,
|
|
622
|
+
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>`,
|
|
623
|
+
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>`,
|
|
624
|
+
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>`,
|
|
625
|
+
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>`
|
|
626
|
+
};
|
|
627
|
+
var ImageEditor = class {
|
|
628
|
+
constructor(opts) {
|
|
629
|
+
this.opts = opts;
|
|
630
|
+
this.cropBox = null;
|
|
631
|
+
// What's currently rendered to the user, sized to fit MAX_DISPLAY.
|
|
632
|
+
this.displayScale = 1;
|
|
633
|
+
// Tracks whether the working canvas has transparency (post-circle), so
|
|
634
|
+
// export() can pick PNG vs JPEG correctly.
|
|
635
|
+
this.hasAlpha = false;
|
|
636
|
+
this.mode = "none";
|
|
637
|
+
this.cropRect = null;
|
|
638
|
+
// display-canvas coords
|
|
639
|
+
// Drag state for crop interaction.
|
|
640
|
+
this.dragKind = null;
|
|
641
|
+
this.dragStart = null;
|
|
642
|
+
this.onPointerMove = (e) => {
|
|
643
|
+
if (!this.dragKind || !this.dragStart || !this.cropRect) return;
|
|
644
|
+
const dx = e.clientX - this.dragStart.px;
|
|
645
|
+
const dy = e.clientY - this.dragStart.py;
|
|
646
|
+
const start = this.dragStart.rect;
|
|
647
|
+
const maxW = this.displayCanvas.width;
|
|
648
|
+
const maxH = this.displayCanvas.height;
|
|
649
|
+
const min = 24;
|
|
650
|
+
let { x, y, w, h } = start;
|
|
651
|
+
switch (this.dragKind) {
|
|
652
|
+
case "move":
|
|
653
|
+
x = clamp(start.x + dx, 0, maxW - start.w);
|
|
654
|
+
y = clamp(start.y + dy, 0, maxH - start.h);
|
|
655
|
+
break;
|
|
656
|
+
case "nw": {
|
|
657
|
+
const nx = clamp(start.x + dx, 0, start.x + start.w - min);
|
|
658
|
+
const ny = clamp(start.y + dy, 0, start.y + start.h - min);
|
|
659
|
+
w = start.w + (start.x - nx);
|
|
660
|
+
h = start.h + (start.y - ny);
|
|
661
|
+
x = nx;
|
|
662
|
+
y = ny;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case "ne": {
|
|
666
|
+
const nw = clamp(start.w + dx, min, maxW - start.x);
|
|
667
|
+
const ny = clamp(start.y + dy, 0, start.y + start.h - min);
|
|
668
|
+
h = start.h + (start.y - ny);
|
|
669
|
+
w = nw;
|
|
670
|
+
y = ny;
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case "sw": {
|
|
674
|
+
const nx = clamp(start.x + dx, 0, start.x + start.w - min);
|
|
675
|
+
const nh = clamp(start.h + dy, min, maxH - start.y);
|
|
676
|
+
w = start.w + (start.x - nx);
|
|
677
|
+
h = nh;
|
|
678
|
+
x = nx;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
case "se":
|
|
682
|
+
w = clamp(start.w + dx, min, maxW - start.x);
|
|
683
|
+
h = clamp(start.h + dy, min, maxH - start.y);
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
this.cropRect = { x, y, w, h };
|
|
687
|
+
this.updateCropBox();
|
|
688
|
+
};
|
|
689
|
+
this.onPointerUp = () => {
|
|
690
|
+
this.dragKind = null;
|
|
691
|
+
this.dragStart = null;
|
|
692
|
+
document.removeEventListener("pointermove", this.onPointerMove);
|
|
693
|
+
document.removeEventListener("pointerup", this.onPointerUp);
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async open() {
|
|
697
|
+
this.mount();
|
|
698
|
+
try {
|
|
699
|
+
await this.loadOriginalIntoWorking();
|
|
700
|
+
if (this.opts.originalFile && this.opts.originalFile !== this.opts.file) {
|
|
701
|
+
this.markEdited(true);
|
|
702
|
+
}
|
|
703
|
+
this.draw();
|
|
704
|
+
} catch (err) {
|
|
705
|
+
this.canvasWrap.textContent = `Couldn't load image: ${err.message}`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
close() {
|
|
709
|
+
document.removeEventListener("pointermove", this.onPointerMove);
|
|
710
|
+
document.removeEventListener("pointerup", this.onPointerUp);
|
|
711
|
+
this.root.remove();
|
|
712
|
+
}
|
|
713
|
+
// ---- mount / dom --------------------------------------------------------
|
|
714
|
+
mount() {
|
|
715
|
+
this.root = el("div", "us-editor");
|
|
716
|
+
const header = el("div", "us-editor-header");
|
|
717
|
+
const back = document.createElement("button");
|
|
718
|
+
back.type = "button";
|
|
719
|
+
back.className = "us-editor-back";
|
|
720
|
+
back.setAttribute("aria-label", "Back");
|
|
721
|
+
back.innerHTML = ICON.back;
|
|
722
|
+
back.onclick = () => {
|
|
723
|
+
this.opts.onCancel();
|
|
724
|
+
this.close();
|
|
725
|
+
};
|
|
726
|
+
header.appendChild(back);
|
|
727
|
+
header.appendChild(el("div", "us-editor-title", this.opts.title));
|
|
728
|
+
this.root.appendChild(header);
|
|
729
|
+
this.canvasWrap = el("div", "us-editor-canvas-wrap");
|
|
730
|
+
this.displayCanvas = document.createElement("canvas");
|
|
731
|
+
this.displayCanvas.className = "us-editor-canvas";
|
|
732
|
+
this.canvasWrap.appendChild(this.displayCanvas);
|
|
733
|
+
this.root.appendChild(this.canvasWrap);
|
|
734
|
+
const toolbar = el("div", "us-editor-toolbar");
|
|
735
|
+
this.cropTool = makeTool("Crop", ICON.crop);
|
|
736
|
+
this.cropTool.onclick = () => this.toggleCropMode();
|
|
737
|
+
this.circleTool = makeTool("Circle", ICON.circle);
|
|
738
|
+
this.circleTool.onclick = () => this.applyCircle();
|
|
739
|
+
this.rotateTool = makeTool("Rotate 90\xB0", ICON.rotate);
|
|
740
|
+
this.rotateTool.onclick = () => this.applyRotate();
|
|
741
|
+
this.revertTool = makeTool("Revert", ICON.undo);
|
|
742
|
+
this.revertTool.onclick = () => this.revert();
|
|
743
|
+
this.revertTool.disabled = true;
|
|
744
|
+
toolbar.appendChild(this.cropTool);
|
|
745
|
+
toolbar.appendChild(this.circleTool);
|
|
746
|
+
toolbar.appendChild(this.rotateTool);
|
|
747
|
+
toolbar.appendChild(el("div", "us-tool-spacer"));
|
|
748
|
+
toolbar.appendChild(this.revertTool);
|
|
749
|
+
this.root.appendChild(toolbar);
|
|
750
|
+
const footer = el("div", "us-editor-footer");
|
|
751
|
+
const cancel = document.createElement("button");
|
|
752
|
+
cancel.type = "button";
|
|
753
|
+
cancel.className = "us-btn";
|
|
754
|
+
cancel.textContent = "Cancel";
|
|
755
|
+
cancel.onclick = () => {
|
|
756
|
+
this.opts.onCancel();
|
|
757
|
+
this.close();
|
|
758
|
+
};
|
|
759
|
+
this.applyBtn = document.createElement("button");
|
|
760
|
+
this.applyBtn.type = "button";
|
|
761
|
+
this.applyBtn.className = "us-btn us-btn-primary";
|
|
762
|
+
this.applyBtn.innerHTML = `${ICON.check} <span>Apply</span>`;
|
|
763
|
+
this.applyBtn.onclick = () => this.applyAndClose();
|
|
764
|
+
footer.appendChild(cancel);
|
|
765
|
+
footer.appendChild(this.applyBtn);
|
|
766
|
+
this.root.appendChild(footer);
|
|
767
|
+
this.opts.host.appendChild(this.root);
|
|
768
|
+
}
|
|
769
|
+
// ---- image loading ------------------------------------------------------
|
|
770
|
+
async loadOriginalIntoWorking() {
|
|
771
|
+
const img = await loadImage(this.opts.file);
|
|
772
|
+
const c = document.createElement("canvas");
|
|
773
|
+
c.width = img.naturalWidth;
|
|
774
|
+
c.height = img.naturalHeight;
|
|
775
|
+
const ctx = c.getContext("2d");
|
|
776
|
+
if (!ctx) throw new Error("Canvas 2D context unavailable");
|
|
777
|
+
ctx.drawImage(img, 0, 0);
|
|
778
|
+
this.working = c;
|
|
779
|
+
this.hasAlpha = looksLikePngMime(this.opts.file.type);
|
|
780
|
+
this.markEdited(false);
|
|
781
|
+
}
|
|
782
|
+
// For Revert. Loads the true original (passed-in `originalFile`, or `file`
|
|
783
|
+
// if the caller didn't distinguish).
|
|
784
|
+
async loadTrueOriginalIntoWorking() {
|
|
785
|
+
const source = this.opts.originalFile ?? this.opts.file;
|
|
786
|
+
const img = await loadImage(source);
|
|
787
|
+
const c = document.createElement("canvas");
|
|
788
|
+
c.width = img.naturalWidth;
|
|
789
|
+
c.height = img.naturalHeight;
|
|
790
|
+
const ctx = c.getContext("2d");
|
|
791
|
+
if (!ctx) throw new Error("Canvas 2D context unavailable");
|
|
792
|
+
ctx.drawImage(img, 0, 0);
|
|
793
|
+
this.working = c;
|
|
794
|
+
this.hasAlpha = looksLikePngMime(source.type);
|
|
795
|
+
this.markEdited(false);
|
|
796
|
+
}
|
|
797
|
+
// ---- rendering ----------------------------------------------------------
|
|
798
|
+
draw() {
|
|
799
|
+
const { width: ww, height: wh } = this.working;
|
|
800
|
+
const scale = Math.min(1, MAX_DISPLAY / Math.max(ww, wh));
|
|
801
|
+
const dw = Math.max(1, Math.round(ww * scale));
|
|
802
|
+
const dh = Math.max(1, Math.round(wh * scale));
|
|
803
|
+
this.displayCanvas.width = dw;
|
|
804
|
+
this.displayCanvas.height = dh;
|
|
805
|
+
this.displayScale = scale;
|
|
806
|
+
const ctx = this.displayCanvas.getContext("2d");
|
|
807
|
+
if (!ctx) return;
|
|
808
|
+
ctx.clearRect(0, 0, dw, dh);
|
|
809
|
+
ctx.drawImage(this.working, 0, 0, dw, dh);
|
|
810
|
+
this.updateCropBox();
|
|
811
|
+
}
|
|
812
|
+
// ---- mode: crop ---------------------------------------------------------
|
|
813
|
+
toggleCropMode() {
|
|
814
|
+
if (this.mode === "crop") {
|
|
815
|
+
this.applyCrop();
|
|
816
|
+
} else {
|
|
817
|
+
this.enterCropMode();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
enterCropMode() {
|
|
821
|
+
this.mode = "crop";
|
|
822
|
+
this.cropTool.dataset.active = "true";
|
|
823
|
+
this.cropTool.innerHTML = `${ICON.check} <span>Apply crop</span>`;
|
|
824
|
+
const dw = this.displayCanvas.width;
|
|
825
|
+
const dh = this.displayCanvas.height;
|
|
826
|
+
const inset = Math.round(Math.min(dw, dh) * 0.1);
|
|
827
|
+
this.cropRect = { x: inset, y: inset, w: dw - inset * 2, h: dh - inset * 2 };
|
|
828
|
+
this.renderCropBox();
|
|
829
|
+
}
|
|
830
|
+
exitCropMode() {
|
|
831
|
+
this.mode = "none";
|
|
832
|
+
this.cropTool.removeAttribute("data-active");
|
|
833
|
+
this.cropTool.innerHTML = `${ICON.crop} <span>Crop</span>`;
|
|
834
|
+
this.cropRect = null;
|
|
835
|
+
if (this.cropBox) {
|
|
836
|
+
this.cropBox.remove();
|
|
837
|
+
this.cropBox = null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
renderCropBox() {
|
|
841
|
+
if (!this.cropRect) return;
|
|
842
|
+
if (this.cropBox) this.cropBox.remove();
|
|
843
|
+
const box = el("div", "us-crop-box");
|
|
844
|
+
for (const pos of ["nw", "ne", "sw", "se"]) {
|
|
845
|
+
const h = el("div", "us-crop-handle");
|
|
846
|
+
h.dataset.pos = pos;
|
|
847
|
+
h.addEventListener("pointerdown", (e) => this.beginDrag(e, pos));
|
|
848
|
+
box.appendChild(h);
|
|
849
|
+
}
|
|
850
|
+
box.addEventListener("pointerdown", (e) => {
|
|
851
|
+
const target = e.target;
|
|
852
|
+
if (target.classList.contains("us-crop-handle")) return;
|
|
853
|
+
this.beginDrag(e, "move");
|
|
854
|
+
});
|
|
855
|
+
this.canvasWrap.appendChild(box);
|
|
856
|
+
this.cropBox = box;
|
|
857
|
+
this.updateCropBox();
|
|
858
|
+
}
|
|
859
|
+
updateCropBox() {
|
|
860
|
+
if (!this.cropBox || !this.cropRect) return;
|
|
861
|
+
const canvasRect = this.displayCanvas.getBoundingClientRect();
|
|
862
|
+
const wrapRect = this.canvasWrap.getBoundingClientRect();
|
|
863
|
+
const left = canvasRect.left - wrapRect.left + this.cropRect.x;
|
|
864
|
+
const top = canvasRect.top - wrapRect.top + this.cropRect.y;
|
|
865
|
+
this.cropBox.style.left = `${left}px`;
|
|
866
|
+
this.cropBox.style.top = `${top}px`;
|
|
867
|
+
this.cropBox.style.width = `${this.cropRect.w}px`;
|
|
868
|
+
this.cropBox.style.height = `${this.cropRect.h}px`;
|
|
869
|
+
}
|
|
870
|
+
// ---- drag handlers (crop) ----------------------------------------------
|
|
871
|
+
beginDrag(e, kind) {
|
|
872
|
+
if (!this.cropRect) return;
|
|
873
|
+
e.preventDefault();
|
|
874
|
+
this.dragKind = kind;
|
|
875
|
+
this.dragStart = { px: e.clientX, py: e.clientY, rect: { ...this.cropRect } };
|
|
876
|
+
document.addEventListener("pointermove", this.onPointerMove);
|
|
877
|
+
document.addEventListener("pointerup", this.onPointerUp);
|
|
878
|
+
}
|
|
879
|
+
// ---- operations (mutate working canvas) --------------------------------
|
|
880
|
+
applyCrop() {
|
|
881
|
+
if (!this.cropRect) {
|
|
882
|
+
this.exitCropMode();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const s = this.displayScale;
|
|
886
|
+
const sx = Math.round(this.cropRect.x / s);
|
|
887
|
+
const sy = Math.round(this.cropRect.y / s);
|
|
888
|
+
const sw = Math.round(this.cropRect.w / s);
|
|
889
|
+
const sh = Math.round(this.cropRect.h / s);
|
|
890
|
+
const next = document.createElement("canvas");
|
|
891
|
+
next.width = sw;
|
|
892
|
+
next.height = sh;
|
|
893
|
+
const ctx = next.getContext("2d");
|
|
894
|
+
if (!ctx) return;
|
|
895
|
+
ctx.drawImage(this.working, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
896
|
+
this.working = next;
|
|
897
|
+
this.exitCropMode();
|
|
898
|
+
this.markEdited(true);
|
|
899
|
+
this.draw();
|
|
900
|
+
}
|
|
901
|
+
applyRotate() {
|
|
902
|
+
const { width: w, height: h } = this.working;
|
|
903
|
+
const next = document.createElement("canvas");
|
|
904
|
+
next.width = h;
|
|
905
|
+
next.height = w;
|
|
906
|
+
const ctx = next.getContext("2d");
|
|
907
|
+
if (!ctx) return;
|
|
908
|
+
ctx.translate(h, 0);
|
|
909
|
+
ctx.rotate(Math.PI / 2);
|
|
910
|
+
ctx.drawImage(this.working, 0, 0);
|
|
911
|
+
this.working = next;
|
|
912
|
+
this.exitCropMode();
|
|
913
|
+
this.markEdited(true);
|
|
914
|
+
this.draw();
|
|
915
|
+
}
|
|
916
|
+
applyCircle() {
|
|
917
|
+
const { width: w, height: h } = this.working;
|
|
918
|
+
const size = Math.min(w, h);
|
|
919
|
+
const sx = Math.round((w - size) / 2);
|
|
920
|
+
const sy = Math.round((h - size) / 2);
|
|
921
|
+
const next = document.createElement("canvas");
|
|
922
|
+
next.width = size;
|
|
923
|
+
next.height = size;
|
|
924
|
+
const ctx = next.getContext("2d");
|
|
925
|
+
if (!ctx) return;
|
|
926
|
+
ctx.save();
|
|
927
|
+
ctx.beginPath();
|
|
928
|
+
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
|
|
929
|
+
ctx.closePath();
|
|
930
|
+
ctx.clip();
|
|
931
|
+
ctx.drawImage(this.working, sx, sy, size, size, 0, 0, size, size);
|
|
932
|
+
ctx.restore();
|
|
933
|
+
this.working = next;
|
|
934
|
+
this.hasAlpha = true;
|
|
935
|
+
this.exitCropMode();
|
|
936
|
+
this.markEdited(true);
|
|
937
|
+
this.draw();
|
|
938
|
+
}
|
|
939
|
+
async revert() {
|
|
940
|
+
await this.loadTrueOriginalIntoWorking();
|
|
941
|
+
this.exitCropMode();
|
|
942
|
+
this.draw();
|
|
943
|
+
}
|
|
944
|
+
markEdited(edited) {
|
|
945
|
+
this.revertTool.disabled = !edited;
|
|
946
|
+
}
|
|
947
|
+
// ---- apply --------------------------------------------------------------
|
|
948
|
+
async applyAndClose() {
|
|
949
|
+
this.applyBtn.disabled = true;
|
|
950
|
+
try {
|
|
951
|
+
const file = await this.exportFile();
|
|
952
|
+
this.opts.onApply(file);
|
|
953
|
+
this.close();
|
|
954
|
+
} catch (err) {
|
|
955
|
+
this.applyBtn.disabled = false;
|
|
956
|
+
console.error("[union-stack] image export failed", err);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
exportFile() {
|
|
960
|
+
return new Promise((resolve, reject) => {
|
|
961
|
+
const mime = this.hasAlpha ? "image/png" : "image/jpeg";
|
|
962
|
+
const quality = mime === "image/jpeg" ? 0.92 : void 0;
|
|
963
|
+
this.working.toBlob(
|
|
964
|
+
(blob) => {
|
|
965
|
+
if (!blob) return reject(new Error("toBlob returned null"));
|
|
966
|
+
const baseName = this.opts.file.name.replace(/\.[^.]+$/, "");
|
|
967
|
+
const ext = mime === "image/png" ? "png" : "jpg";
|
|
968
|
+
resolve(new File([blob], `${baseName}-edited.${ext}`, { type: mime }));
|
|
969
|
+
},
|
|
970
|
+
mime,
|
|
971
|
+
quality
|
|
972
|
+
);
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
function el(tag, className, text) {
|
|
977
|
+
const node = document.createElement(tag);
|
|
978
|
+
if (className) node.className = className;
|
|
979
|
+
if (text !== void 0) node.textContent = text;
|
|
980
|
+
return node;
|
|
981
|
+
}
|
|
982
|
+
function makeTool(label, iconHtml) {
|
|
983
|
+
const btn = document.createElement("button");
|
|
984
|
+
btn.type = "button";
|
|
985
|
+
btn.className = "us-tool";
|
|
986
|
+
btn.innerHTML = `${iconHtml} <span>${escapeText(label)}</span>`;
|
|
987
|
+
return btn;
|
|
988
|
+
}
|
|
989
|
+
function escapeText(s) {
|
|
990
|
+
return s.replace(/[&<>"']/g, (c) => ({
|
|
991
|
+
"&": "&",
|
|
992
|
+
"<": "<",
|
|
993
|
+
">": ">",
|
|
994
|
+
'"': """,
|
|
995
|
+
"'": "'"
|
|
996
|
+
})[c]);
|
|
997
|
+
}
|
|
998
|
+
function clamp(v, lo, hi) {
|
|
999
|
+
return Math.max(lo, Math.min(hi, v));
|
|
1000
|
+
}
|
|
1001
|
+
function loadImage(file) {
|
|
1002
|
+
return new Promise((resolve, reject) => {
|
|
1003
|
+
const url = URL.createObjectURL(file);
|
|
1004
|
+
const img = new Image();
|
|
1005
|
+
img.onload = () => {
|
|
1006
|
+
URL.revokeObjectURL(url);
|
|
1007
|
+
resolve(img);
|
|
1008
|
+
};
|
|
1009
|
+
img.onerror = () => {
|
|
1010
|
+
URL.revokeObjectURL(url);
|
|
1011
|
+
reject(new Error("decode failed"));
|
|
1012
|
+
};
|
|
1013
|
+
img.src = url;
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
function looksLikePngMime(mime) {
|
|
1017
|
+
return /^image\/(png|webp|gif)$/.test(mime);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
336
1020
|
// src/picker/picker.ts
|
|
337
1021
|
function mergeConfig(server, runtime) {
|
|
338
1022
|
const merged = { ...runtime };
|
|
@@ -362,8 +1046,9 @@ function mergeConfig(server, runtime) {
|
|
|
362
1046
|
}
|
|
363
1047
|
var DEFAULT_TITLE = "Upload files";
|
|
364
1048
|
var FOOTER_LINK = "https://unionstack.link";
|
|
365
|
-
var
|
|
1049
|
+
var ICON2 = {
|
|
366
1050
|
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>`,
|
|
1051
|
+
fileUp: `<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"/><path d="M12 18v-6"/><path d="m9 15 3-3 3 3"/></svg>`,
|
|
367
1052
|
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
1053
|
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>`,
|
|
369
1054
|
alert: `<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="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
|
|
@@ -374,28 +1059,39 @@ var ICON = {
|
|
|
374
1059
|
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
1060
|
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
1061
|
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
|
|
1062
|
+
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>`,
|
|
1063
|
+
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>`,
|
|
1064
|
+
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>`,
|
|
1065
|
+
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
1066
|
};
|
|
379
1067
|
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
|
|
1068
|
+
if (!mime) return ICON2.file;
|
|
1069
|
+
if (mime.startsWith("image/")) return ICON2.image;
|
|
1070
|
+
if (mime.startsWith("video/")) return ICON2.video;
|
|
1071
|
+
if (mime.startsWith("audio/")) return ICON2.audio;
|
|
1072
|
+
if (mime === "application/pdf") return ICON2.pdf;
|
|
1073
|
+
if (mime.startsWith("application/zip") || mime.includes("compressed") || mime === "application/x-tar" || mime === "application/gzip") return ICON2.archive;
|
|
1074
|
+
return ICON2.file;
|
|
387
1075
|
}
|
|
388
1076
|
var Picker = class {
|
|
389
1077
|
constructor(client, opts) {
|
|
390
1078
|
this.client = client;
|
|
391
1079
|
this.opts = opts;
|
|
392
1080
|
this.$backdrop = null;
|
|
1081
|
+
this.$panel = null;
|
|
393
1082
|
this.$list = null;
|
|
394
1083
|
this.$confirm = null;
|
|
395
1084
|
this.$cancel = null;
|
|
396
1085
|
this.$closeBtn = null;
|
|
397
1086
|
this.$input = null;
|
|
1087
|
+
// Source-tab DOM (Device vs URL). Only created when both sources are enabled.
|
|
1088
|
+
this.$deviceSource = null;
|
|
1089
|
+
this.$urlSource = null;
|
|
1090
|
+
this.$urlInput = null;
|
|
1091
|
+
this.$urlHint = null;
|
|
1092
|
+
this.$urlAddBtn = null;
|
|
398
1093
|
this.items = [];
|
|
1094
|
+
this.editor = null;
|
|
399
1095
|
this.abortCtrl = new AbortController();
|
|
400
1096
|
this.uploadStarted = false;
|
|
401
1097
|
this.resolved = false;
|
|
@@ -438,38 +1134,50 @@ var Picker = class {
|
|
|
438
1134
|
root.addEventListener("click", (e) => {
|
|
439
1135
|
if (e.target === root && !this.uploadStarted) this.cancel();
|
|
440
1136
|
});
|
|
441
|
-
const panel =
|
|
442
|
-
const header =
|
|
443
|
-
const logoWrap =
|
|
1137
|
+
const panel = el2("div", "us-picker");
|
|
1138
|
+
const header = el2("div", "us-picker-header");
|
|
1139
|
+
const logoWrap = el2("div", "us-picker-header-logo");
|
|
444
1140
|
if (this.opts.branding?.logoUrl) {
|
|
445
1141
|
const logo = document.createElement("img");
|
|
446
1142
|
logo.src = this.opts.branding.logoUrl;
|
|
447
1143
|
logo.alt = "";
|
|
448
1144
|
logoWrap.appendChild(logo);
|
|
449
1145
|
} else {
|
|
450
|
-
logoWrap.innerHTML =
|
|
1146
|
+
logoWrap.innerHTML = ICON2.zap;
|
|
451
1147
|
}
|
|
452
1148
|
header.appendChild(logoWrap);
|
|
453
|
-
const title =
|
|
1149
|
+
const title = el2("div", "us-picker-title", this.opts.branding?.title ?? DEFAULT_TITLE);
|
|
454
1150
|
header.appendChild(title);
|
|
455
1151
|
this.$closeBtn = document.createElement("button");
|
|
456
1152
|
this.$closeBtn.type = "button";
|
|
457
1153
|
this.$closeBtn.className = "us-picker-close";
|
|
458
1154
|
this.$closeBtn.setAttribute("aria-label", "Close");
|
|
459
|
-
this.$closeBtn.innerHTML =
|
|
1155
|
+
this.$closeBtn.innerHTML = ICON2.close;
|
|
460
1156
|
this.$closeBtn.onclick = () => this.cancel();
|
|
461
1157
|
header.appendChild(this.$closeBtn);
|
|
462
1158
|
panel.appendChild(header);
|
|
463
|
-
const body =
|
|
464
|
-
|
|
465
|
-
|
|
1159
|
+
const body = el2("div", "us-picker-body");
|
|
1160
|
+
const sources = this.resolvedSources();
|
|
1161
|
+
if (sources.length > 1) {
|
|
1162
|
+
body.appendChild(this.renderSourceTabs(sources));
|
|
1163
|
+
}
|
|
1164
|
+
if (sources.includes("device")) {
|
|
1165
|
+
this.$deviceSource = this.renderDropzone();
|
|
1166
|
+
body.appendChild(this.$deviceSource);
|
|
1167
|
+
}
|
|
1168
|
+
if (sources.includes("url")) {
|
|
1169
|
+
this.$urlSource = this.renderUrlSource();
|
|
1170
|
+
body.appendChild(this.$urlSource);
|
|
1171
|
+
}
|
|
1172
|
+
this.activateSource(sources[0] ?? "device");
|
|
1173
|
+
this.$list = el2("div", "us-file-list");
|
|
466
1174
|
body.appendChild(this.$list);
|
|
467
1175
|
panel.appendChild(body);
|
|
468
|
-
const autoUpload = this.opts.autoUpload
|
|
469
|
-
const actions =
|
|
470
|
-
this.$summary =
|
|
1176
|
+
const autoUpload = this.opts.autoUpload === true;
|
|
1177
|
+
const actions = el2("div", "us-actions");
|
|
1178
|
+
this.$summary = el2("div", "us-actions-summary", "");
|
|
471
1179
|
actions.appendChild(this.$summary);
|
|
472
|
-
const buttons =
|
|
1180
|
+
const buttons = el2("div", "us-actions-buttons");
|
|
473
1181
|
this.$cancel = document.createElement("button");
|
|
474
1182
|
this.$cancel.type = "button";
|
|
475
1183
|
this.$cancel.className = "us-btn";
|
|
@@ -480,7 +1188,7 @@ var Picker = class {
|
|
|
480
1188
|
this.$confirm = document.createElement("button");
|
|
481
1189
|
this.$confirm.type = "button";
|
|
482
1190
|
this.$confirm.className = "us-btn us-btn-primary";
|
|
483
|
-
this.$confirm.innerHTML = `${
|
|
1191
|
+
this.$confirm.innerHTML = `${ICON2.upload} <span>Upload</span>`;
|
|
484
1192
|
this.$confirm.disabled = true;
|
|
485
1193
|
this.$confirm.onclick = () => this.startUpload();
|
|
486
1194
|
buttons.appendChild(this.$confirm);
|
|
@@ -488,24 +1196,123 @@ var Picker = class {
|
|
|
488
1196
|
actions.appendChild(buttons);
|
|
489
1197
|
panel.appendChild(actions);
|
|
490
1198
|
if (!this.opts.branding?.hideFooter) {
|
|
491
|
-
const footer =
|
|
492
|
-
footer.innerHTML = `${
|
|
1199
|
+
const footer = el2("div", "us-footer");
|
|
1200
|
+
footer.innerHTML = `${ICON2.zap} <span>Powered by <a href="${FOOTER_LINK}" target="_blank" rel="noopener">UnionStack</a></span>`;
|
|
493
1201
|
panel.appendChild(footer);
|
|
494
1202
|
}
|
|
495
1203
|
root.appendChild(panel);
|
|
496
1204
|
(this.opts.container ?? document.body).appendChild(root);
|
|
497
1205
|
this.$backdrop = root;
|
|
1206
|
+
this.$panel = panel;
|
|
1207
|
+
}
|
|
1208
|
+
// Resolves which sources to show, honoring opts.fromSources, defaulting to
|
|
1209
|
+
// both. Empty arrays fall back to ['device'] — we never want to leave a
|
|
1210
|
+
// picker with no way to add files.
|
|
1211
|
+
resolvedSources() {
|
|
1212
|
+
const raw = this.opts.fromSources;
|
|
1213
|
+
if (!raw || raw.length === 0) return ["device", "url"];
|
|
1214
|
+
return raw;
|
|
1215
|
+
}
|
|
1216
|
+
renderSourceTabs(sources) {
|
|
1217
|
+
const tabs = el2("div", "us-source-tabs");
|
|
1218
|
+
tabs.setAttribute("role", "tablist");
|
|
1219
|
+
for (const src of sources) {
|
|
1220
|
+
const btn = document.createElement("button");
|
|
1221
|
+
btn.type = "button";
|
|
1222
|
+
btn.className = "us-source-tab";
|
|
1223
|
+
btn.setAttribute("role", "tab");
|
|
1224
|
+
btn.dataset.source = src;
|
|
1225
|
+
btn.innerHTML = src === "device" ? `${ICON2.device} <span>My Device</span>` : `${ICON2.link} <span>Link</span>`;
|
|
1226
|
+
btn.onclick = () => this.activateSource(src);
|
|
1227
|
+
tabs.appendChild(btn);
|
|
1228
|
+
}
|
|
1229
|
+
return tabs;
|
|
1230
|
+
}
|
|
1231
|
+
activateSource(source) {
|
|
1232
|
+
if (this.$deviceSource) {
|
|
1233
|
+
const active = source === "device";
|
|
1234
|
+
this.$deviceSource.style.display = active ? "" : "none";
|
|
1235
|
+
}
|
|
1236
|
+
if (this.$urlSource) {
|
|
1237
|
+
this.$urlSource.dataset.active = source === "url" ? "true" : "false";
|
|
1238
|
+
}
|
|
1239
|
+
const tabs = this.$panel?.querySelectorAll(".us-source-tab");
|
|
1240
|
+
tabs?.forEach((t) => {
|
|
1241
|
+
t.dataset.active = t.dataset.source === source ? "true" : "false";
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
renderUrlSource() {
|
|
1245
|
+
const wrap = el2("div", "us-url-source");
|
|
1246
|
+
const form = el2("div", "us-url-form");
|
|
1247
|
+
const input = document.createElement("input");
|
|
1248
|
+
input.type = "url";
|
|
1249
|
+
input.className = "us-url-input";
|
|
1250
|
+
input.placeholder = "https://example.com/photo.jpg";
|
|
1251
|
+
input.setAttribute("aria-label", "File URL");
|
|
1252
|
+
input.onkeydown = (e) => {
|
|
1253
|
+
if (e.key === "Enter") {
|
|
1254
|
+
e.preventDefault();
|
|
1255
|
+
this.handleUrlAdd();
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
this.$urlInput = input;
|
|
1259
|
+
const add = document.createElement("button");
|
|
1260
|
+
add.type = "button";
|
|
1261
|
+
add.className = "us-btn us-btn-primary";
|
|
1262
|
+
add.textContent = "Add file";
|
|
1263
|
+
add.onclick = () => this.handleUrlAdd();
|
|
1264
|
+
this.$urlAddBtn = add;
|
|
1265
|
+
form.appendChild(input);
|
|
1266
|
+
form.appendChild(add);
|
|
1267
|
+
wrap.appendChild(form);
|
|
1268
|
+
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.");
|
|
1269
|
+
this.$urlHint = hint;
|
|
1270
|
+
wrap.appendChild(hint);
|
|
1271
|
+
return wrap;
|
|
1272
|
+
}
|
|
1273
|
+
async handleUrlAdd() {
|
|
1274
|
+
if (!this.$urlInput || !this.$urlAddBtn || !this.$urlHint) return;
|
|
1275
|
+
const url = this.$urlInput.value.trim();
|
|
1276
|
+
if (!url) {
|
|
1277
|
+
this.setUrlHint("Enter a URL first.", true);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
1281
|
+
this.setUrlHint("URL must start with http:// or https://", true);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
this.$urlAddBtn.disabled = true;
|
|
1285
|
+
const prevLabel = this.$urlAddBtn.textContent;
|
|
1286
|
+
this.$urlAddBtn.textContent = "Fetching\u2026";
|
|
1287
|
+
this.setUrlHint("Downloading\u2026", false);
|
|
1288
|
+
try {
|
|
1289
|
+
const file = await fetchUrlAsFile(url);
|
|
1290
|
+
this.addFiles([file]);
|
|
1291
|
+
this.$urlInput.value = "";
|
|
1292
|
+
this.setUrlHint("Paste a direct file URL. The host must allow CORS so we can fetch it from the browser.", false);
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
const msg = err.message || "Failed to fetch URL";
|
|
1295
|
+
this.setUrlHint(msg, true);
|
|
1296
|
+
} finally {
|
|
1297
|
+
this.$urlAddBtn.disabled = false;
|
|
1298
|
+
this.$urlAddBtn.textContent = prevLabel || "Add file";
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
setUrlHint(text, isError) {
|
|
1302
|
+
if (!this.$urlHint) return;
|
|
1303
|
+
this.$urlHint.textContent = text;
|
|
1304
|
+
this.$urlHint.dataset.error = isError ? "true" : "false";
|
|
498
1305
|
}
|
|
499
1306
|
renderDropzone() {
|
|
500
|
-
const dz =
|
|
1307
|
+
const dz = el2("div", "us-dropzone");
|
|
501
1308
|
dz.setAttribute("role", "button");
|
|
502
1309
|
dz.setAttribute("tabindex", "0");
|
|
503
1310
|
dz.setAttribute("aria-label", "Drop files here or click to browse");
|
|
504
|
-
const icon =
|
|
505
|
-
icon.innerHTML =
|
|
1311
|
+
const icon = el2("div", "us-dropzone-icon");
|
|
1312
|
+
icon.innerHTML = ICON2.fileUp;
|
|
506
1313
|
dz.appendChild(icon);
|
|
507
|
-
dz.appendChild(
|
|
508
|
-
dz.appendChild(
|
|
1314
|
+
dz.appendChild(el2("div", "us-dropzone-title", "Drop files to upload"));
|
|
1315
|
+
dz.appendChild(el2("div", "us-dropzone-hint", "or click to browse from your device"));
|
|
509
1316
|
const constraintBits = [];
|
|
510
1317
|
if (this.opts.maxFileSize) constraintBits.push(`max ${formatBytes(this.opts.maxFileSize)}`);
|
|
511
1318
|
if (this.opts.accept) {
|
|
@@ -515,7 +1322,7 @@ var Picker = class {
|
|
|
515
1322
|
}
|
|
516
1323
|
}
|
|
517
1324
|
if (constraintBits.length > 0) {
|
|
518
|
-
dz.appendChild(
|
|
1325
|
+
dz.appendChild(el2("div", "us-dropzone-constraints", constraintBits.join(" \xB7 ")));
|
|
519
1326
|
}
|
|
520
1327
|
const input = document.createElement("input");
|
|
521
1328
|
input.type = "file";
|
|
@@ -549,8 +1356,13 @@ var Picker = class {
|
|
|
549
1356
|
return dz;
|
|
550
1357
|
}
|
|
551
1358
|
unmount() {
|
|
1359
|
+
if (this.editor) {
|
|
1360
|
+
this.editor.close();
|
|
1361
|
+
this.editor = null;
|
|
1362
|
+
}
|
|
552
1363
|
if (this.$backdrop?.parentNode) this.$backdrop.parentNode.removeChild(this.$backdrop);
|
|
553
1364
|
this.$backdrop = null;
|
|
1365
|
+
this.$panel = null;
|
|
554
1366
|
for (const item of this.items) {
|
|
555
1367
|
if (item.objectUrl) URL.revokeObjectURL(item.objectUrl);
|
|
556
1368
|
}
|
|
@@ -567,6 +1379,8 @@ var Picker = class {
|
|
|
567
1379
|
const item2 = {
|
|
568
1380
|
uploadId: cryptoId(),
|
|
569
1381
|
file,
|
|
1382
|
+
originalFile: file,
|
|
1383
|
+
edited: false,
|
|
570
1384
|
state: "failed",
|
|
571
1385
|
progress: 0,
|
|
572
1386
|
error: `File exceeds ${formatBytes(this.opts.maxFileSize)} limit`
|
|
@@ -578,6 +1392,8 @@ var Picker = class {
|
|
|
578
1392
|
const item = {
|
|
579
1393
|
uploadId: cryptoId(),
|
|
580
1394
|
file,
|
|
1395
|
+
originalFile: file,
|
|
1396
|
+
edited: false,
|
|
581
1397
|
state: "queued",
|
|
582
1398
|
progress: 0
|
|
583
1399
|
};
|
|
@@ -585,7 +1401,7 @@ var Picker = class {
|
|
|
585
1401
|
this.renderItem(item);
|
|
586
1402
|
}
|
|
587
1403
|
this.refreshConfirm();
|
|
588
|
-
const autoUpload = this.opts.autoUpload
|
|
1404
|
+
const autoUpload = this.opts.autoUpload === true;
|
|
589
1405
|
const hasQueued = this.items.some((i) => i.state === "queued");
|
|
590
1406
|
if (autoUpload && hasQueued && !this.uploadStarted) {
|
|
591
1407
|
requestAnimationFrame(() => {
|
|
@@ -595,12 +1411,12 @@ var Picker = class {
|
|
|
595
1411
|
}
|
|
596
1412
|
renderItem(item) {
|
|
597
1413
|
if (!this.$list) return;
|
|
598
|
-
const row =
|
|
1414
|
+
const row = el2("div", "us-file");
|
|
599
1415
|
row.dataset.state = item.state;
|
|
600
1416
|
row.dataset.uploadId = item.uploadId;
|
|
601
1417
|
const idx = Math.min(this.items.length - 1, 8);
|
|
602
1418
|
row.style.animationDelay = `${idx * 35}ms`;
|
|
603
|
-
const thumb =
|
|
1419
|
+
const thumb = el2("div", "us-file-thumb");
|
|
604
1420
|
if (item.file.type.startsWith("image/") && typeof URL !== "undefined" && URL.createObjectURL) {
|
|
605
1421
|
try {
|
|
606
1422
|
const objectUrl = URL.createObjectURL(item.file);
|
|
@@ -614,18 +1430,32 @@ var Picker = class {
|
|
|
614
1430
|
thumb.innerHTML = iconForMime(item.file.type);
|
|
615
1431
|
}
|
|
616
1432
|
row.appendChild(thumb);
|
|
617
|
-
const main =
|
|
618
|
-
const row1 =
|
|
619
|
-
row1.appendChild(
|
|
620
|
-
const meta =
|
|
1433
|
+
const main = el2("div", "us-file-main");
|
|
1434
|
+
const row1 = el2("div", "us-file-row1");
|
|
1435
|
+
row1.appendChild(el2("div", "us-file-name", item.file.name));
|
|
1436
|
+
const meta = el2("div", "us-file-meta", formatBytes(item.file.size));
|
|
621
1437
|
row1.appendChild(meta);
|
|
622
1438
|
main.appendChild(row1);
|
|
623
|
-
const progress =
|
|
624
|
-
const bar =
|
|
1439
|
+
const progress = el2("div", "us-file-progress");
|
|
1440
|
+
const bar = el2("div", "us-file-progress-bar");
|
|
625
1441
|
progress.appendChild(bar);
|
|
626
1442
|
main.appendChild(progress);
|
|
627
1443
|
row.appendChild(main);
|
|
628
|
-
const
|
|
1444
|
+
const isImage = item.file.type.startsWith("image/");
|
|
1445
|
+
const editingEnabled = this.opts.imageEditing !== false;
|
|
1446
|
+
if (isImage && editingEnabled && item.state === "queued") {
|
|
1447
|
+
const edit = document.createElement("button");
|
|
1448
|
+
edit.type = "button";
|
|
1449
|
+
edit.className = "us-file-action";
|
|
1450
|
+
edit.setAttribute("aria-label", "Edit image");
|
|
1451
|
+
edit.title = "Edit image";
|
|
1452
|
+
edit.innerHTML = ICON2.pencil;
|
|
1453
|
+
edit.dataset.edited = item.edited ? "true" : "false";
|
|
1454
|
+
edit.onclick = () => this.openEditor(item);
|
|
1455
|
+
row.appendChild(edit);
|
|
1456
|
+
item.$edit = edit;
|
|
1457
|
+
}
|
|
1458
|
+
const status = el2("div", "us-file-status");
|
|
629
1459
|
status.setAttribute("aria-label", this.statusLabel(item));
|
|
630
1460
|
status.innerHTML = this.statusIcon(item.state);
|
|
631
1461
|
row.appendChild(status);
|
|
@@ -633,21 +1463,74 @@ var Picker = class {
|
|
|
633
1463
|
item.$bar = bar;
|
|
634
1464
|
item.$status = status;
|
|
635
1465
|
item.$meta = meta;
|
|
1466
|
+
item.$thumb = thumb;
|
|
636
1467
|
this.$list.appendChild(row);
|
|
637
1468
|
this.updateSummary();
|
|
638
1469
|
}
|
|
1470
|
+
// Swap a queued item's file with an edited version. Refreshes thumbnail and
|
|
1471
|
+
// meta in place so the row identity is preserved (uploadId, position).
|
|
1472
|
+
replaceItemFile(item, nextFile, edited) {
|
|
1473
|
+
item.file = nextFile;
|
|
1474
|
+
item.edited = edited;
|
|
1475
|
+
if (item.objectUrl) URL.revokeObjectURL(item.objectUrl);
|
|
1476
|
+
item.objectUrl = void 0;
|
|
1477
|
+
if (item.$thumb) {
|
|
1478
|
+
item.$thumb.style.backgroundImage = "";
|
|
1479
|
+
item.$thumb.removeAttribute("data-image");
|
|
1480
|
+
if (nextFile.type.startsWith("image/") && typeof URL !== "undefined" && URL.createObjectURL) {
|
|
1481
|
+
try {
|
|
1482
|
+
const objectUrl = URL.createObjectURL(nextFile);
|
|
1483
|
+
item.objectUrl = objectUrl;
|
|
1484
|
+
item.$thumb.style.backgroundImage = `url("${objectUrl}")`;
|
|
1485
|
+
item.$thumb.dataset.image = "true";
|
|
1486
|
+
item.$thumb.innerHTML = "";
|
|
1487
|
+
} catch {
|
|
1488
|
+
item.$thumb.innerHTML = iconForMime(nextFile.type);
|
|
1489
|
+
}
|
|
1490
|
+
} else {
|
|
1491
|
+
item.$thumb.innerHTML = iconForMime(nextFile.type);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (item.$meta) item.$meta.textContent = formatBytes(nextFile.size);
|
|
1495
|
+
if (item.$row) {
|
|
1496
|
+
const nameEl = item.$row.querySelector(".us-file-name");
|
|
1497
|
+
if (nameEl) nameEl.textContent = nextFile.name;
|
|
1498
|
+
}
|
|
1499
|
+
if (item.$edit) item.$edit.dataset.edited = edited ? "true" : "false";
|
|
1500
|
+
}
|
|
1501
|
+
// Mount the image editor overlay on top of the picker panel for the given
|
|
1502
|
+
// queued item. The editor calls back with either an edited File (Apply) or
|
|
1503
|
+
// nothing (Cancel/Back).
|
|
1504
|
+
openEditor(item) {
|
|
1505
|
+
if (!this.$panel || this.editor || item.state !== "queued") return;
|
|
1506
|
+
this.editor = new ImageEditor({
|
|
1507
|
+
host: this.$panel,
|
|
1508
|
+
file: item.file,
|
|
1509
|
+
originalFile: item.originalFile,
|
|
1510
|
+
title: item.originalFile.name,
|
|
1511
|
+
onApply: (edited) => {
|
|
1512
|
+
const wasEdited = edited !== item.originalFile;
|
|
1513
|
+
this.replaceItemFile(item, edited, wasEdited);
|
|
1514
|
+
this.editor = null;
|
|
1515
|
+
},
|
|
1516
|
+
onCancel: () => {
|
|
1517
|
+
this.editor = null;
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
this.editor.open();
|
|
1521
|
+
}
|
|
639
1522
|
statusIcon(state) {
|
|
640
1523
|
switch (state) {
|
|
641
1524
|
case "uploading":
|
|
642
|
-
return
|
|
1525
|
+
return ICON2.spinner;
|
|
643
1526
|
case "done":
|
|
644
|
-
return
|
|
1527
|
+
return ICON2.check;
|
|
645
1528
|
case "failed":
|
|
646
|
-
return
|
|
1529
|
+
return ICON2.alert;
|
|
647
1530
|
case "cancelled":
|
|
648
|
-
return
|
|
1531
|
+
return ICON2.alert;
|
|
649
1532
|
default:
|
|
650
|
-
return
|
|
1533
|
+
return ICON2.spinner;
|
|
651
1534
|
}
|
|
652
1535
|
}
|
|
653
1536
|
statusLabel(item) {
|
|
@@ -701,7 +1584,10 @@ var Picker = class {
|
|
|
701
1584
|
const done = this.items.filter((i) => i.state === "done").length;
|
|
702
1585
|
const failed = this.items.filter((i) => i.state === "failed").length;
|
|
703
1586
|
const active = this.items.filter((i) => i.state === "uploading" || i.state === "queued").length;
|
|
704
|
-
if (active > 0) {
|
|
1587
|
+
if (!this.uploadStarted && active > 0) {
|
|
1588
|
+
const ready = this.items.filter((i) => i.state === "queued").length;
|
|
1589
|
+
this.$summary.textContent = `${ready} file${ready === 1 ? "" : "s"} ready`;
|
|
1590
|
+
} else if (active > 0) {
|
|
705
1591
|
this.$summary.textContent = `Uploading ${done + 1} of ${total}`;
|
|
706
1592
|
} else if (failed > 0) {
|
|
707
1593
|
this.$summary.textContent = `${done} of ${total} uploaded \xB7 ${failed} failed`;
|
|
@@ -813,7 +1699,7 @@ var Picker = class {
|
|
|
813
1699
|
this.resolvePromise(fallback);
|
|
814
1700
|
}
|
|
815
1701
|
};
|
|
816
|
-
function
|
|
1702
|
+
function el2(tag, className, text) {
|
|
817
1703
|
const node = document.createElement(tag);
|
|
818
1704
|
if (className) node.className = className;
|
|
819
1705
|
if (text !== void 0) node.textContent = text;
|
|
@@ -831,6 +1717,56 @@ function cryptoId() {
|
|
|
831
1717
|
}
|
|
832
1718
|
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
833
1719
|
}
|
|
1720
|
+
async function fetchUrlAsFile(url) {
|
|
1721
|
+
let res;
|
|
1722
|
+
try {
|
|
1723
|
+
res = await fetch(url, { mode: "cors", credentials: "omit" });
|
|
1724
|
+
} catch {
|
|
1725
|
+
throw new Error(`Could not reach ${shortHost(url)} \u2014 check the URL or CORS.`);
|
|
1726
|
+
}
|
|
1727
|
+
if (!res.ok) throw new Error(`Server returned ${res.status} \u2014 file unavailable.`);
|
|
1728
|
+
const blob = await res.blob();
|
|
1729
|
+
const filename = filenameFromUrl(url) || "download";
|
|
1730
|
+
const type = blob.type || guessMimeFromExt(filename);
|
|
1731
|
+
return new File([blob], filename, { type });
|
|
1732
|
+
}
|
|
1733
|
+
function shortHost(url) {
|
|
1734
|
+
try {
|
|
1735
|
+
return new URL(url).host;
|
|
1736
|
+
} catch {
|
|
1737
|
+
return "that host";
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
function filenameFromUrl(url) {
|
|
1741
|
+
try {
|
|
1742
|
+
const u = new URL(url);
|
|
1743
|
+
const last = u.pathname.split("/").filter(Boolean).pop();
|
|
1744
|
+
return last ? decodeURIComponent(last) : "";
|
|
1745
|
+
} catch {
|
|
1746
|
+
return "";
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
function guessMimeFromExt(filename) {
|
|
1750
|
+
const ext = filename.toLowerCase().split(".").pop() || "";
|
|
1751
|
+
const map = {
|
|
1752
|
+
jpg: "image/jpeg",
|
|
1753
|
+
jpeg: "image/jpeg",
|
|
1754
|
+
png: "image/png",
|
|
1755
|
+
gif: "image/gif",
|
|
1756
|
+
webp: "image/webp",
|
|
1757
|
+
svg: "image/svg+xml",
|
|
1758
|
+
pdf: "application/pdf",
|
|
1759
|
+
mp4: "video/mp4",
|
|
1760
|
+
mov: "video/quicktime",
|
|
1761
|
+
mp3: "audio/mpeg",
|
|
1762
|
+
wav: "audio/wav",
|
|
1763
|
+
zip: "application/zip",
|
|
1764
|
+
json: "application/json",
|
|
1765
|
+
txt: "text/plain",
|
|
1766
|
+
csv: "text/csv"
|
|
1767
|
+
};
|
|
1768
|
+
return map[ext] || "application/octet-stream";
|
|
1769
|
+
}
|
|
834
1770
|
function openPicker(client, opts) {
|
|
835
1771
|
const picker = new Picker(client, opts);
|
|
836
1772
|
return {
|