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