@thetechfossil/upfiles 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -1,46 +1,48 @@
1
- # @upfiles/uploader
1
+ # @thetechfossil/upfiles
2
2
 
3
- Lightweight JavaScript client and React component to upload files to an UpFiles server (presigned S3 under the hood). Similar to UploadThing-like DX.
3
+ Lightweight JavaScript client and React component to upload files via a presigned S3 flow. Similar to UploadThing-like DX.
4
4
 
5
5
  - Client: get presigned URLs and PUT to S3
6
6
  - React `<Uploader />`: drag/drop, progress, single/multiple, accepts/types, size limits
7
+ - React `<ProjectFilesWidget />`: list all files for a project (Plugin API), pick one, and optionally auto-save to your app
7
8
 
8
9
  ## Installation
9
10
 
10
11
  ```bash
11
- npm install @upfiles/uploader
12
- # or
13
- pnpm add @upfiles/uploader
14
- # or
15
- yarn add @upfiles/uploader
12
+ # Bun (recommended)
13
+ bun add @thetechfossil/upfiles
14
+
15
+ # npm
16
+ npm install @thetechfossil/upfiles
17
+
18
+ # pnpm
19
+ pnpm add @thetechfossil/upfiles
20
+
21
+ # yarn
22
+ yarn add @thetechfossil/upfiles
16
23
  ```
17
24
 
18
25
  ## Server prerequisites
19
26
 
20
- Your server must expose a presign endpoint compatible with:
27
+ Your Upfiles app must expose the Plugin API presign endpoint:
21
28
 
22
29
  - Method: `POST`
23
- - Path: `/api/upload/presigned-url` (configurable via `clientOptions.presignPath`)
24
- - Body: `{ fileName, fileType, fileSize? }`
25
- - Response: `{ presignedUrl, fileKey, publicUrl }`
30
+ - Path: `/api/plugin/upload/presigned-url` (default; configurable)
31
+ - Body: `{ fileName, fileType, fileSize, folderPath?, projectId? }` (`fileSize` is required)
32
+ - Response: `{ presignedUrl, fileKey, publicUrl, apiKeyId?, projectId? }`
26
33
 
27
- This repository already provides that at `src/app/api/upload/presigned-url/route.ts`.
34
+ This repository already provides a Next.js route at `src/app/api/plugin/upload/presigned-url/route.ts`.
28
35
 
29
- If your server requires auth, pass cookies (withCredentials) or an Authorization header in `clientOptions.headers`.
36
+ For plugins/packages calling across origins, authenticate using an API key header as documented below. CORS must allow your plugin origin.
30
37
 
31
38
  ## Basic usage (React)
32
39
 
33
40
  ```tsx
34
- import { Uploader } from '@upfiles/uploader';
41
+ import { Uploader } from '@thetechfossil/upfiles';
35
42
 
36
43
  export default function Page() {
37
44
  return (
38
45
  <Uploader
39
- clientOptions={{
40
- baseUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:4000',
41
- // headers: { Authorization: `Bearer ${token}` },
42
- withCredentials: true, // if your server uses NextAuth session cookies
43
- }}
44
46
  multiple
45
47
  accept={["image/*", "application/pdf"]}
46
48
  maxFileSize={100 * 1024 * 1024}
@@ -52,6 +54,13 @@ export default function Page() {
52
54
  }}
53
55
  dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
54
56
  buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
57
+ // Optional: pass clientOptions for cross-origin + API key auth
58
+ // clientOptions={{
59
+ // baseUrl: 'https://YOUR_APP_HOST',
60
+ // // or presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url',
61
+ // apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
62
+ // apiKeyHeader: 'authorization', // or 'x-api-key' | 'x-up-api-key'
63
+ // }}
55
64
  />
56
65
  );
57
66
  }
@@ -60,18 +69,25 @@ export default function Page() {
60
69
  ## Basic usage (vanilla JS)
61
70
 
62
71
  ```ts
63
- import { UpfilesClient } from '@upfiles/uploader';
72
+ import { UpfilesClient } from '@thetechfossil/upfiles';
64
73
 
74
+ // Same-origin by default; only set these when needed:
65
75
  const client = new UpfilesClient({
66
- baseUrl: 'http://localhost:4000',
67
- withCredentials: true,
76
+ // presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url', // full override
77
+ // baseUrl: 'https://YOUR_APP_HOST', // used with presignPath
78
+ // presignPath: '/api/plugin/upload/presigned-url', // default
79
+ // withCredentials: true, // for session routes (not typical for Plugin API)
80
+ // Provide API key for Plugin API auth:
81
+ // apiKey: 'upk_...'
82
+ // apiKeyHeader: 'authorization' // or 'x-api-key' | 'x-up-api-key'
68
83
  });
69
84
 
70
85
  async function upload(file) {
71
86
  const { presignedUrl, publicUrl } = await client.getPresignedUrl({
72
87
  fileName: file.name,
73
88
  fileType: file.type,
74
- fileSize: file.size,
89
+ fileSize: file.size, // required
90
+ folderPath: 'my-plugin/uploads/',
75
91
  });
76
92
  const res = await client.uploadToS3(presignedUrl, file);
77
93
  if (!res.ok) throw new Error('Upload failed');
@@ -82,25 +98,101 @@ async function upload(file) {
82
98
  ## API
83
99
 
84
100
  - `new UpfilesClient(options)`
85
- - `baseUrl` (required): your UpFiles app base URL
86
- - `presignPath` (optional): default `/api/upload/presigned-url`
87
- - `headers` (optional): headers for your server, e.g. Authorization
88
- - `withCredentials` (optional): `true` to send cookies
89
- - `client.getPresignedUrl({ fileName, fileType, fileSize? })`
101
+ - `presignUrl` (optional): full absolute URL to presign endpoint. Highest priority.
102
+ - `baseUrl` (optional): API base origin. Used with `presignPath`.
103
+ - `presignPath` (optional): relative path, default `/api/plugin/upload/presigned-url`.
104
+ - `headers` (optional): extra headers to send.
105
+ - `apiKey` (optional): API key value (e.g., `upk_...`).
106
+ - `apiKeyHeader` (optional): one of `'authorization' | 'x-api-key' | 'x-up-api-key'` (default `'authorization'`). If `'authorization'`, `Bearer` prefix is auto-added for `upk_...`.
107
+ - `withCredentials` (optional): `true` to send cookies.
108
+ - Defaults: same-origin requests to `/api/plugin/upload/presigned-url`.
109
+ - `client.getPresignedUrl({ fileName, fileType, fileSize, projectId?, folderPath? })`
90
110
  - `client.uploadToS3(presignedUrl, file)`
111
+ - `client.upload(file, { projectId?, folderPath?, fetchThumbnails? })` → returns `{ publicUrl, fileKey, fileName, fileType, fileSize, projectId?, apiKeyId?, thumbnails? }`
112
+ - `client.getThumbnails(fileKey)` → returns `Thumbnail[]`
113
+ - `client.getProjectFiles({ folderPath? })` → returns `FileListItem[]`
91
114
 
92
115
  - `<Uploader />` props
93
- - `clientOptions`: same as `UpfilesClient` options
116
+ - `clientOptions` (optional): same as `UpfilesClient` options (only needed for cross-origin or auth customization)
94
117
  - `multiple` (default true)
95
118
  - `accept` (array of MIME patterns)
96
119
  - `maxFileSize` (bytes, default 100MB)
97
120
  - `maxFiles` (default 10)
121
+ - `fetchThumbnails` (boolean): if true, fetch thumbnails after upload via Plugin API
98
122
  - `onComplete(files)` and `onError(error)`
99
123
  - `className`, `buttonClassName`, `dropzoneClassName`
100
124
  - `children`: custom dropzone inner content
101
125
 
126
+ ### Thumbnails usage
127
+
128
+ Your Upfiles app exposes `GET /api/plugin/thumbnails?fileKey=...` for plugins. Enable `fetchThumbnails` to auto-fetch after upload, or call `client.getThumbnails(fileKey)` yourself.
129
+
130
+ Example with `client.upload`:
131
+
132
+ ```ts
133
+ const result = await client.upload(file, { folderPath: 'my-plugin/', fetchThumbnails: true });
134
+ console.log(result.thumbnails);
135
+ ```
136
+
137
+ ### List project files (Plugin API)
138
+
139
+ Your Upfiles app exposes `GET /api/plugin/files?folderPath=...` for plugins (API key auth + CORS). The client can call it via `getProjectFiles`:
140
+
141
+ ```ts
142
+ import { UpfilesClient } from '@thetechfossil/upfiles';
143
+
144
+ const client = new UpfilesClient({
145
+ baseUrl: 'https://YOUR_APP_HOST',
146
+ apiKey: 'upk_...',
147
+ thumbnailsPath: '/api/plugin/thumbnails', // used to derive /api/plugin/files
148
+ });
149
+
150
+ const files = await client.getProjectFiles({ folderPath: 'optional/subfolder' });
151
+ // [{ key, originalName, size, contentType, uploadedAt, url }, ...]
152
+ ```
153
+
154
+ ### `<ProjectFilesWidget />`
155
+
156
+ Simple UI to fetch and select a file for the project tied to the API key. Emits the selection, and can optionally POST it to your backend.
157
+
158
+ ```tsx
159
+ import { ProjectFilesWidget } from '@thetechfossil/upfiles';
160
+
161
+ export default function PickFile() {
162
+ return (
163
+ <ProjectFilesWidget
164
+ clientOptions={{
165
+ baseUrl: 'https://YOUR_APP_HOST',
166
+ apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
167
+ apiKeyHeader: 'authorization',
168
+ thumbnailsPath: '/api/plugin/thumbnails',
169
+ }}
170
+ // Optional: filter within a folder
171
+ // folderPath="my-plugin/"
172
+
173
+ onSelect={(f) => {
174
+ // { name, key, url, size, contentType }
175
+ console.log('Selected:', f.name, f.key);
176
+ }}
177
+
178
+ // Optional: built-in save
179
+ // saveUrl="/api/files/save" // Your app route to persist selection
180
+ // onSave={async (f) => { await fetch('/api/files/save', { method: 'POST', body: JSON.stringify(f) }); }}
181
+ // onSaved={(result) => console.log('Saved!', result)}
182
+ />
183
+ );
184
+ }
185
+ ```
186
+
102
187
  ## Notes
103
188
 
104
- - Ensure CORS for your app domain (S3 bucket CORS and your server CORS as needed).
105
- - For cross-origin usage with cookies (NextAuth), set `withCredentials: true` and configure your server to allow credentials.
106
- - Consider adding API token auth if distributing the SDK publicly.
189
+ - Dev with Vite: configure a proxy so `/api/*` maps to your backend (e.g., `http://localhost:4000`), then `<Uploader />` works with no config.
190
+ - Ensure CORS where applicable (S3 bucket CORS; and set `PLUGIN_ALLOWED_ORIGINS` on your Upfiles app for cross-origin Plugin API calls).
191
+ - For cross-origin with cookies (NextAuth), set `withCredentials: true` and configure your server to allow credentials.
192
+ Plugin API typically uses API keys instead of cookies.
193
+
194
+ ### Plugin endpoints provided by this repo
195
+
196
+ - `POST /api/plugin/upload/presigned-url` – generate presigned S3 URL for upload
197
+ - `GET /api/plugin/thumbnails?fileKey=...` – list thumbnails for a file
198
+ - `GET /api/plugin/files?folderPath=...` – list project files (API key scoped), optional folder prefix
package/dist/index.d.mts CHANGED
@@ -4,27 +4,72 @@ type PresignResponse = {
4
4
  presignedUrl: string;
5
5
  fileKey: string;
6
6
  publicUrl: string;
7
+ apiKeyId?: string;
8
+ projectId?: string;
9
+ };
10
+ type UploadedFileResult = {
11
+ publicUrl: string;
12
+ fileKey: string;
13
+ fileName: string;
14
+ fileType: string;
15
+ fileSize: number;
16
+ projectId?: string;
17
+ apiKeyId?: string;
18
+ thumbnails?: Thumbnail[];
19
+ };
20
+ type Thumbnail = {
21
+ id: string;
22
+ key: string;
23
+ url: string;
24
+ size: number;
25
+ sizeType?: string;
26
+ createdAt?: string;
27
+ };
28
+ type FileListItem = {
29
+ key: string;
30
+ originalName: string;
31
+ size: number;
32
+ contentType: string;
33
+ uploadedAt: string;
34
+ url: string;
7
35
  };
8
36
  type UpfilesClientOptions = {
9
- baseUrl: string;
37
+ baseUrl?: string;
10
38
  presignPath?: string;
39
+ presignUrl?: string;
11
40
  headers?: Record<string, string>;
12
41
  withCredentials?: boolean;
42
+ apiKey?: string;
43
+ apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
44
+ thumbnailsPath?: string;
13
45
  };
14
46
  declare class UpfilesClient {
15
- private baseUrl;
47
+ private baseUrl?;
16
48
  private presignPath;
49
+ private presignUrl?;
17
50
  private headers?;
18
51
  private withCredentials?;
52
+ private apiKey?;
53
+ private apiKeyHeader;
54
+ private thumbnailsPath;
19
55
  constructor(opts: UpfilesClientOptions);
20
56
  getPresignedUrl(params: {
21
57
  fileName: string;
22
58
  fileType: string;
23
- fileSize?: number;
59
+ fileSize: number;
24
60
  projectId?: string;
25
61
  folderPath?: string;
26
62
  }): Promise<PresignResponse>;
27
63
  uploadToS3(presignedUrl: string, file: File): Promise<Response>;
64
+ upload(file: File, extras?: {
65
+ projectId?: string;
66
+ folderPath?: string;
67
+ fetchThumbnails?: boolean;
68
+ }): Promise<UploadedFileResult>;
69
+ getThumbnails(fileKey: string): Promise<Thumbnail[]>;
70
+ getProjectFiles(params?: {
71
+ folderPath?: string;
72
+ }): Promise<FileListItem[]>;
28
73
  }
29
74
 
30
75
  type UploaderFile = {
@@ -38,9 +83,11 @@ type UploaderFile = {
38
83
  error?: string;
39
84
  publicUrl?: string;
40
85
  fileKey?: string;
86
+ projectId?: string;
87
+ apiKeyId?: string;
41
88
  };
42
89
  type UploaderProps = {
43
- clientOptions: UpfilesClientOptions;
90
+ clientOptions?: UpfilesClientOptions;
44
91
  multiple?: boolean;
45
92
  accept?: string[];
46
93
  maxFileSize?: number;
@@ -53,7 +100,33 @@ type UploaderProps = {
53
100
  buttonClassName?: string;
54
101
  dropzoneClassName?: string;
55
102
  children?: React.ReactNode;
103
+ fetchThumbnails?: boolean;
56
104
  };
57
105
  declare const Uploader: React.FC<UploaderProps>;
58
106
 
59
- export { type PresignResponse, UpfilesClient, type UpfilesClientOptions, Uploader, type UploaderFile, type UploaderProps };
107
+ type ProjectFilesWidgetProps = {
108
+ clientOptions: UpfilesClientOptions;
109
+ folderPath?: string;
110
+ className?: string;
111
+ listClassName?: string;
112
+ itemClassName?: string;
113
+ onSelect: (file: {
114
+ name: string;
115
+ key: string;
116
+ url: string;
117
+ size: number;
118
+ contentType: string;
119
+ }) => void;
120
+ saveUrl?: string;
121
+ onSave?: (file: {
122
+ name: string;
123
+ key: string;
124
+ url: string;
125
+ size: number;
126
+ contentType: string;
127
+ }) => Promise<void> | void;
128
+ onSaved?: (result?: any) => void;
129
+ };
130
+ declare const ProjectFilesWidget: React.FC<ProjectFilesWidgetProps>;
131
+
132
+ export { type FileListItem, type PresignResponse, ProjectFilesWidget, type ProjectFilesWidgetProps, type Thumbnail, UpfilesClient, type UpfilesClientOptions, type UploadedFileResult, Uploader, type UploaderFile, type UploaderProps };
package/dist/index.d.ts CHANGED
@@ -4,27 +4,72 @@ type PresignResponse = {
4
4
  presignedUrl: string;
5
5
  fileKey: string;
6
6
  publicUrl: string;
7
+ apiKeyId?: string;
8
+ projectId?: string;
9
+ };
10
+ type UploadedFileResult = {
11
+ publicUrl: string;
12
+ fileKey: string;
13
+ fileName: string;
14
+ fileType: string;
15
+ fileSize: number;
16
+ projectId?: string;
17
+ apiKeyId?: string;
18
+ thumbnails?: Thumbnail[];
19
+ };
20
+ type Thumbnail = {
21
+ id: string;
22
+ key: string;
23
+ url: string;
24
+ size: number;
25
+ sizeType?: string;
26
+ createdAt?: string;
27
+ };
28
+ type FileListItem = {
29
+ key: string;
30
+ originalName: string;
31
+ size: number;
32
+ contentType: string;
33
+ uploadedAt: string;
34
+ url: string;
7
35
  };
8
36
  type UpfilesClientOptions = {
9
- baseUrl: string;
37
+ baseUrl?: string;
10
38
  presignPath?: string;
39
+ presignUrl?: string;
11
40
  headers?: Record<string, string>;
12
41
  withCredentials?: boolean;
42
+ apiKey?: string;
43
+ apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
44
+ thumbnailsPath?: string;
13
45
  };
14
46
  declare class UpfilesClient {
15
- private baseUrl;
47
+ private baseUrl?;
16
48
  private presignPath;
49
+ private presignUrl?;
17
50
  private headers?;
18
51
  private withCredentials?;
52
+ private apiKey?;
53
+ private apiKeyHeader;
54
+ private thumbnailsPath;
19
55
  constructor(opts: UpfilesClientOptions);
20
56
  getPresignedUrl(params: {
21
57
  fileName: string;
22
58
  fileType: string;
23
- fileSize?: number;
59
+ fileSize: number;
24
60
  projectId?: string;
25
61
  folderPath?: string;
26
62
  }): Promise<PresignResponse>;
27
63
  uploadToS3(presignedUrl: string, file: File): Promise<Response>;
64
+ upload(file: File, extras?: {
65
+ projectId?: string;
66
+ folderPath?: string;
67
+ fetchThumbnails?: boolean;
68
+ }): Promise<UploadedFileResult>;
69
+ getThumbnails(fileKey: string): Promise<Thumbnail[]>;
70
+ getProjectFiles(params?: {
71
+ folderPath?: string;
72
+ }): Promise<FileListItem[]>;
28
73
  }
29
74
 
30
75
  type UploaderFile = {
@@ -38,9 +83,11 @@ type UploaderFile = {
38
83
  error?: string;
39
84
  publicUrl?: string;
40
85
  fileKey?: string;
86
+ projectId?: string;
87
+ apiKeyId?: string;
41
88
  };
42
89
  type UploaderProps = {
43
- clientOptions: UpfilesClientOptions;
90
+ clientOptions?: UpfilesClientOptions;
44
91
  multiple?: boolean;
45
92
  accept?: string[];
46
93
  maxFileSize?: number;
@@ -53,7 +100,33 @@ type UploaderProps = {
53
100
  buttonClassName?: string;
54
101
  dropzoneClassName?: string;
55
102
  children?: React.ReactNode;
103
+ fetchThumbnails?: boolean;
56
104
  };
57
105
  declare const Uploader: React.FC<UploaderProps>;
58
106
 
59
- export { type PresignResponse, UpfilesClient, type UpfilesClientOptions, Uploader, type UploaderFile, type UploaderProps };
107
+ type ProjectFilesWidgetProps = {
108
+ clientOptions: UpfilesClientOptions;
109
+ folderPath?: string;
110
+ className?: string;
111
+ listClassName?: string;
112
+ itemClassName?: string;
113
+ onSelect: (file: {
114
+ name: string;
115
+ key: string;
116
+ url: string;
117
+ size: number;
118
+ contentType: string;
119
+ }) => void;
120
+ saveUrl?: string;
121
+ onSave?: (file: {
122
+ name: string;
123
+ key: string;
124
+ url: string;
125
+ size: number;
126
+ contentType: string;
127
+ }) => Promise<void> | void;
128
+ onSaved?: (result?: any) => void;
129
+ };
130
+ declare const ProjectFilesWidget: React.FC<ProjectFilesWidgetProps>;
131
+
132
+ export { type FileListItem, type PresignResponse, ProjectFilesWidget, type ProjectFilesWidgetProps, type Thumbnail, UpfilesClient, type UpfilesClientOptions, type UploadedFileResult, Uploader, type UploaderFile, type UploaderProps };
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ProjectFilesWidget: () => ProjectFilesWidget,
23
24
  UpfilesClient: () => UpfilesClient,
24
25
  Uploader: () => Uploader
25
26
  });
@@ -28,18 +29,36 @@ module.exports = __toCommonJS(index_exports);
28
29
  // src/client.ts
29
30
  var UpfilesClient = class {
30
31
  constructor(opts) {
31
- this.baseUrl = opts.baseUrl.replace(/\/$/, "");
32
- this.presignPath = opts.presignPath ?? "/api/upload/presigned-url";
32
+ this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
33
+ this.presignPath = opts.presignPath ?? "/api/plugin/upload/presigned-url";
34
+ this.presignUrl = opts.presignUrl;
33
35
  this.headers = opts.headers;
34
36
  this.withCredentials = opts.withCredentials;
37
+ this.apiKey = opts.apiKey;
38
+ this.apiKeyHeader = opts.apiKeyHeader ?? "authorization";
39
+ this.thumbnailsPath = opts.thumbnailsPath ?? "/api/plugin/thumbnails";
35
40
  }
36
41
  async getPresignedUrl(params) {
37
- const res = await fetch(`${this.baseUrl}${this.presignPath}`, {
42
+ if (!params.fileName) throw new Error("fileName is required");
43
+ if (!params.fileType) throw new Error("fileType is required");
44
+ if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
45
+ const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
46
+ const headers = {
47
+ "content-type": "application/json",
48
+ ...this.headers || {}
49
+ };
50
+ if (this.apiKey) {
51
+ if (this.apiKeyHeader === "authorization") {
52
+ headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
53
+ } else if (this.apiKeyHeader === "x-api-key") {
54
+ headers["x-api-key"] = this.apiKey;
55
+ } else {
56
+ headers["x-up-api-key"] = this.apiKey;
57
+ }
58
+ }
59
+ const res = await fetch(target, {
38
60
  method: "POST",
39
- headers: {
40
- "Content-Type": "application/json",
41
- ...this.headers || {}
42
- },
61
+ headers,
43
62
  credentials: this.withCredentials ? "include" : "same-origin",
44
63
  body: JSON.stringify(params)
45
64
  });
@@ -63,6 +82,92 @@ var UpfilesClient = class {
63
82
  }
64
83
  });
65
84
  }
85
+ // Convenience: perform full flow and return URL/path metadata for DB
86
+ async upload(file, extras) {
87
+ const presign = await this.getPresignedUrl({
88
+ fileName: file.name,
89
+ fileType: file.type || "application/octet-stream",
90
+ fileSize: file.size,
91
+ projectId: extras?.projectId,
92
+ folderPath: extras?.folderPath
93
+ });
94
+ const res = await this.uploadToS3(presign.presignedUrl, file);
95
+ if (!res.ok) {
96
+ throw new Error(`Upload failed with status ${res.status}`);
97
+ }
98
+ let thumbnails;
99
+ if (extras?.fetchThumbnails) {
100
+ try {
101
+ const t = await this.getThumbnails(presign.fileKey);
102
+ thumbnails = t;
103
+ } catch {
104
+ }
105
+ }
106
+ return {
107
+ publicUrl: presign.publicUrl,
108
+ fileKey: presign.fileKey,
109
+ fileName: file.name,
110
+ fileType: file.type,
111
+ fileSize: file.size,
112
+ projectId: presign.projectId,
113
+ apiKeyId: presign.apiKeyId,
114
+ thumbnails
115
+ };
116
+ }
117
+ async getThumbnails(fileKey) {
118
+ const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
119
+ const url = `${target}?fileKey=${encodeURIComponent(fileKey)}`;
120
+ const headers = { ...this.headers || {} };
121
+ if (this.apiKey) {
122
+ if (this.apiKeyHeader === "authorization") {
123
+ headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
124
+ } else if (this.apiKeyHeader === "x-api-key") {
125
+ headers["x-api-key"] = this.apiKey;
126
+ } else {
127
+ headers["x-up-api-key"] = this.apiKey;
128
+ }
129
+ }
130
+ const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
131
+ if (!res.ok) {
132
+ let msg = "Failed to fetch thumbnails";
133
+ try {
134
+ const data2 = await res.json();
135
+ msg = data2.error || msg;
136
+ } catch {
137
+ }
138
+ throw new Error(msg);
139
+ }
140
+ const data = await res.json();
141
+ return data?.thumbnails ?? [];
142
+ }
143
+ async getProjectFiles(params) {
144
+ const basePath = this.thumbnailsPath.replace(/\/thumbnails$/, "");
145
+ const filesPath = `${basePath}/files`;
146
+ const target = this.baseUrl ? `${this.baseUrl}${filesPath}` : filesPath;
147
+ const url = params?.folderPath ? `${target}?folderPath=${encodeURIComponent(params.folderPath)}` : target;
148
+ const headers = { ...this.headers || {} };
149
+ if (this.apiKey) {
150
+ if (this.apiKeyHeader === "authorization") {
151
+ headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
152
+ } else if (this.apiKeyHeader === "x-api-key") {
153
+ headers["x-api-key"] = this.apiKey;
154
+ } else {
155
+ headers["x-up-api-key"] = this.apiKey;
156
+ }
157
+ }
158
+ const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
159
+ if (!res.ok) {
160
+ let msg = "Failed to fetch files";
161
+ try {
162
+ const data2 = await res.json();
163
+ msg = data2.error || msg;
164
+ } catch {
165
+ }
166
+ throw new Error(msg);
167
+ }
168
+ const data = await res.json();
169
+ return data?.files ?? [];
170
+ }
66
171
  };
67
172
 
68
173
  // src/Uploader.tsx
@@ -81,10 +186,11 @@ var Uploader = ({
81
186
  dropzoneClassName,
82
187
  children,
83
188
  projectId,
84
- folderPath
189
+ folderPath,
190
+ fetchThumbnails
85
191
  }) => {
86
192
  const inputRef = (0, import_react.useRef)(null);
87
- const client = (0, import_react.useMemo)(() => new UpfilesClient(clientOptions), [clientOptions]);
193
+ const client = (0, import_react.useMemo)(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
88
194
  const [files, setFiles] = (0, import_react.useState)([]);
89
195
  const [isUploading, setIsUploading] = (0, import_react.useState)(false);
90
196
  const [isDragOver, setIsDragOver] = (0, import_react.useState)(false);
@@ -151,14 +257,31 @@ var Uploader = ({
151
257
  xhr.setRequestHeader("Content-Type", item.type || "application/octet-stream");
152
258
  xhr.send(item.file);
153
259
  });
260
+ let thumbs;
261
+ try {
262
+ if (fetchThumbnails) {
263
+ thumbs = await client.getThumbnails(presign.fileKey);
264
+ }
265
+ } catch {
266
+ }
154
267
  update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
155
- return { ...item, status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl };
268
+ return {
269
+ ...item,
270
+ status: "success",
271
+ progress: 100,
272
+ fileKey: presign.fileKey,
273
+ publicUrl: presign.publicUrl,
274
+ projectId: presign.projectId,
275
+ apiKeyId: presign.apiKeyId,
276
+ // @ts-ignore - allow downstream to access thumbs if present
277
+ thumbnails: thumbs
278
+ };
156
279
  } catch (err) {
157
280
  const message = err instanceof Error ? err.message : "Upload failed";
158
281
  update({ status: "error", error: message });
159
282
  throw err;
160
283
  }
161
- }, [client]);
284
+ }, [client, projectId, folderPath, fetchThumbnails]);
162
285
  const uploadAll = (0, import_react.useCallback)(async () => {
163
286
  const targets = files.filter((f) => f.status === "pending");
164
287
  if (!targets.length) return;
@@ -259,8 +382,152 @@ var Uploader = ({
259
382
  ] })
260
383
  ] });
261
384
  };
385
+
386
+ // src/ProjectFilesWidget.tsx
387
+ var import_react2 = require("react");
388
+ var import_jsx_runtime2 = require("react/jsx-runtime");
389
+ var ProjectFilesWidget = ({
390
+ clientOptions,
391
+ folderPath,
392
+ className,
393
+ listClassName,
394
+ itemClassName,
395
+ onSelect,
396
+ saveUrl,
397
+ onSave,
398
+ onSaved
399
+ }) => {
400
+ const client = (0, import_react2.useMemo)(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
401
+ const [files, setFiles] = (0, import_react2.useState)([]);
402
+ const [loading, setLoading] = (0, import_react2.useState)(false);
403
+ const [error, setError] = (0, import_react2.useState)(null);
404
+ const [query, setQuery] = (0, import_react2.useState)("");
405
+ const [savingKey, setSavingKey] = (0, import_react2.useState)(null);
406
+ (0, import_react2.useEffect)(() => {
407
+ let cancelled = false;
408
+ const run = async () => {
409
+ setLoading(true);
410
+ setError(null);
411
+ try {
412
+ const data = await client.getProjectFiles({ folderPath });
413
+ if (!cancelled) setFiles(data);
414
+ } catch (e) {
415
+ if (!cancelled) setError(e?.message || "Failed to load files");
416
+ } finally {
417
+ if (!cancelled) setLoading(false);
418
+ }
419
+ };
420
+ run();
421
+ return () => {
422
+ cancelled = true;
423
+ };
424
+ }, [client, folderPath]);
425
+ const visible = files.filter(
426
+ (f) => !query ? true : (f.originalName || "").toLowerCase().includes(query.toLowerCase())
427
+ );
428
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, children: [
429
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }, children: [
430
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
431
+ "input",
432
+ {
433
+ type: "text",
434
+ placeholder: "Search by filename...",
435
+ value: query,
436
+ onChange: (e) => setQuery(e.target.value),
437
+ style: { flex: 1, padding: 8, border: "1px solid #e5e7eb", borderRadius: 6 }
438
+ }
439
+ ),
440
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
441
+ "button",
442
+ {
443
+ onClick: () => {
444
+ setQuery("");
445
+ (async () => {
446
+ setLoading(true);
447
+ setError(null);
448
+ try {
449
+ const data = await client.getProjectFiles({ folderPath });
450
+ setFiles(data);
451
+ } catch (e) {
452
+ setError(e?.message || "Failed to load files");
453
+ } finally {
454
+ setLoading(false);
455
+ }
456
+ })();
457
+ },
458
+ style: { padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#fff" },
459
+ children: "Refresh"
460
+ }
461
+ )
462
+ ] }),
463
+ loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 12, color: "#666" }, children: "Loading files\u2026" }),
464
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 12, color: "#b91c1c" }, children: error }),
465
+ !loading && !error && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("ul", { className: listClassName, style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }, children: [
466
+ visible.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
467
+ "li",
468
+ {
469
+ className: itemClassName,
470
+ style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 10, display: "flex", justifyContent: "space-between", alignItems: "center" },
471
+ children: [
472
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { minWidth: 0 }, children: [
473
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.originalName }),
474
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { fontSize: 12, color: "#666" }, children: [
475
+ (f.size / (1024 * 1024)).toFixed(2),
476
+ " MB \u2022 ",
477
+ f.contentType
478
+ ] })
479
+ ] }),
480
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
481
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: f.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
482
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
483
+ "button",
484
+ {
485
+ onClick: async () => {
486
+ const selected = { name: f.originalName, key: f.key, url: f.url, size: f.size, contentType: f.contentType };
487
+ onSelect(selected);
488
+ if (typeof onSave === "function" || !!saveUrl) {
489
+ try {
490
+ setSavingKey(f.key);
491
+ if (typeof onSave === "function") {
492
+ await onSave(selected);
493
+ onSaved?.();
494
+ }
495
+ if (saveUrl) {
496
+ const res = await fetch(saveUrl, {
497
+ method: "POST",
498
+ headers: { "content-type": "application/json" },
499
+ body: JSON.stringify(selected),
500
+ credentials: "same-origin"
501
+ });
502
+ const data = await res.json().catch(() => void 0);
503
+ if (!res.ok) throw new Error(data?.error || "Failed to save");
504
+ onSaved?.(data);
505
+ }
506
+ } catch (e) {
507
+ console.error("Save selection failed", e);
508
+ alert(e?.message || "Failed to save");
509
+ } finally {
510
+ setSavingKey(null);
511
+ }
512
+ }
513
+ },
514
+ disabled: savingKey === f.key,
515
+ style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey === f.key ? 0.7 : 1 },
516
+ children: savingKey === f.key ? "Saving\u2026" : "Use"
517
+ }
518
+ )
519
+ ] })
520
+ ]
521
+ },
522
+ f.key
523
+ )),
524
+ visible.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { style: { fontSize: 12, color: "#666" }, children: "No files found" })
525
+ ] })
526
+ ] });
527
+ };
262
528
  // Annotate the CommonJS export names for ESM import in node:
263
529
  0 && (module.exports = {
530
+ ProjectFilesWidget,
264
531
  UpfilesClient,
265
532
  Uploader
266
533
  });
package/dist/index.mjs CHANGED
@@ -1,18 +1,36 @@
1
1
  // src/client.ts
2
2
  var UpfilesClient = class {
3
3
  constructor(opts) {
4
- this.baseUrl = opts.baseUrl.replace(/\/$/, "");
5
- this.presignPath = opts.presignPath ?? "/api/upload/presigned-url";
4
+ this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
5
+ this.presignPath = opts.presignPath ?? "/api/plugin/upload/presigned-url";
6
+ this.presignUrl = opts.presignUrl;
6
7
  this.headers = opts.headers;
7
8
  this.withCredentials = opts.withCredentials;
9
+ this.apiKey = opts.apiKey;
10
+ this.apiKeyHeader = opts.apiKeyHeader ?? "authorization";
11
+ this.thumbnailsPath = opts.thumbnailsPath ?? "/api/plugin/thumbnails";
8
12
  }
9
13
  async getPresignedUrl(params) {
10
- const res = await fetch(`${this.baseUrl}${this.presignPath}`, {
14
+ if (!params.fileName) throw new Error("fileName is required");
15
+ if (!params.fileType) throw new Error("fileType is required");
16
+ if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
17
+ const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
18
+ const headers = {
19
+ "content-type": "application/json",
20
+ ...this.headers || {}
21
+ };
22
+ if (this.apiKey) {
23
+ if (this.apiKeyHeader === "authorization") {
24
+ headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
25
+ } else if (this.apiKeyHeader === "x-api-key") {
26
+ headers["x-api-key"] = this.apiKey;
27
+ } else {
28
+ headers["x-up-api-key"] = this.apiKey;
29
+ }
30
+ }
31
+ const res = await fetch(target, {
11
32
  method: "POST",
12
- headers: {
13
- "Content-Type": "application/json",
14
- ...this.headers || {}
15
- },
33
+ headers,
16
34
  credentials: this.withCredentials ? "include" : "same-origin",
17
35
  body: JSON.stringify(params)
18
36
  });
@@ -36,6 +54,92 @@ var UpfilesClient = class {
36
54
  }
37
55
  });
38
56
  }
57
+ // Convenience: perform full flow and return URL/path metadata for DB
58
+ async upload(file, extras) {
59
+ const presign = await this.getPresignedUrl({
60
+ fileName: file.name,
61
+ fileType: file.type || "application/octet-stream",
62
+ fileSize: file.size,
63
+ projectId: extras?.projectId,
64
+ folderPath: extras?.folderPath
65
+ });
66
+ const res = await this.uploadToS3(presign.presignedUrl, file);
67
+ if (!res.ok) {
68
+ throw new Error(`Upload failed with status ${res.status}`);
69
+ }
70
+ let thumbnails;
71
+ if (extras?.fetchThumbnails) {
72
+ try {
73
+ const t = await this.getThumbnails(presign.fileKey);
74
+ thumbnails = t;
75
+ } catch {
76
+ }
77
+ }
78
+ return {
79
+ publicUrl: presign.publicUrl,
80
+ fileKey: presign.fileKey,
81
+ fileName: file.name,
82
+ fileType: file.type,
83
+ fileSize: file.size,
84
+ projectId: presign.projectId,
85
+ apiKeyId: presign.apiKeyId,
86
+ thumbnails
87
+ };
88
+ }
89
+ async getThumbnails(fileKey) {
90
+ const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
91
+ const url = `${target}?fileKey=${encodeURIComponent(fileKey)}`;
92
+ const headers = { ...this.headers || {} };
93
+ if (this.apiKey) {
94
+ if (this.apiKeyHeader === "authorization") {
95
+ headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
96
+ } else if (this.apiKeyHeader === "x-api-key") {
97
+ headers["x-api-key"] = this.apiKey;
98
+ } else {
99
+ headers["x-up-api-key"] = this.apiKey;
100
+ }
101
+ }
102
+ const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
103
+ if (!res.ok) {
104
+ let msg = "Failed to fetch thumbnails";
105
+ try {
106
+ const data2 = await res.json();
107
+ msg = data2.error || msg;
108
+ } catch {
109
+ }
110
+ throw new Error(msg);
111
+ }
112
+ const data = await res.json();
113
+ return data?.thumbnails ?? [];
114
+ }
115
+ async getProjectFiles(params) {
116
+ const basePath = this.thumbnailsPath.replace(/\/thumbnails$/, "");
117
+ const filesPath = `${basePath}/files`;
118
+ const target = this.baseUrl ? `${this.baseUrl}${filesPath}` : filesPath;
119
+ const url = params?.folderPath ? `${target}?folderPath=${encodeURIComponent(params.folderPath)}` : target;
120
+ const headers = { ...this.headers || {} };
121
+ if (this.apiKey) {
122
+ if (this.apiKeyHeader === "authorization") {
123
+ headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
124
+ } else if (this.apiKeyHeader === "x-api-key") {
125
+ headers["x-api-key"] = this.apiKey;
126
+ } else {
127
+ headers["x-up-api-key"] = this.apiKey;
128
+ }
129
+ }
130
+ const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
131
+ if (!res.ok) {
132
+ let msg = "Failed to fetch files";
133
+ try {
134
+ const data2 = await res.json();
135
+ msg = data2.error || msg;
136
+ } catch {
137
+ }
138
+ throw new Error(msg);
139
+ }
140
+ const data = await res.json();
141
+ return data?.files ?? [];
142
+ }
39
143
  };
40
144
 
41
145
  // src/Uploader.tsx
@@ -54,10 +158,11 @@ var Uploader = ({
54
158
  dropzoneClassName,
55
159
  children,
56
160
  projectId,
57
- folderPath
161
+ folderPath,
162
+ fetchThumbnails
58
163
  }) => {
59
164
  const inputRef = useRef(null);
60
- const client = useMemo(() => new UpfilesClient(clientOptions), [clientOptions]);
165
+ const client = useMemo(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
61
166
  const [files, setFiles] = useState([]);
62
167
  const [isUploading, setIsUploading] = useState(false);
63
168
  const [isDragOver, setIsDragOver] = useState(false);
@@ -124,14 +229,31 @@ var Uploader = ({
124
229
  xhr.setRequestHeader("Content-Type", item.type || "application/octet-stream");
125
230
  xhr.send(item.file);
126
231
  });
232
+ let thumbs;
233
+ try {
234
+ if (fetchThumbnails) {
235
+ thumbs = await client.getThumbnails(presign.fileKey);
236
+ }
237
+ } catch {
238
+ }
127
239
  update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
128
- return { ...item, status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl };
240
+ return {
241
+ ...item,
242
+ status: "success",
243
+ progress: 100,
244
+ fileKey: presign.fileKey,
245
+ publicUrl: presign.publicUrl,
246
+ projectId: presign.projectId,
247
+ apiKeyId: presign.apiKeyId,
248
+ // @ts-ignore - allow downstream to access thumbs if present
249
+ thumbnails: thumbs
250
+ };
129
251
  } catch (err) {
130
252
  const message = err instanceof Error ? err.message : "Upload failed";
131
253
  update({ status: "error", error: message });
132
254
  throw err;
133
255
  }
134
- }, [client]);
256
+ }, [client, projectId, folderPath, fetchThumbnails]);
135
257
  const uploadAll = useCallback(async () => {
136
258
  const targets = files.filter((f) => f.status === "pending");
137
259
  if (!targets.length) return;
@@ -232,7 +354,151 @@ var Uploader = ({
232
354
  ] })
233
355
  ] });
234
356
  };
357
+
358
+ // src/ProjectFilesWidget.tsx
359
+ import { useEffect, useMemo as useMemo2, useState as useState2 } from "react";
360
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
361
+ var ProjectFilesWidget = ({
362
+ clientOptions,
363
+ folderPath,
364
+ className,
365
+ listClassName,
366
+ itemClassName,
367
+ onSelect,
368
+ saveUrl,
369
+ onSave,
370
+ onSaved
371
+ }) => {
372
+ const client = useMemo2(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
373
+ const [files, setFiles] = useState2([]);
374
+ const [loading, setLoading] = useState2(false);
375
+ const [error, setError] = useState2(null);
376
+ const [query, setQuery] = useState2("");
377
+ const [savingKey, setSavingKey] = useState2(null);
378
+ useEffect(() => {
379
+ let cancelled = false;
380
+ const run = async () => {
381
+ setLoading(true);
382
+ setError(null);
383
+ try {
384
+ const data = await client.getProjectFiles({ folderPath });
385
+ if (!cancelled) setFiles(data);
386
+ } catch (e) {
387
+ if (!cancelled) setError(e?.message || "Failed to load files");
388
+ } finally {
389
+ if (!cancelled) setLoading(false);
390
+ }
391
+ };
392
+ run();
393
+ return () => {
394
+ cancelled = true;
395
+ };
396
+ }, [client, folderPath]);
397
+ const visible = files.filter(
398
+ (f) => !query ? true : (f.originalName || "").toLowerCase().includes(query.toLowerCase())
399
+ );
400
+ return /* @__PURE__ */ jsxs2("div", { className, children: [
401
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }, children: [
402
+ /* @__PURE__ */ jsx2(
403
+ "input",
404
+ {
405
+ type: "text",
406
+ placeholder: "Search by filename...",
407
+ value: query,
408
+ onChange: (e) => setQuery(e.target.value),
409
+ style: { flex: 1, padding: 8, border: "1px solid #e5e7eb", borderRadius: 6 }
410
+ }
411
+ ),
412
+ /* @__PURE__ */ jsx2(
413
+ "button",
414
+ {
415
+ onClick: () => {
416
+ setQuery("");
417
+ (async () => {
418
+ setLoading(true);
419
+ setError(null);
420
+ try {
421
+ const data = await client.getProjectFiles({ folderPath });
422
+ setFiles(data);
423
+ } catch (e) {
424
+ setError(e?.message || "Failed to load files");
425
+ } finally {
426
+ setLoading(false);
427
+ }
428
+ })();
429
+ },
430
+ style: { padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#fff" },
431
+ children: "Refresh"
432
+ }
433
+ )
434
+ ] }),
435
+ loading && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#666" }, children: "Loading files\u2026" }),
436
+ error && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#b91c1c" }, children: error }),
437
+ !loading && !error && /* @__PURE__ */ jsxs2("ul", { className: listClassName, style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }, children: [
438
+ visible.map((f) => /* @__PURE__ */ jsxs2(
439
+ "li",
440
+ {
441
+ className: itemClassName,
442
+ style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 10, display: "flex", justifyContent: "space-between", alignItems: "center" },
443
+ children: [
444
+ /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0 }, children: [
445
+ /* @__PURE__ */ jsx2("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.originalName }),
446
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: 12, color: "#666" }, children: [
447
+ (f.size / (1024 * 1024)).toFixed(2),
448
+ " MB \u2022 ",
449
+ f.contentType
450
+ ] })
451
+ ] }),
452
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
453
+ /* @__PURE__ */ jsx2("a", { href: f.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
454
+ /* @__PURE__ */ jsx2(
455
+ "button",
456
+ {
457
+ onClick: async () => {
458
+ const selected = { name: f.originalName, key: f.key, url: f.url, size: f.size, contentType: f.contentType };
459
+ onSelect(selected);
460
+ if (typeof onSave === "function" || !!saveUrl) {
461
+ try {
462
+ setSavingKey(f.key);
463
+ if (typeof onSave === "function") {
464
+ await onSave(selected);
465
+ onSaved?.();
466
+ }
467
+ if (saveUrl) {
468
+ const res = await fetch(saveUrl, {
469
+ method: "POST",
470
+ headers: { "content-type": "application/json" },
471
+ body: JSON.stringify(selected),
472
+ credentials: "same-origin"
473
+ });
474
+ const data = await res.json().catch(() => void 0);
475
+ if (!res.ok) throw new Error(data?.error || "Failed to save");
476
+ onSaved?.(data);
477
+ }
478
+ } catch (e) {
479
+ console.error("Save selection failed", e);
480
+ alert(e?.message || "Failed to save");
481
+ } finally {
482
+ setSavingKey(null);
483
+ }
484
+ }
485
+ },
486
+ disabled: savingKey === f.key,
487
+ style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey === f.key ? 0.7 : 1 },
488
+ children: savingKey === f.key ? "Saving\u2026" : "Use"
489
+ }
490
+ )
491
+ ] })
492
+ ]
493
+ },
494
+ f.key
495
+ )),
496
+ visible.length === 0 && /* @__PURE__ */ jsx2("li", { style: { fontSize: 12, color: "#666" }, children: "No files found" })
497
+ ] })
498
+ ] });
499
+ };
235
500
  export {
501
+ ProjectFilesWidget,
236
502
  UpfilesClient,
237
503
  Uploader
238
504
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thetechfossil/upfiles",
3
- "version": "0.1.0",
4
- "description": "Lightweight client and React components to upload files to UpFiles API (presigned S3)",
3
+ "version": "0.3.0",
4
+ "description": "Lightweight client and React components for Upfiles Plugin API (presigned S3)",
5
5
  "license": "MIT",
6
6
  "author": "UpFiles",
7
7
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
30
30
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
31
31
  "typecheck": "tsc -p tsconfig.json --noEmit",
32
- "prepublishOnly": "npm run build"
32
+ "prepublishOnly": "bun run build"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": ">=18"