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