@masters-union/union-stack 0.1.4 → 0.1.6

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/picker.cjs CHANGED
@@ -17,104 +17,322 @@ function themeToCssVars(theme) {
17
17
  "--us-primary": theme?.primary ?? defaults.primary,
18
18
  "--us-bg": theme?.background ?? defaults.background,
19
19
  "--us-fg": theme?.foreground ?? defaults.foreground,
20
- "--us-muted": theme?.muted ?? defaults.muted,
20
+ "--us-muted": defaults.muted,
21
+ "--us-subtle": defaults.subtle,
21
22
  "--us-border": theme?.border ?? defaults.border,
23
+ "--us-border-strong": defaults.borderStrong,
24
+ "--us-elevated": defaults.elevated,
25
+ "--us-success": defaults.success,
26
+ "--us-danger": defaults.danger,
22
27
  "--us-radius": theme?.radius ?? "12px"
23
28
  };
24
29
  }
25
30
  var LIGHT_DEFAULTS = {
26
31
  primary: "#4f46e5",
32
+ // indigo-600 — confident, restrained
27
33
  background: "#ffffff",
28
34
  foreground: "#0f172a",
35
+ // slate-900 — 15.4:1 on white
29
36
  muted: "#64748b",
30
- border: "#e2e8f0"
37
+ // slate-500 — 4.7:1 on white
38
+ subtle: "#f8fafc",
39
+ // slate-50 — chips, icon wells
40
+ border: "#e2e8f0",
41
+ // slate-200
42
+ borderStrong: "#cbd5e1",
43
+ // slate-300 — drag-over emphasis
44
+ elevated: "#ffffff",
45
+ success: "#16a34a",
46
+ danger: "#dc2626"
31
47
  };
32
48
  var DARK_DEFAULTS = {
33
- primary: "#6366f1",
34
- background: "#0f172a",
49
+ primary: "#818cf8",
50
+ // indigo-400 — desaturated for dark mode
51
+ background: "#0b0f1a",
52
+ // near-black, not pure
35
53
  foreground: "#f1f5f9",
54
+ // slate-100 — 15:1 on bg
36
55
  muted: "#94a3b8",
37
- border: "#1e293b"
56
+ // slate-400 — 6.4:1 on bg
57
+ subtle: "#111827",
58
+ // slightly elevated surface
59
+ border: "#1f2937",
60
+ borderStrong: "#334155",
61
+ elevated: "#0f1625",
62
+ success: "#4ade80",
63
+ danger: "#f87171"
38
64
  };
39
65
  var BASE_CSS = `
40
66
  .us-picker-backdrop {
41
67
  position: fixed; inset: 0; z-index: 2147483000;
42
- background: rgba(2, 6, 23, 0.55);
68
+ background: color-mix(in srgb, #02060f 55%, transparent);
69
+ -webkit-backdrop-filter: blur(8px);
70
+ backdrop-filter: blur(8px);
43
71
  display: flex; align-items: center; justify-content: center;
44
- padding: 16px; font-family: ui-sans-serif, system-ui, sans-serif;
45
- animation: us-fade 120ms ease-out;
72
+ padding: 16px;
73
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
74
+ animation: us-fade 140ms ease-out;
46
75
  }
47
76
  @keyframes us-fade { from { opacity: 0; } to { opacity: 1; } }
77
+
48
78
  .us-picker {
49
79
  background: var(--us-bg); color: var(--us-fg);
80
+ border: 1px solid var(--us-border);
50
81
  border-radius: var(--us-radius);
51
- width: 100%; max-width: 480px; max-height: calc(100vh - 32px);
82
+ width: 100%; max-width: 480px;
83
+ max-height: min(calc(100dvh - 32px), 680px);
52
84
  display: flex; flex-direction: column;
53
- box-shadow: 0 25px 50px -12px rgba(0,0,0,0.4);
85
+ box-shadow:
86
+ 0 1px 1px rgba(0,0,0,0.04),
87
+ 0 18px 40px -8px rgba(0,0,0,0.18),
88
+ 0 32px 80px -16px rgba(0,0,0,0.22);
54
89
  overflow: hidden;
90
+ animation: us-rise 240ms cubic-bezier(0.16, 1, 0.3, 1);
91
+ }
92
+ @keyframes us-rise {
93
+ from { opacity: 0; transform: translateY(8px) scale(0.985); }
94
+ to { opacity: 1; transform: translateY(0) scale(1); }
55
95
  }
56
96
  .us-picker * { box-sizing: border-box; }
97
+
98
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
57
99
  .us-picker-header {
58
100
  display: flex; align-items: center; gap: 12px;
59
- padding: 16px 20px; border-bottom: 1px solid var(--us-border);
101
+ padding: 14px 16px;
102
+ border-bottom: 1px solid var(--us-border);
103
+ }
104
+ .us-picker-header-logo {
105
+ display: inline-flex; align-items: center; justify-content: center;
106
+ width: 28px; height: 28px; flex-shrink: 0;
107
+ border-radius: 8px;
108
+ background: var(--us-subtle);
109
+ color: var(--us-primary);
60
110
  }
61
- .us-picker-header img { height: 24px; }
62
- .us-picker-title { font-weight: 600; font-size: 16px; flex: 1; }
111
+ .us-picker-header-logo svg { width: 16px; height: 16px; }
112
+ .us-picker-header-logo img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
113
+ .us-picker-title { font-weight: 600; font-size: 14px; letter-spacing: -0.01em; flex: 1; }
63
114
  .us-picker-close {
64
115
  background: none; border: 0; cursor: pointer;
65
- color: var(--us-muted); font-size: 22px; line-height: 1;
66
- padding: 4px 8px; border-radius: 6px;
116
+ width: 32px; height: 32px;
117
+ display: inline-flex; align-items: center; justify-content: center;
118
+ color: var(--us-muted); border-radius: 8px;
119
+ transition: color 140ms, background 140ms;
67
120
  }
68
- .us-picker-close:hover { background: var(--us-border); color: var(--us-fg); }
69
- .us-picker-body { padding: 20px; overflow-y: auto; }
121
+ .us-picker-close:hover { background: var(--us-subtle); color: var(--us-fg); }
122
+ .us-picker-close:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 1px; }
123
+ .us-picker-close svg { width: 16px; height: 16px; }
124
+
125
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 body / dropzone \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
126
+ .us-picker-body { padding: 16px; overflow-y: auto; }
127
+
70
128
  .us-dropzone {
71
- border: 2px dashed var(--us-border); border-radius: var(--us-radius);
72
- padding: 32px 20px; text-align: center; cursor: pointer;
73
- transition: border-color 120ms, background 120ms;
129
+ position: relative;
130
+ border: 1.5px dashed var(--us-border-strong);
131
+ border-radius: calc(var(--us-radius) - 2px);
132
+ padding: 28px 20px;
133
+ text-align: center;
134
+ cursor: pointer;
135
+ transition: border-color 160ms, background 160ms, transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
136
+ background: color-mix(in srgb, var(--us-subtle) 60%, transparent);
74
137
  }
75
- .us-dropzone:hover, .us-dropzone[data-drag="over"] {
138
+ .us-dropzone:hover {
76
139
  border-color: var(--us-primary);
77
- background: color-mix(in srgb, var(--us-primary) 5%, transparent);
140
+ background: color-mix(in srgb, var(--us-primary) 4%, var(--us-bg));
141
+ }
142
+ .us-dropzone:focus-visible {
143
+ outline: 2px solid var(--us-primary); outline-offset: 2px;
144
+ }
145
+ .us-dropzone[data-drag="over"] {
146
+ border-style: solid;
147
+ border-color: var(--us-primary);
148
+ background: color-mix(in srgb, var(--us-primary) 8%, var(--us-bg));
149
+ transform: scale(1.005);
150
+ }
151
+ .us-dropzone-icon {
152
+ width: 44px; height: 44px;
153
+ border-radius: 12px;
154
+ background: color-mix(in srgb, var(--us-primary) 12%, var(--us-bg));
155
+ color: var(--us-primary);
156
+ display: inline-flex; align-items: center; justify-content: center;
157
+ margin-bottom: 12px;
158
+ transition: transform 240ms cubic-bezier(0.16, 1.4, 0.3, 1);
159
+ }
160
+ .us-dropzone:hover .us-dropzone-icon { transform: translateY(-2px); }
161
+ .us-dropzone[data-drag="over"] .us-dropzone-icon {
162
+ transform: translateY(-3px) scale(1.06);
163
+ background: color-mix(in srgb, var(--us-primary) 18%, var(--us-bg));
164
+ }
165
+ .us-dropzone-icon svg { width: 22px; height: 22px; }
166
+ .us-dropzone-title {
167
+ font-size: 14px; font-weight: 600; letter-spacing: -0.01em;
168
+ margin-bottom: 2px;
169
+ }
170
+ .us-dropzone-hint { color: var(--us-muted); font-size: 12.5px; }
171
+ .us-dropzone-constraints {
172
+ margin-top: 10px;
173
+ font-size: 11px; color: var(--us-muted);
174
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
175
+ letter-spacing: 0.02em;
176
+ }
177
+
178
+ .us-dropzone--compact {
179
+ padding: 12px 16px;
180
+ display: flex; align-items: center; gap: 12px;
181
+ text-align: left;
182
+ }
183
+ .us-dropzone--compact .us-dropzone-icon {
184
+ width: 32px; height: 32px; border-radius: 8px; margin-bottom: 0;
78
185
  }
79
- .us-dropzone-title { font-weight: 500; margin-bottom: 4px; }
80
- .us-dropzone-hint { color: var(--us-muted); font-size: 13px; }
81
- .us-file-list { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; }
186
+ .us-dropzone--compact .us-dropzone-icon svg { width: 16px; height: 16px; }
187
+ .us-dropzone--compact .us-dropzone-title { font-size: 13px; margin: 0; }
188
+ .us-dropzone--compact .us-dropzone-hint { display: none; }
189
+ .us-dropzone--compact .us-dropzone-constraints { display: none; }
190
+
191
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 file list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
192
+ .us-file-list { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
193
+
82
194
  .us-file {
83
195
  display: flex; align-items: center; gap: 12px;
84
- padding: 10px 12px; border: 1px solid var(--us-border);
85
- border-radius: 10px; font-size: 14px;
196
+ padding: 10px 12px;
197
+ background: var(--us-elevated);
198
+ border: 1px solid var(--us-border);
199
+ border-radius: 10px;
200
+ transition: border-color 140ms, background 140ms;
201
+ animation: us-row-in 280ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
202
+ position: relative;
203
+ }
204
+ @keyframes us-row-in {
205
+ from { opacity: 0; transform: translateY(6px); }
206
+ to { opacity: 1; transform: translateY(0); }
207
+ }
208
+ .us-file[data-state="done"] { border-color: color-mix(in srgb, var(--us-success) 30%, var(--us-border)); }
209
+ .us-file[data-state="failed"] { border-color: color-mix(in srgb, var(--us-danger) 30%, var(--us-border)); }
210
+
211
+ .us-file-thumb {
212
+ width: 40px; height: 40px; flex-shrink: 0;
213
+ border-radius: 8px;
214
+ background: var(--us-subtle);
215
+ background-size: cover; background-position: center;
216
+ display: inline-flex; align-items: center; justify-content: center;
217
+ color: var(--us-muted);
218
+ overflow: hidden;
86
219
  }
87
- .us-file-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
88
- .us-file-meta { color: var(--us-muted); font-size: 12px; }
220
+ .us-file-thumb[data-image="true"] { color: transparent; }
221
+ .us-file-thumb svg { width: 18px; height: 18px; }
222
+
223
+ .us-file-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
224
+ .us-file-row1 { display: flex; align-items: center; gap: 8px; }
225
+ .us-file-name {
226
+ font-size: 13px; font-weight: 500;
227
+ flex: 1; min-width: 0;
228
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
229
+ letter-spacing: -0.005em;
230
+ }
231
+ .us-file-meta { color: var(--us-muted); font-size: 11.5px; flex-shrink: 0; font-variant-numeric: tabular-nums; }
232
+
89
233
  .us-file-progress {
90
- height: 4px; background: var(--us-border); border-radius: 999px; overflow: hidden;
91
- margin-top: 6px;
234
+ height: 3px; background: var(--us-border); border-radius: 999px; overflow: hidden;
235
+ position: relative;
92
236
  }
93
237
  .us-file-progress-bar {
94
- height: 100%; background: var(--us-primary);
95
- width: 0%; transition: width 200ms;
238
+ height: 100%; width: 0%;
239
+ background: var(--us-primary);
240
+ border-radius: inherit;
241
+ transition: width 260ms cubic-bezier(0.4, 0.0, 0.2, 1);
242
+ position: relative;
243
+ }
244
+ /* Shimmer overlay while uploading. Stops on terminal states. */
245
+ .us-file[data-state="uploading"] .us-file-progress-bar::after {
246
+ content: ""; position: absolute; inset: 0;
247
+ background: linear-gradient(
248
+ 90deg,
249
+ transparent 0%,
250
+ color-mix(in srgb, #fff 35%, transparent) 50%,
251
+ transparent 100%
252
+ );
253
+ animation: us-shimmer 1.4s linear infinite;
254
+ }
255
+ @keyframes us-shimmer {
256
+ from { transform: translateX(-100%); }
257
+ to { transform: translateX(100%); }
258
+ }
259
+ .us-file[data-state="done"] .us-file-progress-bar { width: 100% !important; background: var(--us-success); }
260
+ .us-file[data-state="failed"] .us-file-progress-bar { background: var(--us-danger); }
261
+
262
+ .us-file-status {
263
+ flex-shrink: 0;
264
+ display: inline-flex; align-items: center; justify-content: center;
265
+ width: 24px; height: 24px;
266
+ color: var(--us-muted);
96
267
  }
97
- .us-file[data-state="done"] .us-file-progress-bar { background: #16a34a; width: 100%; }
98
- .us-file[data-state="failed"] .us-file-progress-bar { background: #dc2626; }
268
+ .us-file-status svg { width: 18px; height: 18px; }
269
+ .us-file[data-state="done"] .us-file-status { color: var(--us-success); animation: us-pop 320ms cubic-bezier(0.16, 1.4, 0.3, 1); }
270
+ .us-file[data-state="failed"] .us-file-status { color: var(--us-danger); animation: us-pop 320ms cubic-bezier(0.16, 1.4, 0.3, 1); }
271
+ .us-file[data-state="uploading"] .us-file-status { animation: us-spin 0.9s linear infinite; }
272
+ @keyframes us-pop {
273
+ from { transform: scale(0.5); opacity: 0; }
274
+ to { transform: scale(1); opacity: 1; }
275
+ }
276
+ @keyframes us-spin {
277
+ from { transform: rotate(0deg); }
278
+ to { transform: rotate(360deg); }
279
+ }
280
+
281
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 actions / footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
99
282
  .us-actions {
100
- display: flex; gap: 8px; justify-content: flex-end;
101
- padding: 14px 20px; border-top: 1px solid var(--us-border);
283
+ display: flex; gap: 8px; justify-content: space-between; align-items: center;
284
+ padding: 12px 16px;
285
+ border-top: 1px solid var(--us-border);
102
286
  }
287
+ .us-actions-summary { font-size: 12px; color: var(--us-muted); font-variant-numeric: tabular-nums; }
288
+ .us-actions-buttons { display: flex; gap: 8px; }
289
+
103
290
  .us-btn {
104
- padding: 8px 14px; border-radius: 8px; border: 1px solid var(--us-border);
105
- background: transparent; color: var(--us-fg); cursor: pointer; font-size: 14px;
106
- font-weight: 500;
291
+ appearance: none;
292
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
293
+ padding: 8px 14px; min-height: 36px;
294
+ border-radius: 8px; border: 1px solid var(--us-border);
295
+ background: transparent; color: var(--us-fg);
296
+ cursor: pointer; font-size: 13px; font-weight: 500;
297
+ font-family: inherit; letter-spacing: -0.005em;
298
+ transition: background 140ms, border-color 140ms, transform 80ms ease-out;
107
299
  }
108
- .us-btn:hover { background: var(--us-border); }
300
+ .us-btn:hover { background: var(--us-subtle); border-color: var(--us-border-strong); }
301
+ .us-btn:active { transform: scale(0.98); }
302
+ .us-btn:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 2px; }
109
303
  .us-btn-primary {
110
- background: var(--us-primary); color: white; border-color: var(--us-primary);
304
+ background: var(--us-primary); color: white;
305
+ border-color: var(--us-primary);
111
306
  }
112
- .us-btn-primary:hover { filter: brightness(0.95); }
307
+ .us-btn-primary:hover { filter: brightness(0.95); background: var(--us-primary); }
113
308
  .us-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
309
+ .us-btn[disabled]:hover { transform: none; }
310
+ .us-btn svg { width: 14px; height: 14px; }
311
+
114
312
  .us-footer {
115
- padding: 8px 20px; font-size: 11px; color: var(--us-muted); text-align: center;
313
+ padding: 8px 16px 12px;
314
+ font-size: 11px; color: var(--us-muted); text-align: center;
315
+ display: flex; align-items: center; justify-content: center; gap: 6px;
316
+ }
317
+ .us-footer svg { width: 11px; height: 11px; opacity: 0.7; }
318
+ .us-footer a { color: var(--us-muted); text-decoration: none; font-weight: 500; }
319
+ .us-footer a:hover { color: var(--us-fg); }
320
+
321
+ /* \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
+ @media (prefers-reduced-motion: reduce) {
323
+ .us-picker-backdrop,
324
+ .us-picker,
325
+ .us-file,
326
+ .us-dropzone,
327
+ .us-dropzone-icon,
328
+ .us-btn,
329
+ .us-file-status,
330
+ .us-file-progress-bar { animation: none !important; transition: none !important; }
331
+ .us-file[data-state="uploading"] .us-file-progress-bar::after { animation: none; opacity: 0; }
116
332
  }
117
- .us-footer a { color: var(--us-muted); }
333
+
334
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 empty state niceties \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
335
+ .us-file-list:empty { display: none; }
118
336
  `;
119
337
 
120
338
  // src/picker/picker.ts
@@ -145,7 +363,30 @@ function mergeConfig(server, runtime) {
145
363
  return merged;
146
364
  }
147
365
  var DEFAULT_TITLE = "Upload files";
148
- var FOOTER_LINK = "https://unionstack.mastersunion.link";
366
+ var FOOTER_LINK = "https://unionstack.link";
367
+ var ICON = {
368
+ upload: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
369
+ 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
+ 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
+ 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>`,
372
+ spinner: `<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9" stroke="currentColor" opacity="0.2"/><path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor"/></svg>`,
373
+ image: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`,
374
+ video: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`,
375
+ audio: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`,
376
+ 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
+ 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
+ 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>`
380
+ };
381
+ function iconForMime(mime) {
382
+ if (!mime) return ICON.file;
383
+ if (mime.startsWith("image/")) return ICON.image;
384
+ if (mime.startsWith("video/")) return ICON.video;
385
+ if (mime.startsWith("audio/")) return ICON.audio;
386
+ if (mime === "application/pdf") return ICON.pdf;
387
+ if (mime.startsWith("application/zip") || mime.includes("compressed") || mime === "application/x-tar" || mime === "application/gzip") return ICON.archive;
388
+ return ICON.file;
389
+ }
149
390
  var Picker = class {
150
391
  constructor(client, opts) {
151
392
  this.client = client;
@@ -160,6 +401,8 @@ var Picker = class {
160
401
  this.abortCtrl = new AbortController();
161
402
  this.uploadStarted = false;
162
403
  this.resolved = false;
404
+ // ---- mount / dom --------------------------------------------------------
405
+ this.$summary = null;
163
406
  this.donePromise = new Promise((res) => {
164
407
  this.resolvePromise = res;
165
408
  });
@@ -188,7 +431,6 @@ var Picker = class {
188
431
  this.opts.onCancel?.();
189
432
  this.close();
190
433
  }
191
- // ---- mount / dom --------------------------------------------------------
192
434
  mount() {
193
435
  const root = document.createElement("div");
194
436
  root.className = "us-picker-backdrop";
@@ -200,19 +442,23 @@ var Picker = class {
200
442
  });
201
443
  const panel = el("div", "us-picker");
202
444
  const header = el("div", "us-picker-header");
445
+ const logoWrap = el("div", "us-picker-header-logo");
203
446
  if (this.opts.branding?.logoUrl) {
204
447
  const logo = document.createElement("img");
205
448
  logo.src = this.opts.branding.logoUrl;
206
- logo.alt = "logo";
207
- header.appendChild(logo);
449
+ logo.alt = "";
450
+ logoWrap.appendChild(logo);
451
+ } else {
452
+ logoWrap.innerHTML = ICON.zap;
208
453
  }
454
+ header.appendChild(logoWrap);
209
455
  const title = el("div", "us-picker-title", this.opts.branding?.title ?? DEFAULT_TITLE);
210
456
  header.appendChild(title);
211
457
  this.$closeBtn = document.createElement("button");
212
458
  this.$closeBtn.type = "button";
213
459
  this.$closeBtn.className = "us-picker-close";
214
460
  this.$closeBtn.setAttribute("aria-label", "Close");
215
- this.$closeBtn.textContent = "\xD7";
461
+ this.$closeBtn.innerHTML = ICON.close;
216
462
  this.$closeBtn.onclick = () => this.cancel();
217
463
  header.appendChild(this.$closeBtn);
218
464
  panel.appendChild(header);
@@ -221,24 +467,31 @@ var Picker = class {
221
467
  this.$list = el("div", "us-file-list");
222
468
  body.appendChild(this.$list);
223
469
  panel.appendChild(body);
470
+ const autoUpload = this.opts.autoUpload !== false;
224
471
  const actions = el("div", "us-actions");
472
+ this.$summary = el("div", "us-actions-summary", "");
473
+ actions.appendChild(this.$summary);
474
+ const buttons = el("div", "us-actions-buttons");
225
475
  this.$cancel = document.createElement("button");
226
476
  this.$cancel.type = "button";
227
477
  this.$cancel.className = "us-btn";
228
478
  this.$cancel.textContent = "Cancel";
229
479
  this.$cancel.onclick = () => this.cancel();
230
- this.$confirm = document.createElement("button");
231
- this.$confirm.type = "button";
232
- this.$confirm.className = "us-btn us-btn-primary";
233
- this.$confirm.textContent = "Upload";
234
- this.$confirm.disabled = true;
235
- this.$confirm.onclick = () => this.startUpload();
236
- actions.appendChild(this.$cancel);
237
- actions.appendChild(this.$confirm);
480
+ buttons.appendChild(this.$cancel);
481
+ if (!autoUpload) {
482
+ this.$confirm = document.createElement("button");
483
+ this.$confirm.type = "button";
484
+ this.$confirm.className = "us-btn us-btn-primary";
485
+ this.$confirm.innerHTML = `${ICON.upload} <span>Upload</span>`;
486
+ this.$confirm.disabled = true;
487
+ this.$confirm.onclick = () => this.startUpload();
488
+ buttons.appendChild(this.$confirm);
489
+ }
490
+ actions.appendChild(buttons);
238
491
  panel.appendChild(actions);
239
492
  if (!this.opts.branding?.hideFooter) {
240
493
  const footer = el("div", "us-footer");
241
- footer.innerHTML = `Powered by <a href="${FOOTER_LINK}" target="_blank" rel="noopener">UnionStack</a>`;
494
+ footer.innerHTML = `${ICON.zap} <span>Powered by <a href="${FOOTER_LINK}" target="_blank" rel="noopener">UnionStack</a></span>`;
242
495
  panel.appendChild(footer);
243
496
  }
244
497
  root.appendChild(panel);
@@ -249,8 +502,23 @@ var Picker = class {
249
502
  const dz = el("div", "us-dropzone");
250
503
  dz.setAttribute("role", "button");
251
504
  dz.setAttribute("tabindex", "0");
252
- dz.appendChild(el("div", "us-dropzone-title", "Drag files here"));
253
- dz.appendChild(el("div", "us-dropzone-hint", "or click to browse"));
505
+ dz.setAttribute("aria-label", "Drop files here or click to browse");
506
+ const icon = el("div", "us-dropzone-icon");
507
+ icon.innerHTML = ICON.upload;
508
+ dz.appendChild(icon);
509
+ dz.appendChild(el("div", "us-dropzone-title", "Drop files to upload"));
510
+ dz.appendChild(el("div", "us-dropzone-hint", "or click to browse from your device"));
511
+ const constraintBits = [];
512
+ if (this.opts.maxFileSize) constraintBits.push(`max ${formatBytes(this.opts.maxFileSize)}`);
513
+ if (this.opts.accept) {
514
+ const types = this.opts.accept.split(",").map((s) => s.trim()).filter((s) => s && s !== "*/*").map((s) => s.replace("/*", ""));
515
+ if (types.length > 0 && types.length <= 4) {
516
+ constraintBits.push(types.join(" \xB7 "));
517
+ }
518
+ }
519
+ if (constraintBits.length > 0) {
520
+ dz.appendChild(el("div", "us-dropzone-constraints", constraintBits.join(" \xB7 ")));
521
+ }
254
522
  const input = document.createElement("input");
255
523
  input.type = "file";
256
524
  input.multiple = (this.opts.maxFiles ?? 10) > 1;
@@ -285,6 +553,9 @@ var Picker = class {
285
553
  unmount() {
286
554
  if (this.$backdrop?.parentNode) this.$backdrop.parentNode.removeChild(this.$backdrop);
287
555
  this.$backdrop = null;
556
+ for (const item of this.items) {
557
+ if (item.objectUrl) URL.revokeObjectURL(item.objectUrl);
558
+ }
288
559
  this.opts.onClose?.();
289
560
  }
290
561
  // ---- file selection -----------------------------------------------------
@@ -316,32 +587,84 @@ var Picker = class {
316
587
  this.renderItem(item);
317
588
  }
318
589
  this.refreshConfirm();
590
+ const autoUpload = this.opts.autoUpload !== false;
591
+ const hasQueued = this.items.some((i) => i.state === "queued");
592
+ if (autoUpload && hasQueued && !this.uploadStarted) {
593
+ requestAnimationFrame(() => {
594
+ if (!this.uploadStarted) this.startUpload();
595
+ });
596
+ }
319
597
  }
320
598
  renderItem(item) {
321
599
  if (!this.$list) return;
322
600
  const row = el("div", "us-file");
323
601
  row.dataset.state = item.state;
324
602
  row.dataset.uploadId = item.uploadId;
325
- const main = el("div", "", "");
326
- main.style.flex = "1";
327
- main.style.minWidth = "0";
328
- main.appendChild(el("div", "us-file-name", item.file.name));
603
+ const idx = Math.min(this.items.length - 1, 8);
604
+ row.style.animationDelay = `${idx * 35}ms`;
605
+ const thumb = el("div", "us-file-thumb");
606
+ if (item.file.type.startsWith("image/") && typeof URL !== "undefined" && URL.createObjectURL) {
607
+ try {
608
+ const objectUrl = URL.createObjectURL(item.file);
609
+ item.objectUrl = objectUrl;
610
+ thumb.style.backgroundImage = `url("${objectUrl}")`;
611
+ thumb.dataset.image = "true";
612
+ } catch {
613
+ thumb.innerHTML = iconForMime(item.file.type);
614
+ }
615
+ } else {
616
+ thumb.innerHTML = iconForMime(item.file.type);
617
+ }
618
+ row.appendChild(thumb);
619
+ const main = el("div", "us-file-main");
620
+ const row1 = el("div", "us-file-row1");
621
+ row1.appendChild(el("div", "us-file-name", item.file.name));
329
622
  const meta = el("div", "us-file-meta", formatBytes(item.file.size));
330
- main.appendChild(meta);
623
+ row1.appendChild(meta);
624
+ main.appendChild(row1);
331
625
  const progress = el("div", "us-file-progress");
332
626
  const bar = el("div", "us-file-progress-bar");
333
627
  progress.appendChild(bar);
334
628
  main.appendChild(progress);
335
629
  row.appendChild(main);
336
- const status = el("div", "us-file-meta");
337
- status.style.minWidth = "60px";
338
- status.style.textAlign = "right";
339
- status.textContent = item.state === "failed" ? item.error || "failed" : item.state === "done" ? "done" : "0%";
630
+ const status = el("div", "us-file-status");
631
+ status.setAttribute("aria-label", this.statusLabel(item));
632
+ status.innerHTML = this.statusIcon(item.state);
340
633
  row.appendChild(status);
341
634
  item.$row = row;
342
635
  item.$bar = bar;
343
636
  item.$status = status;
637
+ item.$meta = meta;
344
638
  this.$list.appendChild(row);
639
+ this.updateSummary();
640
+ }
641
+ statusIcon(state) {
642
+ switch (state) {
643
+ case "uploading":
644
+ return ICON.spinner;
645
+ case "done":
646
+ return ICON.check;
647
+ case "failed":
648
+ return ICON.alert;
649
+ case "cancelled":
650
+ return ICON.alert;
651
+ default:
652
+ return ICON.spinner;
653
+ }
654
+ }
655
+ statusLabel(item) {
656
+ switch (item.state) {
657
+ case "queued":
658
+ return "Waiting to upload";
659
+ case "uploading":
660
+ return `Uploading ${Math.round(item.progress)} percent`;
661
+ case "done":
662
+ return "Upload complete";
663
+ case "failed":
664
+ return item.error ? `Failed: ${item.error}` : "Upload failed";
665
+ case "cancelled":
666
+ return "Cancelled";
667
+ }
345
668
  }
346
669
  setItemState(item, state, progress) {
347
670
  item.state = state;
@@ -349,9 +672,45 @@ var Picker = class {
349
672
  if (item.$row) item.$row.dataset.state = state;
350
673
  if (item.$bar) item.$bar.style.width = `${item.progress}%`;
351
674
  if (item.$status) {
352
- if (state === "failed") item.$status.textContent = item.error || "failed";
353
- else if (state === "done") item.$status.textContent = "done";
354
- else item.$status.textContent = `${Math.round(item.progress)}%`;
675
+ item.$status.innerHTML = this.statusIcon(state);
676
+ item.$status.setAttribute("aria-label", this.statusLabel(item));
677
+ }
678
+ if (item.$meta) {
679
+ const size = formatBytes(item.file.size);
680
+ switch (state) {
681
+ case "uploading":
682
+ item.$meta.textContent = `${size} \xB7 ${Math.round(item.progress)}%`;
683
+ break;
684
+ case "done":
685
+ item.$meta.textContent = size;
686
+ break;
687
+ case "failed":
688
+ item.$meta.textContent = item.error || "Failed";
689
+ break;
690
+ default:
691
+ item.$meta.textContent = size;
692
+ }
693
+ }
694
+ this.updateSummary();
695
+ }
696
+ updateSummary() {
697
+ if (!this.$summary) return;
698
+ const total = this.items.length;
699
+ if (total === 0) {
700
+ this.$summary.textContent = "";
701
+ return;
702
+ }
703
+ const done = this.items.filter((i) => i.state === "done").length;
704
+ const failed = this.items.filter((i) => i.state === "failed").length;
705
+ const active = this.items.filter((i) => i.state === "uploading" || i.state === "queued").length;
706
+ if (active > 0) {
707
+ this.$summary.textContent = `Uploading ${done + 1} of ${total}`;
708
+ } else if (failed > 0) {
709
+ this.$summary.textContent = `${done} of ${total} uploaded \xB7 ${failed} failed`;
710
+ } else if (done === total) {
711
+ this.$summary.textContent = `${total} file${total === 1 ? "" : "s"} uploaded`;
712
+ } else {
713
+ this.$summary.textContent = `${total} file${total === 1 ? "" : "s"} ready`;
355
714
  }
356
715
  }
357
716
  refreshConfirm() {