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.
- package/dist/core/createApp.d.ts +96 -60
- package/dist/core/createBlock.d.ts +58 -232
- package/dist/core/createBlock.js +47 -38
- package/dist/core/lib/contentType.d.ts +44 -24
- package/dist/core/lib/contentType.js +55 -29
- package/dist/core/lib/fieldTypes.js +28 -16
- package/dist/core/lib/imageTransform.js +76 -0
- package/dist/features/content/CamoxContent.js +3 -3
- package/dist/features/content/components/AssetCard.js +124 -77
- package/dist/features/preview/CamoxPreview.d.ts +2 -0
- package/dist/features/preview/components/AssetFieldEditor.js +10 -5
- package/dist/features/preview/components/AssetLightbox.js +197 -13
- package/dist/features/preview/components/EditPageModal.js +244 -74
- package/dist/features/preview/components/ItemFieldsEditor.js +8 -9
- package/dist/features/preview/components/MultipleAssetFieldEditor.js +49 -42
- package/dist/features/preview/components/PageContentSheet.js +2 -3
- package/dist/features/preview/components/PageTree.js +66 -68
- package/dist/features/preview/components/useRepeatableItemActions.js +5 -5
- package/dist/features/routes/pageRoute.d.ts +2 -0
- package/dist/features/routes/pageRoute.js +5 -4
- package/dist/lib/normalized-data.js +6 -4
- package/dist/lib/queries.js +33 -2
- package/dist/studio.css +1 -1
- package/package.json +4 -4
- package/skills/camox-block/SKILL.md +38 -30
- package/skills/camox-cli/SKILL.md +40 -4
|
@@ -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(
|
|
9
|
-
if ($[0] !== "
|
|
10
|
-
for (let $i = 0; $i <
|
|
11
|
-
$[0] = "
|
|
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
|
-
|
|
23
|
-
|
|
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 ($[
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 ($[
|
|
47
|
-
t7 = (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 ($[
|
|
115
|
+
if ($[33] !== t1 || $[34] !== t7) {
|
|
56
116
|
t8 = /* @__PURE__ */ jsx("div", {
|
|
57
|
-
className:
|
|
58
|
-
children:
|
|
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
|
-
$[
|
|
72
|
-
$[
|
|
73
|
-
$[
|
|
74
|
-
|
|
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 ($[
|
|
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
|
-
$[
|
|
85
|
-
$[
|
|
86
|
-
} else t9 = $[
|
|
130
|
+
$[36] = file.filename;
|
|
131
|
+
$[37] = t9;
|
|
132
|
+
} else t9 = $[37];
|
|
87
133
|
let t10;
|
|
88
|
-
if ($[
|
|
134
|
+
if ($[38] !== t2 || $[39] !== t3 || $[40] !== t4 || $[41] !== t5 || $[42] !== t6 || $[43] !== t8 || $[44] !== t9) {
|
|
89
135
|
t10 = /* @__PURE__ */ jsxs("button", {
|
|
90
|
-
type:
|
|
136
|
+
type: t2,
|
|
91
137
|
"data-asset-id": t3,
|
|
92
|
-
className:
|
|
93
|
-
onClick:
|
|
94
|
-
onDoubleClick:
|
|
138
|
+
className: t4,
|
|
139
|
+
onClick: t5,
|
|
140
|
+
onDoubleClick: t6,
|
|
95
141
|
children: [t8, t9]
|
|
96
142
|
});
|
|
97
|
-
$[
|
|
98
|
-
$[
|
|
99
|
-
$[
|
|
100
|
-
$[
|
|
101
|
-
$[
|
|
102
|
-
$[
|
|
103
|
-
$[
|
|
104
|
-
|
|
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] !== "
|
|
24
|
+
if ($[0] !== "578c810b03b0bcf84595cb011652bdf58260b853f4d229ef94d55109880eacf0") {
|
|
24
25
|
for (let $i = 0; $i < 27; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
|
|
25
|
-
$[0] = "
|
|
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] !== "
|
|
129
|
+
if ($[0] !== "578c810b03b0bcf84595cb011652bdf58260b853f4d229ef94d55109880eacf0") {
|
|
129
130
|
for (let $i = 0; $i < 29; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
|
|
130
|
-
$[0] = "
|
|
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 {
|
|
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] !== "
|
|
24
|
+
if ($[0] !== "5a5b1edf9c8dcefe67d31c9a59c95831dcc6dfede24df286efbdba62d1fd4e8d") {
|
|
24
25
|
for (let $i = 0; $i < 9; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
|
|
25
|
-
$[0] = "
|
|
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(
|
|
344
|
-
className:
|
|
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
|
-
|
|
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: "
|
|
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)
|