@masters-union/union-stack 0.3.17 → 0.4.1

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/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/node/source.ts","../src/node/index.ts"],"names":["openAsBlob","basename"],"mappings":"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","file":"node.js","sourcesContent":["import { basename, extname } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport * as fs from 'node:fs';\nimport { Readable } from 'node:stream';\nimport { makeError } from '../errors.js';\n\n/**\n * Anything the Node client knows how to turn into an upload.\n *\n * • `string` — path to a file on disk (absolute or relative to cwd)\n * • `Buffer` / typed arr — raw bytes already in memory\n * • `ArrayBuffer` — raw bytes\n * • `Blob` / `File` — Web file objects (Node 18+ has `Blob`, 20+ has `File`)\n * • `Readable` — a Node stream (buffered in full before upload, since\n * the multipart protocol needs the total size up front)\n */\nexport type NodeUploadSource =\n | string\n | Buffer\n | Uint8Array\n | ArrayBuffer\n | Blob\n | Readable;\n\nexport interface ResolvedSource {\n /** A Web `Blob` the core Uploader can `.slice()` into multipart chunks. */\n blob: Blob;\n filename: string;\n mimetype: string;\n}\n\n// Smallest map that covers the bulk of real-world server uploads. Anything not\n// listed falls back to application/octet-stream — callers can always override\n// via `{ mimeType }`. Kept deliberately short; this is not a full mime database.\nconst EXT_TO_MIME: Record<string, string> = {\n // images\n png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',\n webp: 'image/webp', avif: 'image/avif', svg: 'image/svg+xml', bmp: 'image/bmp',\n ico: 'image/x-icon', heic: 'image/heic', tiff: 'image/tiff', tif: 'image/tiff',\n // documents\n pdf: 'application/pdf', txt: 'text/plain', csv: 'text/csv', md: 'text/markdown',\n json: 'application/json', xml: 'application/xml', html: 'text/html', htm: 'text/html',\n // office\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n // media\n mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', mkv: 'video/x-matroska',\n mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4',\n // archives\n zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar',\n};\n\nfunction mimeFromName(name: string): string {\n const ext = extname(name).slice(1).toLowerCase();\n return EXT_TO_MIME[ext] || 'application/octet-stream';\n}\n\n// `fs.openAsBlob` (Node 19.8+/20+) hands back a Blob backed by the file on disk,\n// so slicing it into parts streams lazily instead of loading the whole file into\n// memory. Typed locally — the global DOM `Blob` is what the Uploader wants, and\n// not every @types/node version exposes openAsBlob.\ntype OpenAsBlob = (path: string, options?: { type?: string }) => Promise<Blob>;\nconst openAsBlob = (fs as unknown as { openAsBlob?: OpenAsBlob }).openAsBlob;\n\n// Node's `Buffer<ArrayBufferLike>` and the DOM lib's `BlobPart`\n// (`ArrayBufferView<ArrayBuffer>`) don't line up under recent @types/node, even\n// though every value passed here is a valid Blob part at runtime. Cast once.\nfunction toBlobPart(data: ArrayBuffer | ArrayBufferView): BlobPart {\n return data as unknown as BlobPart;\n}\n\nfunction isReadable(x: unknown): x is Readable {\n return (\n !!x &&\n typeof (x as Readable).pipe === 'function' &&\n typeof (x as { read?: unknown }).read === 'function'\n );\n}\n\nasync function streamToBuffer(stream: Readable): Promise<Buffer> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(\n Buffer.isBuffer(chunk)\n ? chunk\n : typeof chunk === 'string'\n ? Buffer.from(chunk)\n : Buffer.from(chunk as Uint8Array),\n );\n }\n return Buffer.concat(chunks);\n}\n\n/**\n * Normalize any {@link NodeUploadSource} into a `{ blob, filename, mimetype }`\n * the core Uploader can consume. Caller-supplied `filename` / `mimeType` always\n * win over what we infer from the path or the blob.\n */\nexport async function resolveSource(\n source: NodeUploadSource,\n opts: { filename?: string; mimeType?: string } = {},\n): Promise<ResolvedSource> {\n // 1. File path on disk.\n if (typeof source === 'string') {\n const filename = opts.filename || basename(source) || 'untitled';\n const mimetype = opts.mimeType || mimeFromName(filename);\n try {\n const blob = openAsBlob\n ? await openAsBlob(source, { type: mimetype })\n : new Blob([toBlobPart(await readFile(source))], { type: mimetype });\n return { blob, filename, mimetype };\n } catch (cause) {\n throw makeError('VALIDATION', `Could not read file at \"${source}\".`, {\n retryable: false,\n cause,\n });\n }\n }\n\n // 2. Already a Blob / File.\n if (typeof Blob !== 'undefined' && source instanceof Blob) {\n const filename = opts.filename || (source as Blob & { name?: string }).name || 'untitled';\n const mimetype = opts.mimeType || source.type || mimeFromName(filename);\n // Only re-wrap when we need to stamp a type the blob doesn't already carry.\n const blob =\n opts.mimeType && opts.mimeType !== source.type\n ? new Blob([source], { type: opts.mimeType })\n : source;\n return { blob, filename, mimetype };\n }\n\n // 3. Node Readable stream — buffer it fully; multipart needs the size up front.\n if (isReadable(source)) {\n const filename = opts.filename || 'untitled';\n const mimetype = opts.mimeType || mimeFromName(filename);\n const buf = await streamToBuffer(source);\n return { blob: new Blob([toBlobPart(buf)], { type: mimetype }), filename, mimetype };\n }\n\n // 4. Buffer / Uint8Array / ArrayBuffer (Buffer is an ArrayBuffer view).\n if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {\n const filename = opts.filename || 'untitled';\n const mimetype = opts.mimeType || mimeFromName(filename);\n return { blob: new Blob([toBlobPart(source)], { type: mimetype }), filename, mimetype };\n }\n\n throw makeError(\n 'VALIDATION',\n 'Unsupported upload source. Pass a file path, Buffer, Uint8Array, ArrayBuffer, Blob, or Readable stream.',\n { retryable: false },\n );\n}\n","import { basename } from 'node:path';\nimport { Uploader } from '../uploader.js';\nimport { makeError } from '../errors.js';\nimport { NODE_SDK_VERSION_HEADER } from '../version.js';\nimport { resolveSource, type NodeUploadSource } from './source.js';\nimport type {\n ClientConfig,\n ResolvedClientConfig,\n UploadOptions,\n BatchUploadOptions,\n UploadedFile,\n PickedFile,\n PickResponse,\n UploadError,\n} from '../types.js';\n\n/**\n * UnionStack API URL baked into the SDK. Consumers never see or set this — we\n * ship a new SDK version if the URL ever changes. (Same constant as the browser\n * client; kept independent so the Node entry doesn't pull in client.ts.)\n */\nconst API_BASE = 'https://api.unionstack.link/v1';\n\n// File-level fan-out for uploadMany. Each file still runs its own part-level\n// concurrency (opts.concurrency), so this caps how many files upload at once —\n// keeps a 10k-file batch from opening 10k sockets / file handles simultaneously.\nconst DEFAULT_FILE_CONCURRENCY = 5;\n\n/**\n * Server-side UnionStack client for Node.js. Uploads files straight to your\n * UnionStack bucket from a backend — no browser, no picker, no DOM.\n *\n * Use an API key whose allowed origins include `*` (server-to-server requests\n * carry no `Origin` header). Create the key and add `*` from your dashboard.\n *\n * ```ts\n * import { UnionStack } from '@masters-union/union-stack/node';\n *\n * const client = UnionStack.init({ apiKey: process.env.UNIONSTACK_API_KEY! });\n * const file = await client.upload('./invoice.pdf');\n * console.log(file.url);\n * ```\n */\nexport class UnionStackNodeClient {\n private uploader: Uploader;\n private cfg: ResolvedClientConfig;\n\n constructor(cfg: ClientConfig) {\n // A server SDK can throw on misconfiguration — unlike the browser client,\n // there's no render path to crash.\n if (!cfg?.apiKey) {\n throw makeError('CONFIG', 'apiKey is required. Pass { apiKey: \"unionstack_live_…\" }.', {\n retryable: false,\n });\n }\n this.cfg = {\n ...cfg,\n apiBase: API_BASE,\n // No picker on the server — never prefetch the picker config.\n skipConfigPrefetch: true,\n // Tag the traffic as server-to-server (\"node/…\") so cdn-be can tell it\n // apart from browser SDK calls (\"js/…\").\n sdkHeader: NODE_SDK_VERSION_HEADER,\n };\n this.uploader = new Uploader(this.cfg);\n }\n\n /**\n * Upload one file. Accepts a path on disk, a Buffer/Uint8Array/ArrayBuffer, a\n * Blob/File, or a Readable stream. Returns the stored file (with its public\n * `url`). Rejects with an {@link UploadError}.\n */\n async upload(source: NodeUploadSource, opts: UploadOptions = {}): Promise<UploadedFile> {\n const resolved = await resolveSource(source, { filename: opts.filename, mimeType: opts.mimeType });\n return this.uploader.upload(resolved.blob, {\n ...opts,\n filename: resolved.filename,\n mimeType: resolved.mimetype,\n });\n }\n\n /**\n * Upload many files. Runs up to {@link DEFAULT_FILE_CONCURRENCY} files at once\n * and never rejects on a single file's failure — successes and failures come\n * back separately so a partial failure doesn't drop the whole batch.\n */\n async uploadMany(sources: NodeUploadSource[], opts: BatchUploadOptions = {}): Promise<PickResponse> {\n if (!Array.isArray(sources) || sources.length === 0) {\n const err = makeError('VALIDATION', 'uploadMany requires a non-empty array of sources.', {\n retryable: false,\n });\n opts.onError?.(err);\n throw err;\n }\n\n const filesUploaded: UploadedFile[] = [];\n const filesFailed: Array<{ file: PickedFile; error: UploadError }> = [];\n\n let cursor = 0;\n const worker = async (): Promise<void> => {\n while (true) {\n const i = cursor++;\n if (i >= sources.length) return;\n\n let resolved;\n try {\n resolved = await resolveSource(sources[i], {\n filename: opts.filename,\n mimeType: opts.mimeType,\n });\n } catch (err) {\n filesFailed.push({ file: describeUnresolved(sources[i], i, opts), error: err as UploadError });\n continue;\n }\n\n const picked: PickedFile = {\n uploadId: String(i),\n filename: resolved.filename,\n mimetype: resolved.mimetype,\n size: resolved.blob.size,\n source: 'local',\n };\n try {\n const uploaded = await this.uploader.upload(resolved.blob, {\n ...opts,\n filename: resolved.filename,\n mimeType: resolved.mimetype,\n });\n uploaded.uploadId = picked.uploadId;\n filesUploaded.push(uploaded);\n } catch (err) {\n filesFailed.push({ file: picked, error: err as UploadError });\n }\n }\n };\n\n const pool = Math.max(1, Math.min(DEFAULT_FILE_CONCURRENCY, sources.length));\n await Promise.all(Array.from({ length: pool }, worker));\n\n const result: PickResponse = { filesUploaded, filesFailed };\n opts.onUploadDone?.(result);\n return result;\n }\n\n /** Read-only view of the resolved config. */\n get config(): Readonly<ResolvedClientConfig> {\n return this.cfg;\n }\n}\n\n/** Best-effort descriptor for a file that failed before we could read it. */\nfunction describeUnresolved(\n source: NodeUploadSource,\n index: number,\n opts: BatchUploadOptions,\n): PickedFile {\n return {\n uploadId: String(index),\n filename: opts.filename || (typeof source === 'string' ? basename(source) : 'untitled'),\n mimetype: opts.mimeType || 'application/octet-stream',\n size: 0,\n source: 'local',\n };\n}\n\nexport const UnionStack = {\n /** Create a server-side client. `cfg.apiKey` is required. */\n init(cfg: ClientConfig): UnionStackNodeClient {\n return new UnionStackNodeClient(cfg);\n },\n};\n\nexport type { NodeUploadSource } from './source.js';\nexport type {\n ClientConfig,\n UploadOptions,\n BatchUploadOptions,\n PickedFile,\n UploadedFile,\n ProgressEvent,\n PickResponse,\n UploadError,\n UploadErrorCode,\n Source,\n} from '../types.js';\n"]}
1
+ {"version":3,"sources":["../src/node/source.ts","../src/node/index.ts"],"names":["openAsBlob","basename"],"mappings":"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","file":"node.js","sourcesContent":["import { basename, extname } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport * as fs from 'node:fs';\nimport { Readable } from 'node:stream';\nimport { makeError } from '../errors.js';\n\n/**\n * Anything the Node client knows how to turn into an upload.\n *\n * • `string` — path to a file on disk (absolute or relative to cwd)\n * • `Buffer` / typed arr — raw bytes already in memory\n * • `ArrayBuffer` — raw bytes\n * • `Blob` / `File` — Web file objects (Node 18+ has `Blob`, 20+ has `File`)\n * • `Readable` — a Node stream (buffered in full before upload, since\n * the multipart protocol needs the total size up front)\n */\nexport type NodeUploadSource =\n | string\n | Buffer\n | Uint8Array\n | ArrayBuffer\n | Blob\n | Readable;\n\nexport interface ResolvedSource {\n /** A Web `Blob` the core Uploader can `.slice()` into multipart chunks. */\n blob: Blob;\n filename: string;\n mimetype: string;\n}\n\n// Smallest map that covers the bulk of real-world server uploads. Anything not\n// listed falls back to application/octet-stream — callers can always override\n// via `{ mimeType }`. Kept deliberately short; this is not a full mime database.\nconst EXT_TO_MIME: Record<string, string> = {\n // images\n png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',\n webp: 'image/webp', avif: 'image/avif', svg: 'image/svg+xml', bmp: 'image/bmp',\n ico: 'image/x-icon', heic: 'image/heic', tiff: 'image/tiff', tif: 'image/tiff',\n // documents\n pdf: 'application/pdf', txt: 'text/plain', csv: 'text/csv', md: 'text/markdown',\n json: 'application/json', xml: 'application/xml', html: 'text/html', htm: 'text/html',\n // office\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n // media\n mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', mkv: 'video/x-matroska',\n mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4',\n // archives\n zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar',\n};\n\nfunction mimeFromName(name: string): string {\n const ext = extname(name).slice(1).toLowerCase();\n return EXT_TO_MIME[ext] || 'application/octet-stream';\n}\n\n// `fs.openAsBlob` (Node 19.8+/20+) hands back a Blob backed by the file on disk,\n// so slicing it into parts streams lazily instead of loading the whole file into\n// memory. Typed locally — the global DOM `Blob` is what the Uploader wants, and\n// not every @types/node version exposes openAsBlob.\ntype OpenAsBlob = (path: string, options?: { type?: string }) => Promise<Blob>;\nconst openAsBlob = (fs as unknown as { openAsBlob?: OpenAsBlob }).openAsBlob;\n\n// Node's `Buffer<ArrayBufferLike>` and the DOM lib's `BlobPart`\n// (`ArrayBufferView<ArrayBuffer>`) don't line up under recent @types/node, even\n// though every value passed here is a valid Blob part at runtime. Cast once.\nfunction toBlobPart(data: ArrayBuffer | ArrayBufferView): BlobPart {\n return data as unknown as BlobPart;\n}\n\nfunction isReadable(x: unknown): x is Readable {\n return (\n !!x &&\n typeof (x as Readable).pipe === 'function' &&\n typeof (x as { read?: unknown }).read === 'function'\n );\n}\n\nasync function streamToBuffer(stream: Readable): Promise<Buffer> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(\n Buffer.isBuffer(chunk)\n ? chunk\n : typeof chunk === 'string'\n ? Buffer.from(chunk)\n : Buffer.from(chunk as Uint8Array),\n );\n }\n return Buffer.concat(chunks);\n}\n\n/**\n * Normalize any {@link NodeUploadSource} into a `{ blob, filename, mimetype }`\n * the core Uploader can consume. Caller-supplied `filename` / `mimeType` always\n * win over what we infer from the path or the blob.\n */\nexport async function resolveSource(\n source: NodeUploadSource,\n opts: { filename?: string; mimeType?: string } = {},\n): Promise<ResolvedSource> {\n // 1. File path on disk.\n if (typeof source === 'string') {\n const filename = opts.filename || basename(source) || 'untitled';\n const mimetype = opts.mimeType || mimeFromName(filename);\n try {\n const blob = openAsBlob\n ? await openAsBlob(source, { type: mimetype })\n : new Blob([toBlobPart(await readFile(source))], { type: mimetype });\n return { blob, filename, mimetype };\n } catch (cause) {\n throw makeError('VALIDATION', `Could not read file at \"${source}\".`, {\n retryable: false,\n cause,\n });\n }\n }\n\n // 2. Already a Blob / File.\n if (typeof Blob !== 'undefined' && source instanceof Blob) {\n const filename = opts.filename || (source as Blob & { name?: string }).name || 'untitled';\n const mimetype = opts.mimeType || source.type || mimeFromName(filename);\n // Only re-wrap when we need to stamp a type the blob doesn't already carry.\n const blob =\n opts.mimeType && opts.mimeType !== source.type\n ? new Blob([source], { type: opts.mimeType })\n : source;\n return { blob, filename, mimetype };\n }\n\n // 3. Node Readable stream — buffer it fully; multipart needs the size up front.\n if (isReadable(source)) {\n const filename = opts.filename || 'untitled';\n const mimetype = opts.mimeType || mimeFromName(filename);\n const buf = await streamToBuffer(source);\n return { blob: new Blob([toBlobPart(buf)], { type: mimetype }), filename, mimetype };\n }\n\n // 4. Buffer / Uint8Array / ArrayBuffer (Buffer is an ArrayBuffer view).\n if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {\n const filename = opts.filename || 'untitled';\n const mimetype = opts.mimeType || mimeFromName(filename);\n return { blob: new Blob([toBlobPart(source)], { type: mimetype }), filename, mimetype };\n }\n\n throw makeError(\n 'VALIDATION',\n 'Unsupported upload source. Pass a file path, Buffer, Uint8Array, ArrayBuffer, Blob, or Readable stream.',\n { retryable: false },\n );\n}\n","import { basename } from 'node:path';\nimport { Uploader } from '../uploader.js';\nimport { makeError } from '../errors.js';\nimport { NODE_SDK_VERSION_HEADER } from '../version.js';\nimport { resolveSource, type NodeUploadSource } from './source.js';\nimport type {\n ClientConfig,\n ResolvedClientConfig,\n UploadOptions,\n BatchUploadOptions,\n UploadedFile,\n PickedFile,\n PickResponse,\n UploadError,\n} from '../types.js';\n\n/**\n * UnionStack API URL baked into the SDK. Consumers never see or set this — we\n * ship a new SDK version if the URL ever changes. (Same constant as the browser\n * client; kept independent so the Node entry doesn't pull in client.ts.)\n */\nconst API_BASE = 'https://api.unionstack.in/v1';\n\n// File-level fan-out for uploadMany. Each file still runs its own part-level\n// concurrency (opts.concurrency), so this caps how many files upload at once —\n// keeps a 10k-file batch from opening 10k sockets / file handles simultaneously.\nconst DEFAULT_FILE_CONCURRENCY = 5;\n\n/**\n * Server-side UnionStack client for Node.js. Uploads files straight to your\n * UnionStack bucket from a backend — no browser, no picker, no DOM.\n *\n * Use an API key whose allowed origins include `*` (server-to-server requests\n * carry no `Origin` header). Create the key and add `*` from your dashboard.\n *\n * ```ts\n * import { UnionStack } from '@masters-union/union-stack/node';\n *\n * const client = UnionStack.init({ apiKey: process.env.UNIONSTACK_API_KEY! });\n * const file = await client.upload('./invoice.pdf');\n * console.log(file.url);\n * ```\n */\nexport class UnionStackNodeClient {\n private uploader: Uploader;\n private cfg: ResolvedClientConfig;\n\n constructor(cfg: ClientConfig) {\n // A server SDK can throw on misconfiguration — unlike the browser client,\n // there's no render path to crash.\n if (!cfg?.apiKey) {\n throw makeError('CONFIG', 'apiKey is required. Pass { apiKey: \"unionstack_live_…\" }.', {\n retryable: false,\n });\n }\n this.cfg = {\n ...cfg,\n apiBase: API_BASE,\n // No picker on the server — never prefetch the picker config.\n skipConfigPrefetch: true,\n // Tag the traffic as server-to-server (\"node/…\") so cdn-be can tell it\n // apart from browser SDK calls (\"js/…\").\n sdkHeader: NODE_SDK_VERSION_HEADER,\n };\n this.uploader = new Uploader(this.cfg);\n }\n\n /**\n * Upload one file. Accepts a path on disk, a Buffer/Uint8Array/ArrayBuffer, a\n * Blob/File, or a Readable stream. Returns the stored file (with its public\n * `url`). Rejects with an {@link UploadError}.\n */\n async upload(source: NodeUploadSource, opts: UploadOptions = {}): Promise<UploadedFile> {\n const resolved = await resolveSource(source, { filename: opts.filename, mimeType: opts.mimeType });\n return this.uploader.upload(resolved.blob, {\n ...opts,\n filename: resolved.filename,\n mimeType: resolved.mimetype,\n });\n }\n\n /**\n * Upload many files. Runs up to {@link DEFAULT_FILE_CONCURRENCY} files at once\n * and never rejects on a single file's failure — successes and failures come\n * back separately so a partial failure doesn't drop the whole batch.\n */\n async uploadMany(sources: NodeUploadSource[], opts: BatchUploadOptions = {}): Promise<PickResponse> {\n if (!Array.isArray(sources) || sources.length === 0) {\n const err = makeError('VALIDATION', 'uploadMany requires a non-empty array of sources.', {\n retryable: false,\n });\n opts.onError?.(err);\n throw err;\n }\n\n const filesUploaded: UploadedFile[] = [];\n const filesFailed: Array<{ file: PickedFile; error: UploadError }> = [];\n\n let cursor = 0;\n const worker = async (): Promise<void> => {\n while (true) {\n const i = cursor++;\n if (i >= sources.length) return;\n\n let resolved;\n try {\n resolved = await resolveSource(sources[i], {\n filename: opts.filename,\n mimeType: opts.mimeType,\n });\n } catch (err) {\n filesFailed.push({ file: describeUnresolved(sources[i], i, opts), error: err as UploadError });\n continue;\n }\n\n const picked: PickedFile = {\n uploadId: String(i),\n filename: resolved.filename,\n mimetype: resolved.mimetype,\n size: resolved.blob.size,\n source: 'local',\n };\n try {\n const uploaded = await this.uploader.upload(resolved.blob, {\n ...opts,\n filename: resolved.filename,\n mimeType: resolved.mimetype,\n });\n uploaded.uploadId = picked.uploadId;\n filesUploaded.push(uploaded);\n } catch (err) {\n filesFailed.push({ file: picked, error: err as UploadError });\n }\n }\n };\n\n const pool = Math.max(1, Math.min(DEFAULT_FILE_CONCURRENCY, sources.length));\n await Promise.all(Array.from({ length: pool }, worker));\n\n const result: PickResponse = { filesUploaded, filesFailed };\n opts.onUploadDone?.(result);\n return result;\n }\n\n /** Read-only view of the resolved config. */\n get config(): Readonly<ResolvedClientConfig> {\n return this.cfg;\n }\n}\n\n/** Best-effort descriptor for a file that failed before we could read it. */\nfunction describeUnresolved(\n source: NodeUploadSource,\n index: number,\n opts: BatchUploadOptions,\n): PickedFile {\n return {\n uploadId: String(index),\n filename: opts.filename || (typeof source === 'string' ? basename(source) : 'untitled'),\n mimetype: opts.mimeType || 'application/octet-stream',\n size: 0,\n source: 'local',\n };\n}\n\nexport const UnionStack = {\n /** Create a server-side client. `cfg.apiKey` is required. */\n init(cfg: ClientConfig): UnionStackNodeClient {\n return new UnionStackNodeClient(cfg);\n },\n};\n\nexport type { NodeUploadSource } from './source.js';\nexport type {\n ClientConfig,\n UploadOptions,\n BatchUploadOptions,\n PickedFile,\n UploadedFile,\n ProgressEvent,\n PickResponse,\n UploadError,\n UploadErrorCode,\n Source,\n} from '../types.js';\n"]}
package/dist/picker.cjs CHANGED
@@ -452,14 +452,56 @@ var BASE_CSS = `
452
452
  width: 28px; height: 28px;
453
453
  display: inline-flex; align-items: center; justify-content: center;
454
454
  color: var(--us-muted); border-radius: 6px;
455
- transition: color 100ms, background 0ms;
455
+ transition: color 100ms, background 0ms, transform 90ms ease;
456
456
  }
457
457
  .us-file-action:hover { background: var(--us-overlay); color: var(--us-fg); }
458
458
  .us-file-action:focus-visible { outline: 2px solid var(--us-primary); outline-offset: 1px; }
459
+ .us-file-action:active { transform: scale(0.92); }
459
460
  .us-file-action svg { width: 15px; height: 15px; }
460
461
  .us-file-action[data-edited="true"] { color: var(--us-accent); }
461
462
  .us-file:not([data-state="queued"]) .us-file-action { display: none; }
462
463
 
464
+ /* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 per-row remove button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
465
+ /* Destructive affordance \u2014 reads as danger on hover, not before. */
466
+ .us-file-remove:hover {
467
+ color: var(--us-danger);
468
+ background: color-mix(in srgb, var(--us-danger) 12%, transparent);
469
+ }
470
+ /* Rows that failed validation at selection time keep their remove button so
471
+ the user can clear them (the generic rule above hides non-queued actions). */
472
+ .us-file[data-state="failed"] .us-file-remove { display: inline-flex; }
473
+ /* Once upload starts the batch is locked \u2014 no row removal anywhere. */
474
+ .us-picker[data-uploading="true"] .us-file-remove { display: none; }
475
+
476
+ /* Row exit: content slips up and fades (130ms), then the emptied slot
477
+ collapses (190ms, same spring-out curve as the entrance) so neighbors
478
+ glide into place instead of snapping. Total \u2248240ms \u2014 faster than the
479
+ 280ms entrance, per exit-faster-than-enter. */
480
+ .us-file[data-removing="true"] {
481
+ animation: none;
482
+ pointer-events: none;
483
+ opacity: 0;
484
+ transform: translateY(-4px) scale(0.98);
485
+ height: 0 !important;
486
+ min-height: 0 !important;
487
+ padding-top: 0 !important;
488
+ padding-bottom: 0 !important;
489
+ border-top-width: 0 !important;
490
+ border-bottom-width: 0 !important;
491
+ overflow: hidden;
492
+ transition:
493
+ opacity 130ms cubic-bezier(0.4, 0, 1, 1),
494
+ transform 130ms cubic-bezier(0.4, 0, 1, 1),
495
+ height 190ms cubic-bezier(0.16, 1, 0.3, 1) 50ms,
496
+ padding 190ms cubic-bezier(0.16, 1, 0.3, 1) 50ms,
497
+ margin 190ms cubic-bezier(0.16, 1, 0.3, 1) 50ms,
498
+ border-width 190ms cubic-bezier(0.16, 1, 0.3, 1) 50ms;
499
+ }
500
+ /* Absorb the flex gap the row leaves behind so the list height lands exactly
501
+ where it will be once the node is detached \u2014 no end-of-animation snap. */
502
+ .us-file[data-removing="true"]:not(:last-child) { margin-bottom: -8px !important; }
503
+ .us-file[data-removing="true"]:last-child:not(:only-child) { margin-top: -8px !important; }
504
+
463
505
  /* \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 */
464
506
  .us-editor {
465
507
  position: absolute; inset: 0; z-index: 2;
@@ -1253,7 +1295,7 @@ function mergeConfig(server, runtime) {
1253
1295
  return merged;
1254
1296
  }
1255
1297
  var DEFAULT_TITLE = "Upload files";
1256
- var FOOTER_LINK = "https://unionstack.link";
1298
+ var FOOTER_LINK = "https://unionstack.in";
1257
1299
  var ICON2 = {
1258
1300
  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>`,
1259
1301
  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>`,
@@ -1271,7 +1313,8 @@ var ICON2 = {
1271
1313
  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>`,
1272
1314
  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>`,
1273
1315
  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>`,
1274
- 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>`
1316
+ 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>`,
1317
+ trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>`
1275
1318
  };
1276
1319
  function iconForMime(mime) {
1277
1320
  if (!mime) return ICON2.file;
@@ -1801,6 +1844,16 @@ var Picker = class {
1801
1844
  row.appendChild(edit);
1802
1845
  item.$edit = edit;
1803
1846
  }
1847
+ if (item.state === "queued" || item.state === "failed") {
1848
+ const remove = document.createElement("button");
1849
+ remove.type = "button";
1850
+ remove.className = "us-file-action us-file-remove";
1851
+ remove.setAttribute("aria-label", `Remove ${item.file.name}`);
1852
+ remove.title = "Remove file";
1853
+ remove.innerHTML = ICON2.trash;
1854
+ remove.onclick = () => this.removeItem(item);
1855
+ row.appendChild(remove);
1856
+ }
1804
1857
  const status = el2("div", "us-file-status");
1805
1858
  status.setAttribute("aria-label", this.statusLabel(item));
1806
1859
  status.innerHTML = this.statusIcon(item.state);
@@ -1846,6 +1899,57 @@ var Picker = class {
1846
1899
  }
1847
1900
  if (item.$edit) item.$edit.dataset.edited = edited ? "true" : "false";
1848
1901
  }
1902
+ // Drop a file from the batch before upload starts. The row animates out
1903
+ // (content fades, then the slot collapses so neighbors glide up) unless the
1904
+ // user prefers reduced motion. State is pruned synchronously — only the DOM
1905
+ // exit is deferred.
1906
+ removeItem(item) {
1907
+ if (this.uploadStarted) return;
1908
+ const idx = this.items.indexOf(item);
1909
+ if (idx === -1) return;
1910
+ this.items.splice(idx, 1);
1911
+ item.validateSeq = ++this.validateSeqCounter;
1912
+ item.validating = false;
1913
+ if (item.objectUrl) {
1914
+ URL.revokeObjectURL(item.objectUrl);
1915
+ item.objectUrl = void 0;
1916
+ }
1917
+ const row = item.$row;
1918
+ item.$row = void 0;
1919
+ if (row) {
1920
+ if (row.contains(document.activeElement)) {
1921
+ const rows = Array.from(
1922
+ this.$list?.querySelectorAll(".us-file:not([data-removing])") ?? []
1923
+ );
1924
+ const pos = rows.indexOf(row);
1925
+ const neighbor = rows[pos + 1] ?? rows[pos - 1];
1926
+ const target = neighbor?.querySelector(".us-file-remove") ?? this.$panel?.querySelector('.us-source-tab[data-active="true"]') ?? this.$panel;
1927
+ target?.focus();
1928
+ }
1929
+ const reduceMotion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1930
+ if (reduceMotion) {
1931
+ row.remove();
1932
+ } else {
1933
+ row.style.height = `${row.offsetHeight}px`;
1934
+ void row.offsetHeight;
1935
+ row.dataset.removing = "true";
1936
+ let removed = false;
1937
+ const finalize = () => {
1938
+ if (!removed) {
1939
+ removed = true;
1940
+ row.remove();
1941
+ }
1942
+ };
1943
+ row.addEventListener("transitionend", (e) => {
1944
+ if (e.target === row && e.propertyName === "height") finalize();
1945
+ });
1946
+ setTimeout(finalize, 400);
1947
+ }
1948
+ }
1949
+ this.updateSummary();
1950
+ this.refreshConfirm();
1951
+ this.maybeAutoUpload();
1952
+ }
1849
1953
  // Mount the image editor overlay on top of the picker panel for the given
1850
1954
  // queued item. The editor calls back with either an edited File (Apply) or
1851
1955
  // nothing (Cancel/Back).
@@ -2065,6 +2169,7 @@ var Picker = class {
2065
2169
  const queued = this.items.filter((i) => i.state === "queued");
2066
2170
  if (queued.length === 0) return;
2067
2171
  this.uploadStarted = true;
2172
+ this.$panel?.setAttribute("data-uploading", "true");
2068
2173
  if (this.$confirm) {
2069
2174
  this.$confirm.disabled = true;
2070
2175
  this.$confirm.textContent = "Uploading\u2026";