camox 0.22.0 → 0.24.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.
@@ -1,107 +1,154 @@
1
1
  import { cn } from "../../../lib/utils.js";
2
+ import { transformImageUrl } from "../../../core/lib/imageTransform.js";
2
3
  import { c } from "react/compiler-runtime";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  import { FileIcon } from "lucide-react";
5
6
 
6
7
  //#region src/features/content/components/AssetCard.tsx
8
+ const OPAQUE_IMAGE_MIME_TYPES = new Set(["image/jpeg", "image/jpg"]);
7
9
  const AssetCard = (t0) => {
8
- const $ = c(26);
9
- if ($[0] !== "62dab0d9b73ffb903a48127d8b52340688d238c98cfa8a817c9ae69dc68148ec") {
10
- for (let $i = 0; $i < 26; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
11
- $[0] = "62dab0d9b73ffb903a48127d8b52340688d238c98cfa8a817c9ae69dc68148ec";
10
+ const $ = c(46);
11
+ if ($[0] !== "1047e431261024f9506ec8f4989ecb00b2808af70364054688c5f3d5eb9e1f28") {
12
+ for (let $i = 0; $i < 46; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
13
+ $[0] = "1047e431261024f9506ec8f4989ecb00b2808af70364054688c5f3d5eb9e1f28";
12
14
  }
13
15
  const { file, selected, onSelect, onOpen } = t0;
16
+ let extension;
17
+ let isImage;
18
+ let isOpaqueImage;
14
19
  let t1;
15
- if ($[1] !== file.mimeType) {
16
- t1 = file.mimeType?.startsWith("image/");
17
- $[1] = file.mimeType;
18
- $[2] = t1;
19
- } else t1 = $[2];
20
- const isImage = t1;
21
20
  let t2;
22
- if ($[3] !== file.filename) {
23
- t2 = file.filename?.split(".").pop()?.toUpperCase() ?? "";
24
- $[3] = file.filename;
25
- $[4] = t2;
26
- } else t2 = $[4];
27
- const extension = t2;
28
- const t3 = file.id;
29
- const t4 = selected ? "bg-primary/20 border-2 border-primary" : "hover:bg-accent/75";
21
+ let t3;
22
+ let t4;
30
23
  let t5;
31
- if ($[5] !== t4) {
32
- t5 = cn("group flex flex-col gap-1.5 rounded-lg p-2 text-left border-2 border-transparent", t4);
33
- $[5] = t4;
34
- $[6] = t5;
35
- } else t5 = $[6];
36
24
  let t6;
37
- if ($[7] !== onSelect) {
38
- t6 = (e) => {
39
- e.stopPropagation();
40
- onSelect();
41
- };
42
- $[7] = onSelect;
43
- $[8] = t6;
44
- } else t6 = $[8];
25
+ if ($[1] !== file.filename || $[2] !== file.id || $[3] !== file.mimeType || $[4] !== onOpen || $[5] !== onSelect || $[6] !== selected) {
26
+ isImage = file.mimeType?.startsWith("image/");
27
+ isOpaqueImage = isImage && OPAQUE_IMAGE_MIME_TYPES.has(file.mimeType ?? "");
28
+ let t7;
29
+ if ($[16] !== file.filename) {
30
+ t7 = file.filename?.split(".").pop()?.toUpperCase() ?? "";
31
+ $[16] = file.filename;
32
+ $[17] = t7;
33
+ } else t7 = $[17];
34
+ extension = t7;
35
+ t2 = "button";
36
+ t3 = file.id;
37
+ const t8 = selected ? "bg-primary/20 border-2 border-primary" : "hover:bg-accent/75";
38
+ if ($[18] !== t8) {
39
+ t4 = cn("group flex flex-col gap-1.5 rounded-lg p-2 text-left border-2 border-transparent", t8);
40
+ $[18] = t8;
41
+ $[19] = t4;
42
+ } else t4 = $[19];
43
+ if ($[20] !== onSelect) {
44
+ t5 = (e) => {
45
+ e.stopPropagation();
46
+ onSelect();
47
+ };
48
+ $[20] = onSelect;
49
+ $[21] = t5;
50
+ } else t5 = $[21];
51
+ if ($[22] !== onOpen) {
52
+ t6 = (e_0) => {
53
+ e_0.stopPropagation();
54
+ onOpen();
55
+ };
56
+ $[22] = onOpen;
57
+ $[23] = t6;
58
+ } else t6 = $[23];
59
+ t1 = cn("flex aspect-4/3 w-full items-center justify-center overflow-hidden rounded-sm", isOpaqueImage && "bg-muted", !isImage && "bg-muted", isImage && !isOpaqueImage && "checkered p-1.5");
60
+ $[1] = file.filename;
61
+ $[2] = file.id;
62
+ $[3] = file.mimeType;
63
+ $[4] = onOpen;
64
+ $[5] = onSelect;
65
+ $[6] = selected;
66
+ $[7] = extension;
67
+ $[8] = isImage;
68
+ $[9] = isOpaqueImage;
69
+ $[10] = t1;
70
+ $[11] = t2;
71
+ $[12] = t3;
72
+ $[13] = t4;
73
+ $[14] = t5;
74
+ $[15] = t6;
75
+ } else {
76
+ extension = $[7];
77
+ isImage = $[8];
78
+ isOpaqueImage = $[9];
79
+ t1 = $[10];
80
+ t2 = $[11];
81
+ t3 = $[12];
82
+ t4 = $[13];
83
+ t5 = $[14];
84
+ t6 = $[15];
85
+ }
45
86
  let t7;
46
- if ($[9] !== onOpen) {
47
- t7 = (e_0) => {
48
- e_0.stopPropagation();
49
- onOpen();
50
- };
51
- $[9] = onOpen;
52
- $[10] = t7;
53
- } else t7 = $[10];
87
+ if ($[24] !== extension || $[25] !== file.alt || $[26] !== file.filename || $[27] !== file.mimeType || $[28] !== file.size || $[29] !== file.url || $[30] !== isImage || $[31] !== isOpaqueImage) {
88
+ t7 = isImage ? /* @__PURE__ */ jsx("img", {
89
+ src: transformImageUrl(file.url, {
90
+ width: 480,
91
+ mimeType: file.mimeType,
92
+ size: file.size
93
+ }),
94
+ alt: file.alt || file.filename,
95
+ draggable: false,
96
+ className: cn("pointer-events-none h-full w-full", isOpaqueImage ? "object-cover" : "object-contain")
97
+ }) : /* @__PURE__ */ jsxs("div", {
98
+ className: "text-muted-foreground flex flex-col items-center gap-1",
99
+ children: [/* @__PURE__ */ jsx(FileIcon, { className: "h-8 w-8" }), extension && /* @__PURE__ */ jsx("span", {
100
+ className: "text-sm font-medium",
101
+ children: extension
102
+ })]
103
+ });
104
+ $[24] = extension;
105
+ $[25] = file.alt;
106
+ $[26] = file.filename;
107
+ $[27] = file.mimeType;
108
+ $[28] = file.size;
109
+ $[29] = file.url;
110
+ $[30] = isImage;
111
+ $[31] = isOpaqueImage;
112
+ $[32] = t7;
113
+ } else t7 = $[32];
54
114
  let t8;
55
- if ($[11] !== extension || $[12] !== file.alt || $[13] !== file.filename || $[14] !== file.url || $[15] !== isImage) {
115
+ if ($[33] !== t1 || $[34] !== t7) {
56
116
  t8 = /* @__PURE__ */ jsx("div", {
57
- className: "bg-muted flex aspect-4/3 w-full items-center justify-center overflow-hidden rounded-sm",
58
- children: isImage ? /* @__PURE__ */ jsx("img", {
59
- src: file.url,
60
- alt: file.alt || file.filename,
61
- draggable: false,
62
- className: "pointer-events-none h-full w-full object-cover"
63
- }) : /* @__PURE__ */ jsxs("div", {
64
- className: "text-muted-foreground flex flex-col items-center gap-1",
65
- children: [/* @__PURE__ */ jsx(FileIcon, { className: "h-8 w-8" }), extension && /* @__PURE__ */ jsx("span", {
66
- className: "text-sm font-medium",
67
- children: extension
68
- })]
69
- })
117
+ className: t1,
118
+ children: t7
70
119
  });
71
- $[11] = extension;
72
- $[12] = file.alt;
73
- $[13] = file.filename;
74
- $[14] = file.url;
75
- $[15] = isImage;
76
- $[16] = t8;
77
- } else t8 = $[16];
120
+ $[33] = t1;
121
+ $[34] = t7;
122
+ $[35] = t8;
123
+ } else t8 = $[35];
78
124
  let t9;
79
- if ($[17] !== file.filename) {
125
+ if ($[36] !== file.filename) {
80
126
  t9 = /* @__PURE__ */ jsx("p", {
81
127
  className: "line-clamp-2 px-0.5 text-xs break-all",
82
128
  children: file.filename
83
129
  });
84
- $[17] = file.filename;
85
- $[18] = t9;
86
- } else t9 = $[18];
130
+ $[36] = file.filename;
131
+ $[37] = t9;
132
+ } else t9 = $[37];
87
133
  let t10;
88
- if ($[19] !== file.id || $[20] !== t5 || $[21] !== t6 || $[22] !== t7 || $[23] !== t8 || $[24] !== t9) {
134
+ if ($[38] !== t2 || $[39] !== t3 || $[40] !== t4 || $[41] !== t5 || $[42] !== t6 || $[43] !== t8 || $[44] !== t9) {
89
135
  t10 = /* @__PURE__ */ jsxs("button", {
90
- type: "button",
136
+ type: t2,
91
137
  "data-asset-id": t3,
92
- className: t5,
93
- onClick: t6,
94
- onDoubleClick: t7,
138
+ className: t4,
139
+ onClick: t5,
140
+ onDoubleClick: t6,
95
141
  children: [t8, t9]
96
142
  });
97
- $[19] = file.id;
98
- $[20] = t5;
99
- $[21] = t6;
100
- $[22] = t7;
101
- $[23] = t8;
102
- $[24] = t9;
103
- $[25] = t10;
104
- } else t10 = $[25];
143
+ $[38] = t2;
144
+ $[39] = t3;
145
+ $[40] = t4;
146
+ $[41] = t5;
147
+ $[42] = t6;
148
+ $[43] = t8;
149
+ $[44] = t9;
150
+ $[45] = t10;
151
+ } else t10 = $[45];
105
152
  return t10;
106
153
  };
107
154
 
@@ -15,6 +15,8 @@ declare function usePreviewedPage(): {
15
15
  metaTitle: string | null;
16
16
  metaDescription: string | null;
17
17
  aiSeoEnabled: boolean | null;
18
+ customOgImageBlobId: string | null;
19
+ customOgImageUrl: string | null;
18
20
  createdAt: number;
19
21
  updatedAt: number;
20
22
  };
@@ -1,5 +1,6 @@
1
1
  import { useProjectSlug } from "../../../lib/auth.js";
2
2
  import { projectQueries } from "../../../lib/queries.js";
3
+ import { transformImageUrl } from "../../../core/lib/imageTransform.js";
3
4
  import { UploadDropZone } from "../../content/components/UploadDropZone.js";
4
5
  import { UploadItemRow } from "../../content/components/UploadProgressDrawer.js";
5
6
  import { useFileUpload } from "../../../hooks/use-file-upload.js";
@@ -20,9 +21,9 @@ function assetLabel(isImage, multiple) {
20
21
  }
21
22
  const AssetActionButtons = (t0) => {
22
23
  const $ = c(27);
23
- if ($[0] !== "511bbc6572b1a9ed249b95be0b61223953390bf5d43cd01f6b380f115b1aadaa") {
24
+ if ($[0] !== "578c810b03b0bcf84595cb011652bdf58260b853f4d229ef94d55109880eacf0") {
24
25
  for (let $i = 0; $i < 27; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
25
- $[0] = "511bbc6572b1a9ed249b95be0b61223953390bf5d43cd01f6b380f115b1aadaa";
26
+ $[0] = "578c810b03b0bcf84595cb011652bdf58260b853f4d229ef94d55109880eacf0";
26
27
  }
27
28
  const { isImage, multiple, fileInputRef, onPickerOpen, onFilesSelected, uploads } = t0;
28
29
  let t1;
@@ -125,9 +126,9 @@ const AssetActionButtons = (t0) => {
125
126
  };
126
127
  const SingleAssetFieldEditor = (t0) => {
127
128
  const $ = c(29);
128
- if ($[0] !== "511bbc6572b1a9ed249b95be0b61223953390bf5d43cd01f6b380f115b1aadaa") {
129
+ if ($[0] !== "578c810b03b0bcf84595cb011652bdf58260b853f4d229ef94d55109880eacf0") {
129
130
  for (let $i = 0; $i < 29; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
130
- $[0] = "511bbc6572b1a9ed249b95be0b61223953390bf5d43cd01f6b380f115b1aadaa";
131
+ $[0] = "578c810b03b0bcf84595cb011652bdf58260b853f4d229ef94d55109880eacf0";
131
132
  }
132
133
  const { fieldName, assetType, currentData, onFieldChange } = t0;
133
134
  const asset = currentData[fieldName];
@@ -207,7 +208,11 @@ const SingleAssetFieldEditor = (t0) => {
207
208
  children: [isImage ? /* @__PURE__ */ jsx("div", {
208
209
  className: "border-border h-10 w-10 shrink-0 overflow-hidden rounded border",
209
210
  children: /* @__PURE__ */ jsx("img", {
210
- src: asset.url,
211
+ src: transformImageUrl(asset.url, {
212
+ width: 128,
213
+ mimeType: asset.mimeType,
214
+ size: asset.size
215
+ }),
211
216
  alt: asset.alt || asset.filename,
212
217
  className: "h-full w-full object-cover"
213
218
  })
@@ -2,17 +2,18 @@ import { trackClientEvent } from "../../../lib/telemetry-client.js";
2
2
  import { getAuthCookieHeader } from "../../../lib/auth.js";
3
3
  import { getApiUrl, getEnvironmentName } from "../../../lib/api-client.js";
4
4
  import { fileMutations, fileQueries } from "../../../lib/queries.js";
5
- import { DebouncedFieldEditor } from "./DebouncedFieldEditor.js";
5
+ import { isRasterImage, transformImageUrl } from "../../../core/lib/imageTransform.js";
6
6
  import { UploadDropZone } from "../../content/components/UploadDropZone.js";
7
+ import { DebouncedFieldEditor } from "./DebouncedFieldEditor.js";
7
8
  import { c } from "react/compiler-runtime";
8
9
  import { Label } from "@camox/ui/label";
9
10
  import { toast } from "@camox/ui/toaster";
10
11
  import { useMutation, useQuery } from "@tanstack/react-query";
11
12
  import { useCallback, useEffect, useRef, useState } from "react";
12
- import { jsx, jsxs } from "react/jsx-runtime";
13
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
14
  import { Button } from "@camox/ui/button";
14
15
  import { Tooltip, TooltipContent, TooltipTrigger } from "@camox/ui/tooltip";
15
- import { Check, Download, FileIcon, Link, Loader2, Trash2, X } from "lucide-react";
16
+ import { Check, Download, FileIcon, Info, Link, Loader2, Trash2, X } from "lucide-react";
16
17
  import { Dialog, DialogContent, DialogTitle } from "@camox/ui/dialog";
17
18
  import { Switch } from "@camox/ui/switch";
18
19
  import { ButtonGroup } from "@camox/ui/button-group";
@@ -20,9 +21,9 @@ import { ButtonGroup } from "@camox/ui/button-group";
20
21
  //#region src/features/preview/components/AssetLightbox.tsx
21
22
  function MetadataRow(t0) {
22
23
  const $ = c(9);
23
- if ($[0] !== "37940925d91368c87c0e95d97da969b420d7241705fd90761ebca29def086c1e") {
24
+ if ($[0] !== "5a5b1edf9c8dcefe67d31c9a59c95831dcc6dfede24df286efbdba62d1fd4e8d") {
24
25
  for (let $i = 0; $i < 9; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
25
- $[0] = "37940925d91368c87c0e95d97da969b420d7241705fd90761ebca29def086c1e";
26
+ $[0] = "5a5b1edf9c8dcefe67d31c9a59c95831dcc6dfede24df286efbdba62d1fd4e8d";
26
27
  }
27
28
  const { label, children } = t0;
28
29
  let t1;
@@ -64,6 +65,116 @@ function MetadataRow(t0) {
64
65
  } else t4 = $[8];
65
66
  return t4;
66
67
  }
68
+ function DeliveredSize(t0) {
69
+ const $ = c(16);
70
+ if ($[0] !== "5a5b1edf9c8dcefe67d31c9a59c95831dcc6dfede24df286efbdba62d1fd4e8d") {
71
+ for (let $i = 0; $i < 16; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
72
+ $[0] = "5a5b1edf9c8dcefe67d31c9a59c95831dcc6dfede24df286efbdba62d1fd4e8d";
73
+ }
74
+ const { bytes, raw } = t0;
75
+ if (bytes == null) {
76
+ let t1;
77
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
78
+ t1 = /* @__PURE__ */ jsx(Fragment, { children: "…" });
79
+ $[1] = t1;
80
+ } else t1 = $[1];
81
+ return t1;
82
+ }
83
+ let t1;
84
+ if ($[2] !== bytes || $[3] !== raw) {
85
+ t1 = raw != null && raw > 0 ? Math.round((raw - bytes) / raw * 100) : null;
86
+ $[2] = bytes;
87
+ $[3] = raw;
88
+ $[4] = t1;
89
+ } else t1 = $[4];
90
+ const savingsPct = t1;
91
+ if (savingsPct == null || savingsPct <= 0) {
92
+ let t2;
93
+ if ($[5] !== bytes) {
94
+ t2 = formatFileSize(bytes);
95
+ $[5] = bytes;
96
+ $[6] = t2;
97
+ } else t2 = $[6];
98
+ let t3;
99
+ if ($[7] !== t2) {
100
+ t3 = /* @__PURE__ */ jsxs(Fragment, { children: ["≈", t2] });
101
+ $[7] = t2;
102
+ $[8] = t3;
103
+ } else t3 = $[8];
104
+ return t3;
105
+ }
106
+ let t2;
107
+ if ($[9] !== bytes) {
108
+ t2 = formatFileSize(bytes);
109
+ $[9] = bytes;
110
+ $[10] = t2;
111
+ } else t2 = $[10];
112
+ let t3;
113
+ if ($[11] !== savingsPct) {
114
+ t3 = /* @__PURE__ */ jsxs("span", {
115
+ className: "text-muted-foreground",
116
+ children: [
117
+ "(−",
118
+ savingsPct,
119
+ "%)"
120
+ ]
121
+ });
122
+ $[11] = savingsPct;
123
+ $[12] = t3;
124
+ } else t3 = $[12];
125
+ let t4;
126
+ if ($[13] !== t2 || $[14] !== t3) {
127
+ t4 = /* @__PURE__ */ jsxs(Fragment, { children: [
128
+ "≈",
129
+ t2,
130
+ " ",
131
+ t3
132
+ ] });
133
+ $[13] = t2;
134
+ $[14] = t3;
135
+ $[15] = t4;
136
+ } else t4 = $[15];
137
+ return t4;
138
+ }
139
+ function DeliveredLabel(t0) {
140
+ const $ = c(4);
141
+ if ($[0] !== "5a5b1edf9c8dcefe67d31c9a59c95831dcc6dfede24df286efbdba62d1fd4e8d") {
142
+ for (let $i = 0; $i < 4; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
143
+ $[0] = "5a5b1edf9c8dcefe67d31c9a59c95831dcc6dfede24df286efbdba62d1fd4e8d";
144
+ }
145
+ const { children } = t0;
146
+ let t1;
147
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
148
+ t1 = /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
149
+ render: /* @__PURE__ */ jsx("button", {
150
+ type: "button",
151
+ className: "text-muted-foreground hover:text-foreground",
152
+ "aria-label": "About image optimization"
153
+ }),
154
+ children: /* @__PURE__ */ jsx(Info, { className: "h-3.5 w-3.5" })
155
+ }), /* @__PURE__ */ jsxs(TooltipContent, {
156
+ className: "max-w-xs",
157
+ children: [
158
+ "Visitors automatically receive a compressed WebP/AVIF version sized to their device. Estimates use ",
159
+ DELIVERED_PHONE_WIDTH,
160
+ "px (phone) and ",
161
+ DELIVERED_LAPTOP_WIDTH,
162
+ "px (laptop) — the original is preserved."
163
+ ]
164
+ })] });
165
+ $[1] = t1;
166
+ } else t1 = $[1];
167
+ let t2;
168
+ if ($[2] !== children) {
169
+ t2 = /* @__PURE__ */ jsxs("span", {
170
+ className: "inline-flex items-center gap-1",
171
+ children: [children, t1]
172
+ });
173
+ $[2] = children;
174
+ $[3] = t2;
175
+ } else t2 = $[3];
176
+ return t2;
177
+ }
67
178
  function formatRelativeTime(epochMs) {
68
179
  const now = Temporal.Now.instant();
69
180
  const then = Temporal.Instant.fromEpochMilliseconds(epochMs);
@@ -86,6 +197,20 @@ function formatFileSize(bytes) {
86
197
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
87
198
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
88
199
  }
200
+ const DELIVERED_PHONE_WIDTH = 640;
201
+ const DELIVERED_LAPTOP_WIDTH = 1280;
202
+ async function measureContentLength(url, signal) {
203
+ try {
204
+ const res = await fetch(url, { signal });
205
+ res.body?.cancel();
206
+ const cl = res.headers.get("content-length");
207
+ if (!cl) return null;
208
+ const parsed = Number.parseInt(cl, 10);
209
+ return Number.isFinite(parsed) ? parsed : null;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
89
214
  const AssetLightbox = ({ open, onOpenChange, fileId }) => {
90
215
  const replaceFile = useMutation(fileMutations.replace());
91
216
  const deleteFile = useMutation(fileMutations.delete());
@@ -100,12 +225,57 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
100
225
  const [zoomed, setZoomed] = useState(false);
101
226
  const [zoomedWidth, setZoomedWidth] = useState(null);
102
227
  const clickFractionRef = useRef(null);
228
+ const [deliveredSizes, setDeliveredSizes] = useState(null);
103
229
  useEffect(() => {
104
230
  if (!open) {
105
231
  setZoomed(false);
106
232
  setZoomedWidth(null);
107
233
  }
108
234
  }, [open]);
235
+ const isImage = file?.mimeType?.startsWith("image/") ?? false;
236
+ const canUseAiMetadata = isRasterImage(file?.mimeType);
237
+ const fileUrl = file?.url;
238
+ const fileMimeType = file?.mimeType;
239
+ useEffect(() => {
240
+ if (!open || !isImage || !fileUrl) {
241
+ setDeliveredSizes(null);
242
+ return;
243
+ }
244
+ const phoneUrl = transformImageUrl(fileUrl, {
245
+ width: DELIVERED_PHONE_WIDTH,
246
+ mimeType: fileMimeType,
247
+ size: file?.size
248
+ });
249
+ const laptopUrl = transformImageUrl(fileUrl, {
250
+ width: DELIVERED_LAPTOP_WIDTH,
251
+ mimeType: fileMimeType,
252
+ size: file?.size
253
+ });
254
+ if (phoneUrl === fileUrl && laptopUrl === fileUrl) {
255
+ setDeliveredSizes(null);
256
+ return;
257
+ }
258
+ setDeliveredSizes({
259
+ phone: null,
260
+ laptop: null,
261
+ measured: false
262
+ });
263
+ const controller = new AbortController();
264
+ Promise.all([measureContentLength(phoneUrl, controller.signal), measureContentLength(laptopUrl, controller.signal)]).then(([phone, laptop]) => {
265
+ if (controller.signal.aborted) return;
266
+ setDeliveredSizes({
267
+ phone,
268
+ laptop,
269
+ measured: true
270
+ });
271
+ });
272
+ return () => controller.abort();
273
+ }, [
274
+ open,
275
+ isImage,
276
+ fileUrl,
277
+ fileMimeType
278
+ ]);
109
279
  useEffect(() => {
110
280
  if (!zoomed || !zoomedWidth || !containerRef.current || !clickFractionRef.current) return;
111
281
  const container = containerRef.current;
@@ -194,7 +364,6 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
194
364
  onOpenChange(false);
195
365
  };
196
366
  if (!file) return null;
197
- const isImage = file.mimeType?.startsWith("image/");
198
367
  return /* @__PURE__ */ jsx(Dialog, {
199
368
  open,
200
369
  onOpenChange,
@@ -340,11 +509,12 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
340
509
  children: /* @__PURE__ */ jsx(Trash2, {})
341
510
  }), /* @__PURE__ */ jsx(TooltipContent, { children: "Delete" })] })
342
511
  ] }),
343
- /* @__PURE__ */ jsxs("div", {
344
- className: "flex items-center gap-2",
512
+ /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsxs(TooltipTrigger, {
513
+ render: /* @__PURE__ */ jsx("div", { className: `flex items-center gap-2 ${canUseAiMetadata ? "" : "cursor-not-allowed"}` }),
345
514
  children: [/* @__PURE__ */ jsx(Switch, {
346
515
  id: "ai-metadata",
347
- checked: file.aiMetadataEnabled !== false,
516
+ disabled: !canUseAiMetadata,
517
+ checked: canUseAiMetadata && file.aiMetadataEnabled !== false,
348
518
  onCheckedChange: (checked) => {
349
519
  setAiMetadata.mutate({
350
520
  id: fileId,
@@ -359,14 +529,15 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
359
529
  }
360
530
  }), /* @__PURE__ */ jsx(Label, {
361
531
  htmlFor: "ai-metadata",
532
+ className: canUseAiMetadata ? "" : "text-muted-foreground",
362
533
  children: "AI metadata"
363
534
  })]
364
- }),
535
+ }), !canUseAiMetadata && /* @__PURE__ */ jsx(TooltipContent, { children: "AI metadata is only available for raster images." })] }),
365
536
  /* @__PURE__ */ jsx(DebouncedFieldEditor, {
366
537
  label: "File name",
367
538
  placeholder: "File name...",
368
539
  initialValue: file.filename,
369
- disabled: file.aiMetadataEnabled !== false,
540
+ disabled: canUseAiMetadata && file.aiMetadataEnabled !== false,
370
541
  onSave: (value) => setFilename.mutate({
371
542
  id: fileId,
372
543
  filename: value
@@ -376,7 +547,7 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
376
547
  label: "Alt text",
377
548
  placeholder: "Describe this file...",
378
549
  initialValue: file.alt,
379
- disabled: file.aiMetadataEnabled !== false,
550
+ disabled: canUseAiMetadata && file.aiMetadataEnabled !== false,
380
551
  rows: 2,
381
552
  onSave: (value_0) => setAlt.mutate({
382
553
  id: fileId,
@@ -391,9 +562,22 @@ const AssetLightbox = ({ open, onOpenChange, fileId }) => {
391
562
  children: file.mimeType.split("/").pop()?.toUpperCase() ?? "Unknown"
392
563
  }),
393
564
  /* @__PURE__ */ jsx(MetadataRow, {
394
- label: "Size",
565
+ label: "Raw size",
395
566
  children: file.size != null ? formatFileSize(file.size) : "Unknown"
396
567
  }),
568
+ isImage && deliveredSizes && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MetadataRow, {
569
+ label: /* @__PURE__ */ jsx(DeliveredLabel, { children: "On phone" }),
570
+ children: /* @__PURE__ */ jsx(DeliveredSize, {
571
+ bytes: deliveredSizes.phone,
572
+ raw: file.size
573
+ })
574
+ }), /* @__PURE__ */ jsx(MetadataRow, {
575
+ label: /* @__PURE__ */ jsx(DeliveredLabel, { children: "On laptop" }),
576
+ children: /* @__PURE__ */ jsx(DeliveredSize, {
577
+ bytes: deliveredSizes.laptop,
578
+ raw: file.size
579
+ })
580
+ })] }),
397
581
  /* @__PURE__ */ jsx(MetadataRow, {
398
582
  label: "Created",
399
583
  children: formatRelativeTime(file.createdAt)