@autorender/sdk-core 0.1.19 → 0.1.22

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/index.d.cts CHANGED
@@ -9,6 +9,7 @@ interface UploadItem {
9
9
  progress: number;
10
10
  previewUrl?: string;
11
11
  error?: string;
12
+ info?: string;
12
13
  uploadedAt?: Date;
13
14
  response?: UploadedFileResponse;
14
15
  }
@@ -98,6 +99,7 @@ interface UploadSettings {
98
99
  pretransformations?: string;
99
100
  tags?: string[];
100
101
  is_unique_suffix_name?: boolean;
102
+ custom_id?: string;
101
103
  }
102
104
  type Environment = 'prod' | 'dev';
103
105
  type Palette = 'at-blue' | 'at-purple' | 'at-red' | 'at-orange' | 'at-green' | 'at-turquoise' | 'at-gray';
@@ -225,6 +227,7 @@ interface TransformOptions {
225
227
  ar?: string;
226
228
  p?: string;
227
229
  ps?: 'center' | 'entropy' | 'attention';
230
+ focus?: string;
228
231
  r?: number | 'portrait' | 'landscape';
229
232
  z?: number;
230
233
  flip?: 'h' | 'v' | 'hv' | 'vh';
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ interface UploadItem {
9
9
  progress: number;
10
10
  previewUrl?: string;
11
11
  error?: string;
12
+ info?: string;
12
13
  uploadedAt?: Date;
13
14
  response?: UploadedFileResponse;
14
15
  }
@@ -98,6 +99,7 @@ interface UploadSettings {
98
99
  pretransformations?: string;
99
100
  tags?: string[];
100
101
  is_unique_suffix_name?: boolean;
102
+ custom_id?: string;
101
103
  }
102
104
  type Environment = 'prod' | 'dev';
103
105
  type Palette = 'at-blue' | 'at-purple' | 'at-red' | 'at-orange' | 'at-green' | 'at-turquoise' | 'at-gray';
@@ -225,6 +227,7 @@ interface TransformOptions {
225
227
  ar?: string;
226
228
  p?: string;
227
229
  ps?: 'center' | 'entropy' | 'attention';
230
+ focus?: string;
228
231
  r?: number | 'portrait' | 'landscape';
229
232
  z?: number;
230
233
  flip?: 'h' | 'v' | 'hv' | 'vh';
package/dist/index.js CHANGED
@@ -152,119 +152,86 @@ var AutorenderApiClient = class {
152
152
  this.apiKey = opts.apiKey;
153
153
  this.baseUrl = (opts.baseUrl || getBaseUrl(opts.environment || "prod")).replace(/\/+$/, "");
154
154
  }
155
- async generateToken(params) {
155
+ uploadFile(params, onProgress, signal) {
156
156
  return new Promise((resolve, reject) => {
157
+ if (signal?.aborted) {
158
+ reject(new DOMException("Upload aborted", "AbortError"));
159
+ return;
160
+ }
157
161
  const xhr = new XMLHttpRequest();
162
+ if (signal) {
163
+ signal.addEventListener("abort", () => {
164
+ xhr.abort();
165
+ reject(new DOMException("Upload aborted", "AbortError"));
166
+ });
167
+ }
168
+ let lastProgress = 0;
169
+ xhr.upload.onprogress = (event) => {
170
+ if (event.lengthComputable && onProgress) {
171
+ const progress = Math.round(event.loaded / event.total * 100);
172
+ lastProgress = progress;
173
+ onProgress(progress);
174
+ } else if (onProgress && event.loaded > 0) {
175
+ const estimatedProgress = Math.min(
176
+ 95,
177
+ Math.round(event.loaded / (event.loaded + 1e6) * 100)
178
+ );
179
+ if (estimatedProgress > lastProgress) {
180
+ lastProgress = estimatedProgress;
181
+ onProgress(estimatedProgress);
182
+ }
183
+ }
184
+ };
158
185
  xhr.onerror = () => {
159
- reject(new Error(`Failed to generate token: ${xhr.statusText}`));
186
+ reject(new Error(`Upload failed: ${xhr.statusText}`));
160
187
  };
161
188
  xhr.onload = () => {
162
189
  try {
163
190
  if (xhr.status < 200 || xhr.status >= 300) {
164
191
  const errorText = xhr.responseText || xhr.statusText;
165
- reject(new Error(`Failed to generate token: ${errorText}`));
166
- return;
167
- }
168
- const response = JSON.parse(xhr.responseText);
169
- if (!response.token) {
170
- reject(new Error("Token not found in response"));
192
+ reject(new Error(`Upload failed: ${errorText}`));
171
193
  return;
172
194
  }
173
- resolve(response.token);
195
+ const newResponse = JSON.parse(xhr.responseText);
196
+ const response = {
197
+ success: true,
198
+ file_no: newResponse.file_no || newResponse.id,
199
+ name: newResponse.name,
200
+ url: newResponse.url,
201
+ file_size: newResponse.size,
202
+ format: newResponse.format,
203
+ width: newResponse.width,
204
+ height: newResponse.height,
205
+ created_at: newResponse.created_at,
206
+ path: newResponse.path,
207
+ workspace_no: newResponse.workspace_id,
208
+ isDuplicate: newResponse.is_duplicate || false
209
+ };
210
+ resolve(response);
174
211
  } catch (error) {
175
- reject(new Error("Failed to parse token response"));
212
+ reject(new Error("Failed to parse response"));
176
213
  }
177
214
  };
178
- let folder = params.folderPath || "";
179
- if (params.relativePath) {
180
- folder = folder ? `${folder}/${params.relativePath}` : params.relativePath;
215
+ const endpoint = `${this.baseUrl}/uploads`;
216
+ const formData = new FormData();
217
+ formData.append("file", params.file);
218
+ formData.append("file_name", params.file.name);
219
+ if (params.folderPath) {
220
+ formData.append("folder", params.folderPath);
181
221
  }
182
- if (folder && !folder.endsWith("/")) {
183
- folder += "/";
222
+ if (params.settings?.tags?.length) {
223
+ formData.append("tags", params.settings.tags.join(","));
224
+ }
225
+ if (params.settings?.pretransformations) {
226
+ formData.append("transform", params.settings.pretransformations);
227
+ }
228
+ if (params.settings?.custom_id) {
229
+ formData.append("custom_id", params.settings.custom_id);
184
230
  }
185
- const tokenRequest = {
186
- ...folder && { folder },
187
- ...params.settings?.tags && { tags: params.settings.tags },
188
- ...params.settings?.pretransformations && { transform: params.settings.pretransformations },
189
- max_file_size: params.file.size,
190
- allow_override: {
191
- folder: true,
192
- tags: true,
193
- transform: true
194
- },
195
- random_prefix: !params.settings?.is_unique_suffix_name
196
- };
197
- const endpoint = `${this.baseUrl}/generate-token`;
198
231
  xhr.open("POST", endpoint);
199
232
  xhr.setRequestHeader("Authorization", `Bearer ${this.apiKey}`);
200
- xhr.setRequestHeader("Content-Type", "application/json");
201
233
  xhr.setRequestHeader("accept", "application/json");
202
- xhr.send(JSON.stringify(tokenRequest));
203
- });
204
- }
205
- uploadFile(params, onProgress, signal) {
206
- return new Promise(async (resolve, reject) => {
207
- if (signal?.aborted) {
208
- reject(new DOMException("Upload aborted", "AbortError"));
209
- return;
210
- }
211
- try {
212
- const token = await this.generateToken(params);
213
- if (signal?.aborted) {
214
- reject(new DOMException("Upload aborted", "AbortError"));
215
- return;
216
- }
217
- const xhr = new XMLHttpRequest();
218
- if (signal) {
219
- signal.addEventListener("abort", () => {
220
- xhr.abort();
221
- reject(new DOMException("Upload aborted", "AbortError"));
222
- });
223
- }
224
- xhr.upload.onprogress = (event) => {
225
- if (event.lengthComputable && onProgress) {
226
- const progress = Math.round(event.loaded / event.total * 100);
227
- onProgress(progress);
228
- }
229
- };
230
- xhr.onerror = () => {
231
- reject(new Error(`Upload failed: ${xhr.statusText}`));
232
- };
233
- xhr.onload = () => {
234
- try {
235
- if (xhr.status < 200 || xhr.status >= 300) {
236
- const errorText = xhr.responseText || xhr.statusText;
237
- reject(new Error(`Upload failed: ${errorText}`));
238
- return;
239
- }
240
- const newResponse = JSON.parse(xhr.responseText);
241
- const response = {
242
- success: true,
243
- file_no: newResponse.file_no || newResponse.id,
244
- name: newResponse.name,
245
- url: newResponse.url,
246
- file_size: newResponse.size,
247
- format: newResponse.format,
248
- width: newResponse.width,
249
- height: newResponse.height,
250
- created_at: newResponse.created_at,
251
- path: newResponse.path,
252
- workspace_no: newResponse.workspace_id,
253
- isDuplicate: newResponse.is_duplicate || false
254
- };
255
- resolve(response);
256
- } catch (error) {
257
- reject(new Error("Failed to parse response"));
258
- }
259
- };
260
- const endpoint = `${this.baseUrl}/uploads/${token}`;
261
- xhr.open("POST", endpoint);
262
- xhr.setRequestHeader("accept", "application/json");
263
- xhr.setRequestHeader("Content-Type", params.file.type || "image/*");
264
- xhr.send(params.file);
265
- } catch (error) {
266
- reject(error);
267
- }
234
+ xhr.send(formData);
268
235
  });
269
236
  }
270
237
  };
@@ -378,7 +345,9 @@ function buildAssetDeliveryUrl(baseUrl, workspace, path, fallback) {
378
345
  }
379
346
 
380
347
  // src/core/uploader-controller.ts
348
+ import { fileTypeFromBlob } from "file-type";
381
349
  var DEFAULT_PARALLEL_UPLOADS = 4;
350
+ var MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
382
351
  var UploaderController = class extends EventTarget {
383
352
  constructor(client, target, options) {
384
353
  super();
@@ -417,19 +386,44 @@ var UploaderController = class extends EventTarget {
417
386
  duplicates: Array.from(this.duplicateConflicts)
418
387
  };
419
388
  }
420
- addFiles(files) {
389
+ async addFiles(files) {
421
390
  const existingKeys = /* @__PURE__ */ new Map();
422
391
  const basePathKey = this.normalizePath(this.options.folderPath) ?? "";
423
392
  const buildKey = (file, folderPath, relativePath) => `${basePathKey}::${folderPath ?? ""}::${relativePath ?? ""}::${file.name}::${file.size}`;
424
393
  this.items.forEach((item) => {
425
394
  existingKeys.set(buildKey(item.file, item.folderPath, item.relativePath), item);
426
395
  });
427
- const newItems = files.map(({ file, relativePath }) => {
396
+ const filesWithMimeType = await Promise.all(
397
+ files.map(async ({ file, relativePath }) => {
398
+ let mimeType = file.type;
399
+ if (!mimeType || mimeType === "application/octet-stream" || mimeType === "") {
400
+ try {
401
+ const detected = await fileTypeFromBlob(file);
402
+ if (detected?.mime) {
403
+ mimeType = detected.mime;
404
+ Object.defineProperty(file, "type", {
405
+ value: mimeType,
406
+ writable: false,
407
+ configurable: true
408
+ });
409
+ } else {
410
+ mimeType = "application/octet-stream";
411
+ }
412
+ } catch (error) {
413
+ mimeType = "application/octet-stream";
414
+ }
415
+ }
416
+ return { file, relativePath, mimeType };
417
+ })
418
+ );
419
+ const newItems = filesWithMimeType.map(({ file, relativePath, mimeType }) => {
428
420
  const folderPath = getRelativeFolderPath(
429
421
  this.options.folderPath,
430
422
  relativePath
431
423
  ) ?? "";
432
- const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
424
+ const previewUrl = mimeType.startsWith("image/") ? URL.createObjectURL(file) : void 0;
425
+ const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
426
+ const info = file.size > MAX_FILE_SIZE_BYTES ? `File size (${fileSizeMB}MB) exceeds the maximum limit of 100MB` : void 0;
433
427
  const baseItem = {
434
428
  id: generateId(),
435
429
  file,
@@ -437,7 +431,8 @@ var UploaderController = class extends EventTarget {
437
431
  folderPath,
438
432
  status: "pending",
439
433
  progress: 0,
440
- previewUrl
434
+ previewUrl,
435
+ info
441
436
  };
442
437
  const key = buildKey(file, folderPath, relativePath);
443
438
  if (existingKeys.has(key)) {
@@ -620,6 +615,13 @@ var UploaderController = class extends EventTarget {
620
615
  await this.runWithConcurrency(tasks, parallel);
621
616
  }
622
617
  async uploadSingleFile(item, uploadSettings) {
618
+ if (item.file.size > MAX_FILE_SIZE_BYTES) {
619
+ const fileSizeMB = (item.file.size / (1024 * 1024)).toFixed(2);
620
+ item.info = `File size (${fileSizeMB}MB) exceeds the maximum limit of 100MB`;
621
+ this.dispatch("fileprogress", { item });
622
+ this.dispatch("statechange");
623
+ return;
624
+ }
623
625
  item.status = "uploading";
624
626
  item.progress = 0;
625
627
  this.dispatch("statechange");
@@ -632,7 +634,8 @@ var UploaderController = class extends EventTarget {
632
634
  settings: uploadSettings ? {
633
635
  pretransformations: uploadSettings.pretransformations,
634
636
  tags: uploadSettings.tags,
635
- is_unique_suffix_name: uploadSettings.is_unique_suffix_name ?? false
637
+ is_unique_suffix_name: uploadSettings.is_unique_suffix_name ?? false,
638
+ custom_id: uploadSettings.custom_id
636
639
  } : void 0
637
640
  },
638
641
  (progress) => {
@@ -1132,7 +1135,8 @@ var widgetStyles = (
1132
1135
  gap: 0.45rem;
1133
1136
  position: relative;
1134
1137
  box-sizing: border-box;
1135
- height: 50px;
1138
+ min-height: 50px;
1139
+ overflow: hidden;
1136
1140
  }
1137
1141
 
1138
1142
  .ar-file-item.is-grid {
@@ -1149,6 +1153,8 @@ var widgetStyles = (
1149
1153
  display: flex;
1150
1154
  align-items: center;
1151
1155
  gap: 0.6rem;
1156
+ min-width: 0;
1157
+ flex: 1;
1152
1158
  }
1153
1159
 
1154
1160
  .ar-file-header.is-grid {
@@ -1171,6 +1177,17 @@ var widgetStyles = (
1171
1177
  position: relative;
1172
1178
  }
1173
1179
 
1180
+ .ar-file-thumb-document {
1181
+ background: rgba(148, 163, 184, 0.15);
1182
+ color: var(--ar-color-muted-foreground);
1183
+ }
1184
+
1185
+ .ar-file-thumb-document svg {
1186
+ width: 20px;
1187
+ height: 20px;
1188
+ color: currentColor;
1189
+ }
1190
+
1174
1191
  .ar-file-thumb.is-grid {
1175
1192
  width: 100%;
1176
1193
  height: 140px;
@@ -1178,6 +1195,11 @@ var widgetStyles = (
1178
1195
  font-size: 1rem;
1179
1196
  }
1180
1197
 
1198
+ .ar-file-thumb.is-grid.ar-file-thumb-document svg {
1199
+ width: 48px;
1200
+ height: 48px;
1201
+ }
1202
+
1181
1203
  .ar-file-item.is-completed .ar-file-thumb {
1182
1204
  color: #fff;
1183
1205
  }
@@ -1212,13 +1234,39 @@ var widgetStyles = (
1212
1234
  font-size: 0.8rem;
1213
1235
  margin: 0;
1214
1236
  color: var(--ar-color-text);
1237
+ overflow: hidden;
1238
+ text-overflow: ellipsis;
1239
+ white-space: nowrap;
1240
+ flex: 1;
1241
+ min-width: 0;
1215
1242
  }
1216
1243
 
1217
1244
  .ar-file-meta {
1218
- display: flex;
1219
- gap: 0.75rem;
1245
+ display: block;
1246
+ margin-top: 0.25rem;
1220
1247
  font-size: 0.8rem;
1221
1248
  color: var(--ar-color-muted-foreground);
1249
+ width: 100%;
1250
+ flex-shrink: 0;
1251
+ }
1252
+
1253
+ .ar-file-info {
1254
+ display: block;
1255
+ font-size: 0.75rem;
1256
+ color: var(--ar-color-muted-foreground);
1257
+ font-style: italic;
1258
+ padding: 0.25rem 0.5rem;
1259
+ background: rgba(148, 163, 184, 0.15);
1260
+ border-radius: calc(var(--ar-radius) * 0.5);
1261
+ width: 100%;
1262
+ box-sizing: border-box;
1263
+ word-wrap: break-word;
1264
+ overflow-wrap: break-word;
1265
+ }
1266
+
1267
+ .ar-file-item.is-grid .ar-file-info {
1268
+ font-size: 0.7rem;
1269
+ padding: 0.2rem 0.4rem;
1222
1270
  }
1223
1271
 
1224
1272
  .ar-file-item.is-grid .ar-file-meta {
@@ -2044,7 +2092,6 @@ var AutorenderUploaderElement = class extends HTMLElementShim {
2044
2092
  const themeInput = options.theme ?? previousOptions.theme ?? DEFAULT_THEME;
2045
2093
  const resolvedTheme = {
2046
2094
  appearance: themeInput.appearance ?? DEFAULT_THEME.appearance,
2047
- accentColor: themeInput.accentColor ?? DEFAULT_THEME.accentColor,
2048
2095
  borderRadius: themeInput.borderRadius ?? DEFAULT_THEME.borderRadius,
2049
2096
  dropzoneBackground: themeInput.dropzoneBackground ?? DEFAULT_THEME.dropzoneBackground,
2050
2097
  dropzoneBorder: themeInput.dropzoneBorder ?? DEFAULT_THEME.dropzoneBorder,
@@ -2420,19 +2467,19 @@ var AutorenderUploaderElement = class extends HTMLElementShim {
2420
2467
  this.dropzone?.classList.remove("is-dragging");
2421
2468
  const files = await this.extractFilesFromEvent(event);
2422
2469
  if (files.length) {
2423
- this.controller?.addFiles(files);
2470
+ await this.controller?.addFiles(files);
2424
2471
  }
2425
2472
  if (this.fileInput) {
2426
2473
  this.fileInput.value = "";
2427
2474
  }
2428
2475
  });
2429
- this.fileInput?.addEventListener("change", () => {
2476
+ this.fileInput?.addEventListener("change", async () => {
2430
2477
  const files = this.fileInput?.files ? Array.from(this.fileInput.files) : [];
2431
2478
  const mapped = files.map((file) => ({
2432
2479
  file,
2433
2480
  relativePath: file.webkitRelativePath
2434
2481
  }));
2435
- this.controller?.addFiles(mapped);
2482
+ await this.controller?.addFiles(mapped);
2436
2483
  if (this.fileInput) this.fileInput.value = "";
2437
2484
  });
2438
2485
  this.uploadButton?.addEventListener("click", () => {
@@ -2626,13 +2673,35 @@ var AutorenderUploaderElement = class extends HTMLElementShim {
2626
2673
  const thumb = document.createElement("div");
2627
2674
  thumb.className = "ar-file-thumb";
2628
2675
  thumb.classList.toggle("is-grid", isGridLayout);
2629
- if (item.previewUrl) {
2676
+ const isImage = item.file.type.startsWith("image/");
2677
+ if (item.previewUrl && isImage) {
2630
2678
  const img = document.createElement("img");
2631
2679
  img.src = item.previewUrl;
2632
2680
  img.alt = item.file.name;
2633
2681
  thumb.appendChild(img);
2634
2682
  } else {
2635
- thumb.textContent = item.file.name.charAt(0).toUpperCase();
2683
+ thumb.classList.add("ar-file-thumb-document");
2684
+ const documentIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2685
+ documentIcon.setAttribute("viewBox", "0 0 24 24");
2686
+ documentIcon.setAttribute("width", "24");
2687
+ documentIcon.setAttribute("height", "24");
2688
+ documentIcon.setAttribute("fill", "none");
2689
+ documentIcon.setAttribute("aria-hidden", "true");
2690
+ const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2691
+ path1.setAttribute("d", "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6Z");
2692
+ path1.setAttribute("stroke", "currentColor");
2693
+ path1.setAttribute("stroke-width", "1.5");
2694
+ path1.setAttribute("stroke-linecap", "round");
2695
+ path1.setAttribute("stroke-linejoin", "round");
2696
+ const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2697
+ path2.setAttribute("d", "M14 2v6h6");
2698
+ path2.setAttribute("stroke", "currentColor");
2699
+ path2.setAttribute("stroke-width", "1.5");
2700
+ path2.setAttribute("stroke-linecap", "round");
2701
+ path2.setAttribute("stroke-linejoin", "round");
2702
+ documentIcon.appendChild(path1);
2703
+ documentIcon.appendChild(path2);
2704
+ thumb.appendChild(documentIcon);
2636
2705
  }
2637
2706
  if (item.status === "completed") {
2638
2707
  const statusOverlay = document.createElement("div");
@@ -2679,6 +2748,15 @@ var AutorenderUploaderElement = class extends HTMLElementShim {
2679
2748
  header.appendChild(name);
2680
2749
  header.appendChild(removeButton);
2681
2750
  li.appendChild(header);
2751
+ if (item.info) {
2752
+ const metaRow = document.createElement("div");
2753
+ metaRow.className = "ar-file-meta";
2754
+ const infoText = document.createElement("span");
2755
+ infoText.className = "ar-file-info";
2756
+ infoText.textContent = item.info;
2757
+ metaRow.appendChild(infoText);
2758
+ li.appendChild(metaRow);
2759
+ }
2682
2760
  const progressBorder = document.createElement("div");
2683
2761
  progressBorder.className = "ar-progress-border";
2684
2762
  const progressFill = document.createElement("div");
@@ -2689,8 +2767,8 @@ var AutorenderUploaderElement = class extends HTMLElementShim {
2689
2767
  const rawProgress = Math.max(0, Math.min(100, item.progress ?? 0));
2690
2768
  const calculatedTrain = !isCompleteStatus && rawProgress < 20;
2691
2769
  const shouldAnimateTrain = (isUploadingStatus || isUploading && isPendingStatus) && calculatedTrain;
2692
- const isActive = shouldAnimateTrain || rawProgress > 0 && !isCompleteStatus;
2693
- const stateLabel = isCompleteStatus ? "complete" : shouldAnimateTrain ? "train" : rawProgress >= 20 ? "progress" : "idle";
2770
+ const isActive = isUploadingStatus || shouldAnimateTrain || rawProgress > 0 && !isCompleteStatus;
2771
+ const stateLabel = isCompleteStatus ? "complete" : shouldAnimateTrain ? "train" : rawProgress >= 20 ? "progress" : isUploadingStatus ? "progress" : "idle";
2694
2772
  progressBorder.dataset.state = stateLabel;
2695
2773
  progressBorder.dataset.status = item.status;
2696
2774
  progressBorder.dataset.progress = rawProgress.toFixed(0);
@@ -2704,17 +2782,14 @@ var AutorenderUploaderElement = class extends HTMLElementShim {
2704
2782
  progressFill.style.width = "100%";
2705
2783
  } else if (shouldAnimateTrain) {
2706
2784
  progressFill.style.width = "100%";
2785
+ } else if (isUploadingStatus && rawProgress === 0) {
2786
+ progressFill.style.width = "3%";
2707
2787
  } else {
2708
2788
  const computedWidth = Math.max(rawProgress, 3);
2709
2789
  progressFill.style.width = `${Math.min(computedWidth, 100)}%`;
2710
2790
  }
2711
2791
  progressBorder.appendChild(progressFill);
2712
- const metaRow = document.createElement("div");
2713
- metaRow.className = "ar-file-meta";
2714
2792
  li.appendChild(progressBorder);
2715
- if (metaRow.childNodes.length > 0) {
2716
- li.appendChild(metaRow);
2717
- }
2718
2793
  this.fileList?.appendChild(li);
2719
2794
  });
2720
2795
  }
@@ -3023,6 +3098,8 @@ function buildTransformString(transform, defaults, enableDPR = true, connectionQ
3023
3098
  sortKey = "f_" + param;
3024
3099
  } else if (param.startsWith("fl_")) {
3025
3100
  sortKey = "fl_" + param;
3101
+ } else if (param.startsWith("fo_")) {
3102
+ sortKey = "fo_" + param;
3026
3103
  } else if (param.startsWith("flip_")) {
3027
3104
  sortKey = "flip_" + param;
3028
3105
  } else if (param.startsWith("h_")) {
@@ -3102,6 +3179,7 @@ function buildTransformString(transform, defaults, enableDPR = true, connectionQ
3102
3179
  if (transform.ar) addPart("ar_", transform.ar);
3103
3180
  if (transform.p) addPart("p_", transform.p);
3104
3181
  if (transform.ps) addPart("ps_", transform.ps);
3182
+ if (transform.focus) addPart("fo_", transform.focus);
3105
3183
  if (transform.r !== void 0) addPart("r_", transform.r);
3106
3184
  if (transform.z !== void 0) addPart("z_", transform.z);
3107
3185
  if (transform.flip) addPart("flip_", transform.flip);
@@ -3202,6 +3280,7 @@ function buildTransformString(transform, defaults, enableDPR = true, connectionQ
3202
3280
  "ar",
3203
3281
  "p",
3204
3282
  "ps",
3283
+ "focus",
3205
3284
  "r",
3206
3285
  "z",
3207
3286
  "flip",