@calmlens/sdk 0.16.3 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +398 -163
- package/cjs/BrowserUpload-DZ0Vbmxi.js +10 -0
- package/cjs/BrowserUpload-uZjVFGfP.d.ts +11 -0
- package/cjs/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
- package/cjs/CalmLensAppClient-DBGmaDlG.js +89 -0
- package/cjs/CalmLensConnection-K1TAD1Z-.js +659 -0
- package/cjs/CalmLensServerClient-C-pxiz0C.js +645 -0
- package/cjs/CalmLensServerClient-JtP687a3.d.ts +235 -0
- package/cjs/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
- package/cjs/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
- package/cjs/CryptoUtils-B_x8lMIx-YBW1-bvL.js +42 -0
- package/cjs/FileIcons-Ob003MdS.js +175 -0
- package/cjs/Jurisdiction-FusEXb4U-zVs21tDv.js +91 -0
- package/cjs/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
- package/cjs/ModerationContextLimits-Dj1YAl3G.js +1311 -0
- package/cjs/ReactUpload-DxtFMr06.js +54 -0
- package/cjs/ReactUpload-nZahpmyg.d.ts +17 -0
- package/cjs/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
- package/cjs/TestTriggers-DcArt0Nu-CvoaONUB.js +1326 -0
- package/cjs/UploadController-BBkN4Ni6.js +618 -0
- package/{esm/index-C9Xw1aid.d.ts → cjs/WebhookPayload-2aL31v0J.d.ts} +131 -155
- package/cjs/app.d.ts +7 -44
- package/cjs/app.js +11 -89
- package/cjs/elements.d.ts +88 -0
- package/cjs/elements.js +555 -0
- package/cjs/file-icons.d.ts +67 -0
- package/cjs/file-icons.js +10 -0
- package/cjs/index-CI4BzN4W.d.ts +252 -0
- package/cjs/index.d.ts +6 -4
- package/cjs/index.js +9 -4
- package/cjs/react-native.d.ts +56 -0
- package/cjs/react-native.js +495 -0
- package/cjs/react.d.ts +30 -0
- package/cjs/react.js +370 -0
- package/cjs/server.d.ts +12 -191
- package/cjs/server.js +18 -378
- package/cjs/test-inputs.d.ts +2 -2
- package/cjs/test-inputs.js +9 -264
- package/cjs/testing.d.ts +285 -190
- package/cjs/testing.js +2393 -1203
- package/cjs/ui-CgZpgpNo.d.ts +526 -0
- package/cjs/ui-KceuoVCM.js +583 -0
- package/cjs/ui.d.ts +2 -0
- package/cjs/ui.js +27 -0
- package/cjs/vue.d.ts +490 -0
- package/cjs/vue.js +419 -0
- package/esm/BrowserUpload-D7-zNmc0.js +5 -0
- package/esm/BrowserUpload-uZjVFGfP.d.ts +11 -0
- package/esm/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
- package/esm/CalmLensAppClient-FVMwrHlH.js +84 -0
- package/esm/CalmLensConnection-iyNQHA8L.js +582 -0
- package/esm/CalmLensServerClient-JtP687a3.d.ts +235 -0
- package/esm/CalmLensServerClient-fQEYKLw4.js +586 -0
- package/esm/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
- package/esm/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
- package/esm/CryptoUtils-B_x8lMIx-DhuWGzqn.js +25 -0
- package/esm/FileIcons-M6dzjFxd.js +128 -0
- package/esm/Jurisdiction-FusEXb4U-1ov6m5yE.js +46 -0
- package/esm/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
- package/esm/ModerationContextLimits-D1x_WAtP.js +1114 -0
- package/esm/ReactUpload-D6yBCkcw.js +37 -0
- package/esm/ReactUpload-nZahpmyg.d.ts +17 -0
- package/esm/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
- package/esm/TestTriggers-DcArt0Nu-s0AQZuWE.js +1266 -0
- package/esm/UploadController-txp5NuzH.js +589 -0
- package/{cjs/index-C9Xw1aid.d.ts → esm/WebhookPayload-2aL31v0J.d.ts} +131 -155
- package/esm/app.d.ts +7 -44
- package/esm/app.js +3 -86
- package/esm/elements.d.ts +88 -0
- package/esm/elements.js +528 -0
- package/esm/file-icons.d.ts +67 -0
- package/esm/file-icons.js +1 -0
- package/esm/index-CI4BzN4W.d.ts +252 -0
- package/esm/index.d.ts +6 -4
- package/esm/index.js +1 -1
- package/esm/react-native.d.ts +56 -0
- package/esm/react-native.js +467 -0
- package/esm/react.d.ts +30 -0
- package/esm/react.js +343 -0
- package/esm/server.d.ts +12 -191
- package/esm/server.js +3 -369
- package/esm/styles.css +147 -0
- package/esm/svelte/CalmLensMedia.svelte +134 -0
- package/esm/svelte/CalmLensUploader.svelte +175 -0
- package/esm/svelte/Icon.svelte +7 -0
- package/esm/svelte/index.d.ts +37 -0
- package/esm/svelte/index.js +6 -0
- package/esm/test-inputs.d.ts +2 -2
- package/esm/test-inputs.js +1 -256
- package/esm/testing.d.ts +285 -190
- package/esm/testing.js +2329 -1122
- package/esm/ui-Ca_SpJXf.js +458 -0
- package/esm/ui-CgZpgpNo.d.ts +526 -0
- package/esm/ui.d.ts +2 -0
- package/esm/ui.js +3 -0
- package/esm/vue.d.ts +490 -0
- package/esm/vue.js +392 -0
- package/package.json +110 -11
- package/cjs/CalmLensConnection-BNUHp_AQ.js +0 -234
- package/cjs/CalmLensUtils-o171ownK.js +0 -699
- package/cjs/CalmLensVersion-DwhgxQK5.js +0 -480
- package/cjs/CalmLensWebhooks-BKrq30Yp.js +0 -201
- package/cjs/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
- package/cjs/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
- package/cjs/WebhookPayload-CJoIkGBD.d.ts +0 -11753
- package/esm/CalmLensConnection-XTqmlwzG.js +0 -193
- package/esm/CalmLensUtils-DsF8Aa_x.js +0 -574
- package/esm/CalmLensVersion-DcifRKsx.js +0 -415
- package/esm/CalmLensWebhooks-BvGyk5d3.js +0 -148
- package/esm/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
- package/esm/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
- package/esm/WebhookPayload-CJoIkGBD.d.ts +0 -11753
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { g as transformSelector, p as serializeTransform, u as getAssetUrl } from "./ModerationContextLimits-D1x_WAtP.js";
|
|
2
|
+
import { t as EU_COUNTRIES } from "./Jurisdiction-FusEXb4U-1ov6m5yE.js";
|
|
3
|
+
import { a as placeholderHashToDataURL } from "./UploadController-txp5NuzH.js";
|
|
4
|
+
import { a as getFileIcon, c as renderFileIconSvg } from "./FileIcons-M6dzjFxd.js";
|
|
5
|
+
// src/ui/Icons.ts
|
|
6
|
+
/** Path data for the components' 24×24 stroke icons, so every framework draws the same ones. */
|
|
7
|
+
const ICONS = {
|
|
8
|
+
upload: "M12 16V4m0 0L7 9m5-5 5 5M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2",
|
|
9
|
+
remove: "M18 6 6 18M6 6l12 12",
|
|
10
|
+
retry: "M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7",
|
|
11
|
+
unavailable: "M3 3l18 18M10.6 10.6a2 2 0 0 0 2.8 2.8M9.9 5.1A9.8 9.8 0 0 1 12 5c5 0 9 5 10 7a17 17 0 0 1-3.3 4.2M6.6 6.6C4.4 8 2.8 10.4 2 12c1 2 5 7 10 7a9.6 9.6 0 0 0 5.4-1.6"
|
|
12
|
+
};
|
|
13
|
+
/** Attributes shared by every icon's `<svg>`. */
|
|
14
|
+
const ICON_ATTRIBUTES = {
|
|
15
|
+
width: "18",
|
|
16
|
+
height: "18",
|
|
17
|
+
viewBox: "0 0 24 24",
|
|
18
|
+
fill: "none",
|
|
19
|
+
stroke: "currentColor",
|
|
20
|
+
"stroke-width": "2",
|
|
21
|
+
"stroke-linecap": "round",
|
|
22
|
+
"stroke-linejoin": "round",
|
|
23
|
+
"aria-hidden": "true"
|
|
24
|
+
};
|
|
25
|
+
// src/ui/Media.ts
|
|
26
|
+
/**
|
|
27
|
+
* The image URL to load for a box `displayWidth` pixels wide: the smallest width that covers it,
|
|
28
|
+
* or the largest when none does. Without `widths`, or before the box is measured, the image's own
|
|
29
|
+
* `src`. React Native, which has no `srcset`, calls this with its measured size.
|
|
30
|
+
*/
|
|
31
|
+
const pickMediaSource = (state, displayWidth) => {
|
|
32
|
+
const { sources } = state;
|
|
33
|
+
if (!sources?.length || !displayWidth) return state.src;
|
|
34
|
+
return (sources.find((source) => source.width >= displayWidth) ?? sources.at(-1))?.src ?? state.src;
|
|
35
|
+
};
|
|
36
|
+
const decodedPlaceholders = /* @__PURE__ */ new Map();
|
|
37
|
+
/** Renders a ThumbHash as a PNG data URL, once per hash; `undefined` for an invalid hash. */
|
|
38
|
+
const decodePlaceholderHash = (hash) => {
|
|
39
|
+
if (!hash) return void 0;
|
|
40
|
+
if (!decodedPlaceholders.has(hash)) {
|
|
41
|
+
let decoded;
|
|
42
|
+
try {
|
|
43
|
+
decoded = placeholderHashToDataURL(hash);
|
|
44
|
+
} catch {}
|
|
45
|
+
if (decodedPlaceholders.size > 500) decodedPlaceholders.clear();
|
|
46
|
+
decodedPlaceholders.set(hash, decoded);
|
|
47
|
+
}
|
|
48
|
+
return decodedPlaceholders.get(hash);
|
|
49
|
+
};
|
|
50
|
+
const joinTransforms = (base, extra) => [transformSelector(base), extra && serializeTransform(extra)].filter(Boolean).join(",") || void 0;
|
|
51
|
+
/**
|
|
52
|
+
* Fills the props left unset from `asset`, if there is one: the project and ID, its moderation
|
|
53
|
+
* status, `video` or `audio` for those asset types, and its stored preview's ThumbHash, average
|
|
54
|
+
* colour and size. Every framework's media component calls this before rendering.
|
|
55
|
+
*/
|
|
56
|
+
const resolveMediaProps = (props) => {
|
|
57
|
+
const { asset } = props;
|
|
58
|
+
if (!asset) return props;
|
|
59
|
+
const preview = asset.content?.image;
|
|
60
|
+
const regionEntries = asset.classification?.moderation?.regions ?? [];
|
|
61
|
+
const ranks = {
|
|
62
|
+
pending: 0,
|
|
63
|
+
allowed: 0,
|
|
64
|
+
limited: 1,
|
|
65
|
+
"age-restricted": 2,
|
|
66
|
+
blocked: 3,
|
|
67
|
+
purged: 4
|
|
68
|
+
};
|
|
69
|
+
const statuses = {
|
|
70
|
+
limit: "limited",
|
|
71
|
+
"age-restrict": "age-restricted",
|
|
72
|
+
block: "blocked",
|
|
73
|
+
purge: "purged"
|
|
74
|
+
};
|
|
75
|
+
let status = props.status ?? asset.classification?.moderation?.status ?? "allowed";
|
|
76
|
+
for (const entry of regionEntries) {
|
|
77
|
+
const region = props.viewerClaims?.region;
|
|
78
|
+
if (region && (entry.region === region || entry.region === "EU" && EU_COUNTRIES.has(region))) {
|
|
79
|
+
const regionalStatus = statuses[entry.action];
|
|
80
|
+
if (regionalStatus && ranks[regionalStatus] > ranks[status]) status = regionalStatus;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const kind = asset.type === "video" || asset.type === "audio" ? asset.type : void 0;
|
|
84
|
+
return {
|
|
85
|
+
...props,
|
|
86
|
+
projectId: props.projectId ?? asset.projectId,
|
|
87
|
+
assetId: props.assetId ?? asset.id,
|
|
88
|
+
status,
|
|
89
|
+
contentRemoved: props.contentRemoved ?? asset.content?.status === "removed",
|
|
90
|
+
type: props.type ?? kind,
|
|
91
|
+
placeholderHash: props.placeholderHash ?? preview?.thumbhash ?? void 0,
|
|
92
|
+
averageColor: props.averageColor ?? preview?.averageColor ?? void 0,
|
|
93
|
+
width: props.width ?? preview?.width ?? void 0,
|
|
94
|
+
height: props.height ?? preview?.height ?? void 0
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const getMediaState = (options) => buildMediaState(resolveMediaProps(options));
|
|
98
|
+
const buildMediaState = ({ projectId, assetId, status = "allowed", contentRemoved = false, type = "image", resource = "rendered", lens, transform, widths, url, placeholder = true, placeholderHash, placeholderUrl: customPlaceholderUrl, cdnBaseUrl, viewerClaims }) => {
|
|
99
|
+
if (status === "blocked" || status === "purged" || contentRemoved) return { kind: "unavailable" };
|
|
100
|
+
const region = viewerClaims?.region;
|
|
101
|
+
const ageConfirmed = viewerClaims?.ageConfirmed === true;
|
|
102
|
+
const asset = {
|
|
103
|
+
projectId,
|
|
104
|
+
assetIdentifier: assetId,
|
|
105
|
+
cdnBaseUrl,
|
|
106
|
+
viewerClaims: region || ageConfirmed ? {
|
|
107
|
+
...region ? { region } : {},
|
|
108
|
+
...ageConfirmed ? { ageConfirmed } : {}
|
|
109
|
+
} : void 0
|
|
110
|
+
};
|
|
111
|
+
const placeholderUrl = placeholder ? decodePlaceholderHash(placeholderHash) ?? customPlaceholderUrl ?? (type !== "image" ? void 0 : getAssetUrl({
|
|
112
|
+
projectId,
|
|
113
|
+
assetIdentifier: assetId,
|
|
114
|
+
cdnBaseUrl,
|
|
115
|
+
resource: "placeholder",
|
|
116
|
+
viewerClaims: region ? { region } : void 0
|
|
117
|
+
})) : void 0;
|
|
118
|
+
if (status === "age-restricted" && !ageConfirmed) return {
|
|
119
|
+
kind: "gated",
|
|
120
|
+
placeholderUrl
|
|
121
|
+
};
|
|
122
|
+
const build = (options = {}) => {
|
|
123
|
+
const chosen = options.resource ?? resource;
|
|
124
|
+
const rendered = chosen === "rendered";
|
|
125
|
+
return getAssetUrl({
|
|
126
|
+
...asset,
|
|
127
|
+
resource: chosen,
|
|
128
|
+
lens: rendered ? lens : void 0,
|
|
129
|
+
transform: rendered ? joinTransforms(transform, options.extra) : void 0
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
if (type === "audio") return {
|
|
133
|
+
kind: "audio",
|
|
134
|
+
src: url ?? build({ resource: "original" })
|
|
135
|
+
};
|
|
136
|
+
if (type === "video") return {
|
|
137
|
+
kind: "video",
|
|
138
|
+
src: url ?? build({ resource: "original" }),
|
|
139
|
+
placeholderUrl
|
|
140
|
+
};
|
|
141
|
+
if (url) return {
|
|
142
|
+
kind: "image",
|
|
143
|
+
src: url,
|
|
144
|
+
placeholderUrl
|
|
145
|
+
};
|
|
146
|
+
const sources = [...new Set(widths ?? [])].filter((width) => Number.isSafeInteger(width) && width > 0).sort((a, b) => a - b).map((width) => ({
|
|
147
|
+
src: build({ extra: {
|
|
148
|
+
type: "imageTransform",
|
|
149
|
+
options: { width }
|
|
150
|
+
} }),
|
|
151
|
+
width
|
|
152
|
+
}));
|
|
153
|
+
return {
|
|
154
|
+
kind: "image",
|
|
155
|
+
src: sources.at(-1)?.src ?? build(),
|
|
156
|
+
srcSet: sources.length ? sources.map(({ src, width }) => `${src} ${width}w`).join(", ") : void 0,
|
|
157
|
+
sources: sources.length ? sources : void 0,
|
|
158
|
+
placeholderUrl
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
// src/ui/Messages.ts
|
|
162
|
+
/** The size of a row's file-type icon, the same as an image preview. */
|
|
163
|
+
const FILE_ICON_SIZE = 40;
|
|
164
|
+
const DEFAULT_MESSAGES = {
|
|
165
|
+
dropzoneTitle: "Click to upload or drag and drop",
|
|
166
|
+
dropzoneTitleDragging: "Drop to upload",
|
|
167
|
+
chooseFile: "Select file",
|
|
168
|
+
chooseFiles: "Select files",
|
|
169
|
+
chooseAnother: "Choose another",
|
|
170
|
+
maxFileSizeHint: "Up to {size}",
|
|
171
|
+
selected: "",
|
|
172
|
+
queued: "Waiting…",
|
|
173
|
+
uploading: "Uploading…",
|
|
174
|
+
checking: "Checking…",
|
|
175
|
+
submitted: "Uploaded.",
|
|
176
|
+
pending: "Uploaded. It will be shown once it has been checked.",
|
|
177
|
+
allowed: "Uploaded.",
|
|
178
|
+
limited: "Uploaded.",
|
|
179
|
+
"age-restricted": "Uploaded. It will only be shown to people who can see sensitive content.",
|
|
180
|
+
blocked: "This file can't be used because it may break our community guidelines.",
|
|
181
|
+
purged: "This file can't be used because it breaks our community guidelines.",
|
|
182
|
+
failed: "We couldn't upload this file. Please try again.",
|
|
183
|
+
"file/not-accepted": "This type of file isn't accepted.",
|
|
184
|
+
"file/too-large": "This file is too large.",
|
|
185
|
+
"upload-preset/too-large": "This file is too large.",
|
|
186
|
+
"upload-preset/content-type-not-allowed": "This type of file isn't accepted.",
|
|
187
|
+
"upload-preset/daily-limit-reached": "You've reached today's upload limit. Try again tomorrow.",
|
|
188
|
+
"upload-token/expired": "Your upload expired. Please try again.",
|
|
189
|
+
"sdk/cancelled": "Cancelled.",
|
|
190
|
+
retry: "Retry",
|
|
191
|
+
remove: "Remove {name}",
|
|
192
|
+
cancel: "Cancel {name}",
|
|
193
|
+
sensitiveTitle: "Sensitive content",
|
|
194
|
+
sensitiveBody: "This may not be suitable for everyone.",
|
|
195
|
+
reveal: "View",
|
|
196
|
+
unavailable: "Unavailable",
|
|
197
|
+
regionalUnavailable: "Unavailable in your region"
|
|
198
|
+
};
|
|
199
|
+
const resolveMessages = (overrides) => overrides ? {
|
|
200
|
+
...DEFAULT_MESSAGES,
|
|
201
|
+
...overrides
|
|
202
|
+
} : DEFAULT_MESSAGES;
|
|
203
|
+
/** The unavailable notice every media binding shows for the same resolved state. */
|
|
204
|
+
const getMediaUnavailableMessage = (state, messages) => state.kind === "unavailable" && state.reason === "regional" ? messages.regionalUnavailable : messages.unavailable;
|
|
205
|
+
/** Fills `{name}`-style placeholders. */
|
|
206
|
+
const formatMessage = (message, values) => message.replace(/\{(\w+)\}/g, (match, name) => values[name] ?? match);
|
|
207
|
+
/** The message for a file's current state. */
|
|
208
|
+
const getUploadItemMessage = (item, messages) => {
|
|
209
|
+
if (item.stage === "failed") return item.errorCode && messages[item.errorCode] || messages.failed;
|
|
210
|
+
if (item.stage === "complete") return messages[item.status ?? "submitted"] ?? messages.submitted;
|
|
211
|
+
return messages[item.stage];
|
|
212
|
+
};
|
|
213
|
+
/** How a finished file turned out, for styling: `accepted`, `restricted` or `rejected`. */
|
|
214
|
+
const getUploadItemOutcome = (item) => {
|
|
215
|
+
if (item.stage === "failed") return "rejected";
|
|
216
|
+
if (item.stage !== "complete") return void 0;
|
|
217
|
+
if (item.status === "blocked" || item.status === "purged") return "rejected";
|
|
218
|
+
return item.status === "age-restricted" ? "restricted" : "accepted";
|
|
219
|
+
};
|
|
220
|
+
/** A byte count for people, such as `1.2 MB`. */
|
|
221
|
+
const formatBytes = (bytes) => {
|
|
222
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
223
|
+
const units = [
|
|
224
|
+
"KB",
|
|
225
|
+
"MB",
|
|
226
|
+
"GB"
|
|
227
|
+
];
|
|
228
|
+
let value = bytes / 1024;
|
|
229
|
+
let unit = 0;
|
|
230
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
231
|
+
value /= 1024;
|
|
232
|
+
unit++;
|
|
233
|
+
}
|
|
234
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
235
|
+
};
|
|
236
|
+
const describeUploadItem = (item, messages) => {
|
|
237
|
+
const busy = item.stage === "queued" || item.stage === "uploading" || item.stage === "checking";
|
|
238
|
+
return {
|
|
239
|
+
message: getUploadItemMessage(item, messages),
|
|
240
|
+
outcome: getUploadItemOutcome(item),
|
|
241
|
+
busy,
|
|
242
|
+
progressPercent: item.stage === "uploading" && item.progress !== null ? Math.round(item.progress * 100) : void 0,
|
|
243
|
+
canRetry: item.stage === "failed" && !item.errorCode?.startsWith("file/"),
|
|
244
|
+
removeLabel: formatMessage(busy ? messages.cancel : messages.remove, { name: item.file.name }),
|
|
245
|
+
fileIcon: item.previewUrl ? "" : renderFileIconSvg(getFileIcon(item.file), FILE_ICON_SIZE),
|
|
246
|
+
size: formatBytes(item.file.size)
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
/** The hint under the drop zone's title, or `undefined` without a size limit. */
|
|
250
|
+
const getDropzoneHint = (messages, maxFileSize) => maxFileSize === void 0 ? void 0 : formatMessage(messages.maxFileSizeHint, { size: formatBytes(maxFileSize) });
|
|
251
|
+
const describeUploader = (snapshot, options, messages) => ({
|
|
252
|
+
showDropzone: Boolean(options.multiple) || snapshot.items.length === 0,
|
|
253
|
+
chooseLabel: options.multiple ? messages.chooseFiles : messages.chooseFile,
|
|
254
|
+
hint: getDropzoneHint(messages, options.maxFileSize)
|
|
255
|
+
});
|
|
256
|
+
// src/ui/Props.ts
|
|
257
|
+
/** The aspect ratio a media component's box keeps, if it knows one. */
|
|
258
|
+
const getMediaAspectRatio = ({ aspectRatio, width, height }) => aspectRatio ?? (width && height ? width / height : void 0);
|
|
259
|
+
/** A key that changes whenever a media component must load something new. */
|
|
260
|
+
const getMediaLoadKey = (state) => state.kind === "image" ? `${state.src} ${state.srcSet ?? ""}` : state.kind === "video" || state.kind === "audio" ? state.src : state.kind === "unavailable" && state.reason === "regional" ? "unavailable-regional" : state.kind;
|
|
261
|
+
// src/ui/Theme.ts
|
|
262
|
+
const CALMLENS_THEME = {
|
|
263
|
+
accent: "#635bff",
|
|
264
|
+
accentText: "#fff",
|
|
265
|
+
radius: 12,
|
|
266
|
+
text: "#1f2328",
|
|
267
|
+
background: "transparent",
|
|
268
|
+
success: "#12a26f",
|
|
269
|
+
warning: "#e8a317",
|
|
270
|
+
danger: "#e5484d"
|
|
271
|
+
};
|
|
272
|
+
/** `color` at `alpha` as `rgba()`, for a hex colour; other colours are returned as they are. */
|
|
273
|
+
const withAlpha = (color, alpha) => {
|
|
274
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color)?.[1];
|
|
275
|
+
if (!hex) return color;
|
|
276
|
+
const full = hex.length === 3 ? [...hex].map((digit) => digit + digit).join("") : hex;
|
|
277
|
+
const [red, green, blue] = [
|
|
278
|
+
0,
|
|
279
|
+
2,
|
|
280
|
+
4
|
|
281
|
+
].map((start) => Number.parseInt(full.slice(start, start + 2), 16));
|
|
282
|
+
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* The full theme from your overrides, mixing the muted, border and surface colours from `text`
|
|
286
|
+
* as the stylesheet mixes them from the page's text colour.
|
|
287
|
+
*/
|
|
288
|
+
const resolveTheme = (overrides = {}) => {
|
|
289
|
+
const text = overrides.text ?? CALMLENS_THEME.text;
|
|
290
|
+
return {
|
|
291
|
+
...CALMLENS_THEME,
|
|
292
|
+
muted: withAlpha(text, .72),
|
|
293
|
+
border: withAlpha(text, .18),
|
|
294
|
+
surface: withAlpha(text, .04),
|
|
295
|
+
...overrides,
|
|
296
|
+
text
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
// src/ui/Styles.ts
|
|
300
|
+
/**
|
|
301
|
+
* The components' stylesheet, also shipped as `@calmlens/sdk/styles.css`. The React components
|
|
302
|
+
* render it themselves (React hoists it into the head, on the server too); with Vue, Svelte or
|
|
303
|
+
* the web components, import or link `@calmlens/sdk/styles.css` once. Nothing is added to the page
|
|
304
|
+
* at runtime. Theme it with the `--calmlens-*` custom properties, or leave it out and style the
|
|
305
|
+
* `calmlens-*` classes yourself.
|
|
306
|
+
*/
|
|
307
|
+
const CALMLENS_STYLES = `
|
|
308
|
+
/* Zero specificity, so a page's own --calmlens-* values win even on the same element. */
|
|
309
|
+
:where(.calmlens-uploader, .calmlens-media) {
|
|
310
|
+
--calmlens-accent: ${CALMLENS_THEME.accent};
|
|
311
|
+
--calmlens-accent-text: ${CALMLENS_THEME.accentText};
|
|
312
|
+
--calmlens-radius: ${CALMLENS_THEME.radius}px;
|
|
313
|
+
--calmlens-border: color-mix(in srgb, currentColor 18%, transparent);
|
|
314
|
+
--calmlens-surface: color-mix(in srgb, currentColor 4%, transparent);
|
|
315
|
+
--calmlens-background: ${CALMLENS_THEME.background};
|
|
316
|
+
--calmlens-muted: color-mix(in srgb, currentColor 72%, transparent);
|
|
317
|
+
--calmlens-success: ${CALMLENS_THEME.success};
|
|
318
|
+
--calmlens-warning: ${CALMLENS_THEME.warning};
|
|
319
|
+
--calmlens-danger: ${CALMLENS_THEME.danger};
|
|
320
|
+
--calmlens-focus-ring: var(--calmlens-accent);
|
|
321
|
+
--calmlens-focus-width: 2px;
|
|
322
|
+
--calmlens-focus-offset: 2px;
|
|
323
|
+
box-sizing: border-box;
|
|
324
|
+
font: inherit;
|
|
325
|
+
}
|
|
326
|
+
.calmlens-uploader *, .calmlens-media * { box-sizing: border-box; }
|
|
327
|
+
.calmlens-uploader { display: grid; gap: 12px; }
|
|
328
|
+
.calmlens-uploader > [hidden] { display: none; }
|
|
329
|
+
.calmlens-dropzone {
|
|
330
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
|
|
331
|
+
padding: 40px 24px; text-align: center;
|
|
332
|
+
border: 1.5px dashed var(--calmlens-border); border-radius: var(--calmlens-radius);
|
|
333
|
+
background: var(--calmlens-surface); cursor: pointer;
|
|
334
|
+
transition: border-color 200ms ease-in-out, background-color 200ms ease-in-out;
|
|
335
|
+
}
|
|
336
|
+
.calmlens-dropzone:hover, .calmlens-dropzone:focus-within {
|
|
337
|
+
border-color: color-mix(in srgb, var(--calmlens-accent) 30%, transparent);
|
|
338
|
+
background: color-mix(in srgb, var(--calmlens-accent) 4%, transparent);
|
|
339
|
+
}
|
|
340
|
+
.calmlens-dropzone[data-dragging] {
|
|
341
|
+
border-color: var(--calmlens-accent);
|
|
342
|
+
background: color-mix(in srgb, var(--calmlens-accent) 9%, transparent);
|
|
343
|
+
}
|
|
344
|
+
.calmlens-dropzone[data-disabled] {
|
|
345
|
+
cursor: not-allowed; border-color: var(--calmlens-border); background: none; color: var(--calmlens-muted);
|
|
346
|
+
}
|
|
347
|
+
.calmlens-dropzone__icon {
|
|
348
|
+
display: grid; place-items: center; width: 48px; height: 48px; border-radius: 50%;
|
|
349
|
+
color: var(--calmlens-accent); background: color-mix(in srgb, var(--calmlens-accent) 10%, transparent);
|
|
350
|
+
transition: transform 200ms;
|
|
351
|
+
}
|
|
352
|
+
.calmlens-dropzone[data-disabled] .calmlens-dropzone__icon { color: inherit; background: var(--calmlens-surface); }
|
|
353
|
+
.calmlens-dropzone[data-dragging] .calmlens-dropzone__icon { transform: scale(1.1); }
|
|
354
|
+
.calmlens-dropzone__text { display: grid; gap: 4px; }
|
|
355
|
+
.calmlens-dropzone__title { font-weight: 500; margin: 0; }
|
|
356
|
+
.calmlens-dropzone__hint { font-size: 0.875em; margin: 0; color: var(--calmlens-muted); }
|
|
357
|
+
.calmlens-button, .calmlens-icon-button { margin: 0; width: auto; text-transform: none; letter-spacing: normal; }
|
|
358
|
+
.calmlens-button {
|
|
359
|
+
font: inherit; font-weight: 500; font-size: 0.875em; padding: 7px 14px; border-radius: 8px; cursor: pointer;
|
|
360
|
+
border: 1px solid var(--calmlens-accent); background: var(--calmlens-accent); color: var(--calmlens-accent-text);
|
|
361
|
+
white-space: nowrap; transition: background-color 150ms, border-color 150ms;
|
|
362
|
+
}
|
|
363
|
+
.calmlens-button:focus-visible, .calmlens-icon-button:focus-visible {
|
|
364
|
+
outline: var(--calmlens-focus-width) solid var(--calmlens-focus-ring); outline-offset: var(--calmlens-focus-offset);
|
|
365
|
+
}
|
|
366
|
+
.calmlens-button:hover:not(:disabled) { background: var(--calmlens-accent); filter: brightness(1.08); }
|
|
367
|
+
.calmlens-button[data-variant="outline"] { background: none; color: inherit; border-color: var(--calmlens-border); }
|
|
368
|
+
.calmlens-button[data-variant="outline"]:hover:not(:disabled) {
|
|
369
|
+
filter: none; background: color-mix(in srgb, var(--calmlens-accent) 8%, transparent); border-color: var(--calmlens-accent);
|
|
370
|
+
}
|
|
371
|
+
.calmlens-button[data-size="small"] { padding: 4px 10px; }
|
|
372
|
+
.calmlens-button:disabled {
|
|
373
|
+
cursor: not-allowed; border-style: dashed; background: none; color: var(--calmlens-muted); border-color: var(--calmlens-border);
|
|
374
|
+
}
|
|
375
|
+
.calmlens-uploads { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
376
|
+
.calmlens-upload {
|
|
377
|
+
display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 12px;
|
|
378
|
+
border: 1px solid var(--calmlens-border); border-radius: var(--calmlens-radius); background: var(--calmlens-background);
|
|
379
|
+
}
|
|
380
|
+
/* A finished file's outcome is a dot in its status colour before the message. */
|
|
381
|
+
.calmlens-upload[data-outcome] .calmlens-upload__message::before {
|
|
382
|
+
content: ""; display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 50%;
|
|
383
|
+
vertical-align: 0.05em;
|
|
384
|
+
}
|
|
385
|
+
.calmlens-upload[data-outcome="accepted"] .calmlens-upload__message::before { background: var(--calmlens-success); }
|
|
386
|
+
.calmlens-upload[data-outcome="restricted"] .calmlens-upload__message::before { background: var(--calmlens-warning); }
|
|
387
|
+
.calmlens-upload[data-outcome="rejected"] .calmlens-upload__message::before { background: var(--calmlens-danger); }
|
|
388
|
+
.calmlens-upload__thumb {
|
|
389
|
+
width: 40px; height: 40px; border-radius: 8px; object-fit: cover; display: grid; place-items: center;
|
|
390
|
+
background: var(--calmlens-surface); border: 1px solid var(--calmlens-border);
|
|
391
|
+
font-size: 0.6875em; font-weight: 700; text-transform: uppercase; overflow: hidden;
|
|
392
|
+
}
|
|
393
|
+
.calmlens-upload__thumb[data-kind="file"] { background: none; border: none; }
|
|
394
|
+
.calmlens-upload[data-outcome="rejected"] img.calmlens-upload__thumb { filter: blur(6px); }
|
|
395
|
+
.calmlens-upload__body { min-width: 0; display: grid; gap: 4px; }
|
|
396
|
+
.calmlens-upload__name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
397
|
+
.calmlens-upload__size { font-size: 0.875em; color: var(--calmlens-muted); }
|
|
398
|
+
.calmlens-upload__message { font-size: 0.875em; margin: 0; }
|
|
399
|
+
.calmlens-progress {
|
|
400
|
+
height: 4px; border-radius: 2px; overflow: hidden; background: var(--calmlens-border); position: relative;
|
|
401
|
+
}
|
|
402
|
+
.calmlens-progress__bar {
|
|
403
|
+
position: absolute; inset: 0 auto 0 0; background: var(--calmlens-accent); border-radius: inherit;
|
|
404
|
+
transition: width 150ms;
|
|
405
|
+
}
|
|
406
|
+
.calmlens-progress:not([aria-valuenow]) .calmlens-progress__bar {
|
|
407
|
+
width: 35%; animation: calmlens-indeterminate 1.1s ease-in-out infinite;
|
|
408
|
+
}
|
|
409
|
+
@keyframes calmlens-indeterminate { from { left: -35%; } to { left: 100%; } }
|
|
410
|
+
.calmlens-upload__actions { display: flex; gap: 8px; align-items: center; }
|
|
411
|
+
.calmlens-icon-button {
|
|
412
|
+
display: grid; place-items: center; width: 30px; height: 30px; padding: 0; border-radius: 8px; cursor: pointer;
|
|
413
|
+
border: 1px solid transparent; background: none; color: inherit;
|
|
414
|
+
}
|
|
415
|
+
.calmlens-icon-button:hover { background: color-mix(in srgb, var(--calmlens-accent) 8%, transparent); border-color: var(--calmlens-border); }
|
|
416
|
+
@media (prefers-reduced-motion: reduce) {
|
|
417
|
+
.calmlens-progress__bar, .calmlens-dropzone, .calmlens-dropzone__icon, .calmlens-media__content { transition: none; }
|
|
418
|
+
.calmlens-progress:not([aria-valuenow]) .calmlens-progress__bar { animation: none; left: 0; width: 100%; }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.calmlens-media {
|
|
422
|
+
position: relative; display: block; overflow: hidden; margin: 0; border-radius: var(--calmlens-media-radius, 0);
|
|
423
|
+
background: var(--calmlens-surface);
|
|
424
|
+
}
|
|
425
|
+
.calmlens-media__placeholder, .calmlens-media__content {
|
|
426
|
+
position: absolute; inset: 0; width: 100%; height: 100%; object-fit: var(--calmlens-fit, cover);
|
|
427
|
+
object-position: var(--calmlens-position, center);
|
|
428
|
+
}
|
|
429
|
+
.calmlens-media:not([data-sized]) .calmlens-media__content { position: relative; height: auto; }
|
|
430
|
+
.calmlens-media:not([data-sized])[data-kind="gated"], .calmlens-media:not([data-sized])[data-kind="unavailable"],
|
|
431
|
+
.calmlens-media:not([data-sized])[data-state="error"] { min-height: 160px; }
|
|
432
|
+
.calmlens-media__placeholder { transform: scale(1.1); filter: blur(12px); }
|
|
433
|
+
.calmlens-media__content { opacity: 0; transition: opacity 250ms; }
|
|
434
|
+
.calmlens-media[data-state="loaded"] .calmlens-media__content { opacity: 1; }
|
|
435
|
+
.calmlens-media[data-kind="video"] .calmlens-media__content, .calmlens-media[data-kind="audio"] .calmlens-media__content { opacity: 1; }
|
|
436
|
+
.calmlens-media[data-kind="audio"] { background: none; }
|
|
437
|
+
.calmlens-media[data-kind="audio"] .calmlens-media__content { position: static; height: auto; }
|
|
438
|
+
.calmlens-media__notice {
|
|
439
|
+
position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; gap: 6px;
|
|
440
|
+
padding: 12px; text-align: center; color: #fff; background: rgba(0, 0, 0, 0.45);
|
|
441
|
+
}
|
|
442
|
+
.calmlens-media__notice p { margin: 0; display: flex; gap: 6px; align-items: center; }
|
|
443
|
+
/* Thumbnails keep the notice's icon and leave its text to screen readers. */
|
|
444
|
+
.calmlens-media[data-sized] { container-type: inline-size; }
|
|
445
|
+
@container (max-width: 119px) {
|
|
446
|
+
.calmlens-media__label, .calmlens-media__notice > strong, .calmlens-media__notice > p:not(:has(svg)) {
|
|
447
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
.calmlens-media__notice strong { font-weight: 600; }
|
|
451
|
+
.calmlens-media__notice .calmlens-button { margin-top: 4px; border-color: #fff; background: #fff; color: #111; }
|
|
452
|
+
.calmlens-media[data-state="error"] .calmlens-media__notice, .calmlens-media[data-kind="unavailable"] .calmlens-media__notice {
|
|
453
|
+
color: inherit; background: var(--calmlens-surface);
|
|
454
|
+
}
|
|
455
|
+
`;
|
|
456
|
+
/** Identifies the stylesheet, so React adds it once however many components render. */
|
|
457
|
+
const CALMLENS_STYLES_ID = "calmlens-styles";
|
|
458
|
+
export { getMediaState as _, getMediaAspectRatio as a, ICONS as b, describeUploadItem as c, formatMessage as d, getDropzoneHint as f, resolveMessages as g, getUploadItemOutcome as h, resolveTheme as i, describeUploader as l, getUploadItemMessage as m, CALMLENS_STYLES_ID as n, getMediaLoadKey as o, getMediaUnavailableMessage as p, CALMLENS_THEME as r, DEFAULT_MESSAGES as s, CALMLENS_STYLES as t, formatBytes as u, pickMediaSource as v, ICON_ATTRIBUTES as x, resolveMediaProps as y };
|