@dispatchcms/next 0.0.7 → 0.0.9
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 +15 -0
- package/dist/index.d.mts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/client.ts +62 -0
- package/src/index.ts +2 -1
- package/test/image-props.test.ts +22 -0
package/README.md
CHANGED
|
@@ -148,3 +148,18 @@ export default async function PreviewPage({
|
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
In the CMS, set **Site URL** in Site settings to your site’s base URL (e.g. `https://yoursite.com`). The preview link will use that URL plus `/preview?token=...`.
|
|
151
|
+
|
|
152
|
+
## Images
|
|
153
|
+
|
|
154
|
+
An image field is a URL until an editor crops it, then `{ url, crop }` with a unit rectangle (`x`, `y`, `width`, `height` in `0…1`). The original file is unchanged. Wrap the image in a sized `overflow-hidden` frame and spread `imageProps` onto the `<img>`:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { imageProps } from "@dispatchcms/next";
|
|
158
|
+
|
|
159
|
+
const photo = imageProps(entry.data.hero);
|
|
160
|
+
photo ? (
|
|
161
|
+
<div className="relative h-64 w-full overflow-hidden">
|
|
162
|
+
<img src={photo.src} alt="" style={photo.style} />
|
|
163
|
+
</div>
|
|
164
|
+
) : null;
|
|
165
|
+
```
|
package/dist/index.d.mts
CHANGED
|
@@ -22,5 +22,25 @@ declare function getEntries(collection: string, siteKey?: string): Promise<Entry
|
|
|
22
22
|
declare function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null>;
|
|
23
23
|
/** Draft or published entry. The token is the secret; no site key is required. */
|
|
24
24
|
declare function getEntryByPreviewToken(token: string): Promise<Entry | null>;
|
|
25
|
+
type ImageCrop = {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
};
|
|
31
|
+
/** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */
|
|
32
|
+
declare function imageProps(value: unknown): {
|
|
33
|
+
src: string;
|
|
34
|
+
crop: ImageCrop;
|
|
35
|
+
style: {
|
|
36
|
+
position: "absolute";
|
|
37
|
+
left: string;
|
|
38
|
+
top: string;
|
|
39
|
+
width: string;
|
|
40
|
+
height: string;
|
|
41
|
+
maxWidth: "none";
|
|
42
|
+
maxHeight: "none";
|
|
43
|
+
};
|
|
44
|
+
} | null;
|
|
25
45
|
|
|
26
|
-
export { type DispatchConfig, type Entry, getConfig, getEntries, getEntry, getEntryByPreviewToken, initDispatch };
|
|
46
|
+
export { type DispatchConfig, type Entry, type ImageCrop, getConfig, getEntries, getEntry, getEntryByPreviewToken, imageProps, initDispatch };
|
package/dist/index.d.ts
CHANGED
|
@@ -22,5 +22,25 @@ declare function getEntries(collection: string, siteKey?: string): Promise<Entry
|
|
|
22
22
|
declare function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null>;
|
|
23
23
|
/** Draft or published entry. The token is the secret; no site key is required. */
|
|
24
24
|
declare function getEntryByPreviewToken(token: string): Promise<Entry | null>;
|
|
25
|
+
type ImageCrop = {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
};
|
|
31
|
+
/** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */
|
|
32
|
+
declare function imageProps(value: unknown): {
|
|
33
|
+
src: string;
|
|
34
|
+
crop: ImageCrop;
|
|
35
|
+
style: {
|
|
36
|
+
position: "absolute";
|
|
37
|
+
left: string;
|
|
38
|
+
top: string;
|
|
39
|
+
width: string;
|
|
40
|
+
height: string;
|
|
41
|
+
maxWidth: "none";
|
|
42
|
+
maxHeight: "none";
|
|
43
|
+
};
|
|
44
|
+
} | null;
|
|
25
45
|
|
|
26
|
-
export { type DispatchConfig, type Entry, getConfig, getEntries, getEntry, getEntryByPreviewToken, initDispatch };
|
|
46
|
+
export { type DispatchConfig, type Entry, type ImageCrop, getConfig, getEntries, getEntry, getEntryByPreviewToken, imageProps, initDispatch };
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
getEntries: () => getEntries,
|
|
25
25
|
getEntry: () => getEntry,
|
|
26
26
|
getEntryByPreviewToken: () => getEntryByPreviewToken,
|
|
27
|
+
imageProps: () => imageProps,
|
|
27
28
|
initDispatch: () => initDispatch
|
|
28
29
|
});
|
|
29
30
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -85,12 +86,57 @@ async function getEntryByPreviewToken(token) {
|
|
|
85
86
|
}
|
|
86
87
|
return await res.json();
|
|
87
88
|
}
|
|
89
|
+
function clampCropSide(n, fallback) {
|
|
90
|
+
return typeof n === "number" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;
|
|
91
|
+
}
|
|
92
|
+
function readCrop(value) {
|
|
93
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
94
|
+
return { x: 0, y: 0, width: 1, height: 1 };
|
|
95
|
+
}
|
|
96
|
+
const row = value;
|
|
97
|
+
let x = clampCropSide(row.x, 0);
|
|
98
|
+
let y = clampCropSide(row.y, 0);
|
|
99
|
+
let width = clampCropSide(row.width, 1);
|
|
100
|
+
let height = clampCropSide(row.height, 1);
|
|
101
|
+
width = Math.max(0.05, Math.min(width, 1 - x));
|
|
102
|
+
height = Math.max(0.05, Math.min(height, 1 - y));
|
|
103
|
+
return { x, y, width, height };
|
|
104
|
+
}
|
|
105
|
+
function imageProps(value) {
|
|
106
|
+
let src = "";
|
|
107
|
+
let crop = { x: 0, y: 0, width: 1, height: 1 };
|
|
108
|
+
if (typeof value === "string") {
|
|
109
|
+
src = value;
|
|
110
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
111
|
+
const row = value;
|
|
112
|
+
if (typeof row.url !== "string") return null;
|
|
113
|
+
src = row.url;
|
|
114
|
+
if (row.crop != null) crop = readCrop(row.crop);
|
|
115
|
+
} else {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (!src) return null;
|
|
119
|
+
return {
|
|
120
|
+
src,
|
|
121
|
+
crop,
|
|
122
|
+
style: {
|
|
123
|
+
position: "absolute",
|
|
124
|
+
left: `${-crop.x / crop.width * 100}%`,
|
|
125
|
+
top: `${-crop.y / crop.height * 100}%`,
|
|
126
|
+
width: `${1 / crop.width * 100}%`,
|
|
127
|
+
height: `${1 / crop.height * 100}%`,
|
|
128
|
+
maxWidth: "none",
|
|
129
|
+
maxHeight: "none"
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
88
133
|
// Annotate the CommonJS export names for ESM import in node:
|
|
89
134
|
0 && (module.exports = {
|
|
90
135
|
getConfig,
|
|
91
136
|
getEntries,
|
|
92
137
|
getEntry,
|
|
93
138
|
getEntryByPreviewToken,
|
|
139
|
+
imageProps,
|
|
94
140
|
initDispatch
|
|
95
141
|
});
|
|
96
142
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Entry, DispatchConfig } from \"./client\";\nexport {\n initDispatch,\n getConfig,\n getEntries,\n getEntry,\n getEntryByPreviewToken,\n} from \"./client\";\n","export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAE7B,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,WAAW,YAAoB,SAAoC;AACvF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC;AAC9E,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,SAAS,YAAoB,MAAc,SAAyC;AACxG,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC,IAAI,mBAAmB,IAAI,CAAC;AAC1G,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAGA,eAAsB,uBAAuB,OAAsC;AACjF,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,QAAM,MAAM,GAAG,iBAAiB,sBAAsB,mBAAmB,MAAM,KAAK,CAAC,CAAC;AACtF,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Entry, DispatchConfig, ImageCrop } from \"./client\";\nexport {\n initDispatch,\n getConfig,\n getEntries,\n getEntry,\n getEntryByPreviewToken,\n imageProps,\n} from \"./client\";\n","export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\nexport type ImageCrop = { x: number; y: number; width: number; height: number };\n\nfunction clampCropSide(n: number, fallback: number) {\n return typeof n === \"number\" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;\n}\n\nfunction readCrop(value: unknown): ImageCrop {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { x: 0, y: 0, width: 1, height: 1 };\n }\n const row = value as Record<string, unknown>;\n let x = clampCropSide(row.x as number, 0);\n let y = clampCropSide(row.y as number, 0);\n let width = clampCropSide(row.width as number, 1);\n let height = clampCropSide(row.height as number, 1);\n width = Math.max(0.05, Math.min(width, 1 - x));\n height = Math.max(0.05, Math.min(height, 1 - y));\n return { x, y, width, height };\n}\n\n/** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */\nexport function imageProps(value: unknown): {\n src: string;\n crop: ImageCrop;\n style: {\n position: \"absolute\";\n left: string;\n top: string;\n width: string;\n height: string;\n maxWidth: \"none\";\n maxHeight: \"none\";\n };\n} | null {\n let src = \"\";\n let crop: ImageCrop = { x: 0, y: 0, width: 1, height: 1 };\n if (typeof value === \"string\") {\n src = value;\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const row = value as Record<string, unknown>;\n if (typeof row.url !== \"string\") return null;\n src = row.url;\n if (row.crop != null) crop = readCrop(row.crop);\n } else {\n return null;\n }\n if (!src) return null;\n return {\n src,\n crop,\n style: {\n position: \"absolute\",\n left: `${(-crop.x / crop.width) * 100}%`,\n top: `${(-crop.y / crop.height) * 100}%`,\n width: `${(1 / crop.width) * 100}%`,\n height: `${(1 / crop.height) * 100}%`,\n maxWidth: \"none\",\n maxHeight: \"none\",\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -55,11 +55,56 @@ async function getEntryByPreviewToken(token) {
|
|
|
55
55
|
}
|
|
56
56
|
return await res.json();
|
|
57
57
|
}
|
|
58
|
+
function clampCropSide(n, fallback) {
|
|
59
|
+
return typeof n === "number" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;
|
|
60
|
+
}
|
|
61
|
+
function readCrop(value) {
|
|
62
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
63
|
+
return { x: 0, y: 0, width: 1, height: 1 };
|
|
64
|
+
}
|
|
65
|
+
const row = value;
|
|
66
|
+
let x = clampCropSide(row.x, 0);
|
|
67
|
+
let y = clampCropSide(row.y, 0);
|
|
68
|
+
let width = clampCropSide(row.width, 1);
|
|
69
|
+
let height = clampCropSide(row.height, 1);
|
|
70
|
+
width = Math.max(0.05, Math.min(width, 1 - x));
|
|
71
|
+
height = Math.max(0.05, Math.min(height, 1 - y));
|
|
72
|
+
return { x, y, width, height };
|
|
73
|
+
}
|
|
74
|
+
function imageProps(value) {
|
|
75
|
+
let src = "";
|
|
76
|
+
let crop = { x: 0, y: 0, width: 1, height: 1 };
|
|
77
|
+
if (typeof value === "string") {
|
|
78
|
+
src = value;
|
|
79
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
80
|
+
const row = value;
|
|
81
|
+
if (typeof row.url !== "string") return null;
|
|
82
|
+
src = row.url;
|
|
83
|
+
if (row.crop != null) crop = readCrop(row.crop);
|
|
84
|
+
} else {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (!src) return null;
|
|
88
|
+
return {
|
|
89
|
+
src,
|
|
90
|
+
crop,
|
|
91
|
+
style: {
|
|
92
|
+
position: "absolute",
|
|
93
|
+
left: `${-crop.x / crop.width * 100}%`,
|
|
94
|
+
top: `${-crop.y / crop.height * 100}%`,
|
|
95
|
+
width: `${1 / crop.width * 100}%`,
|
|
96
|
+
height: `${1 / crop.height * 100}%`,
|
|
97
|
+
maxWidth: "none",
|
|
98
|
+
maxHeight: "none"
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
58
102
|
export {
|
|
59
103
|
getConfig,
|
|
60
104
|
getEntries,
|
|
61
105
|
getEntry,
|
|
62
106
|
getEntryByPreviewToken,
|
|
107
|
+
imageProps,
|
|
63
108
|
initDispatch
|
|
64
109
|
};
|
|
65
110
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n"],"mappings":";AAcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAE7B,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,WAAW,YAAoB,SAAoC;AACvF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC;AAC9E,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,SAAS,YAAoB,MAAc,SAAyC;AACxG,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC,IAAI,mBAAmB,IAAI,CAAC;AAC1G,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAGA,eAAsB,uBAAuB,OAAsC;AACjF,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,QAAM,MAAM,GAAG,iBAAiB,sBAAsB,mBAAmB,MAAM,KAAK,CAAC,CAAC;AACtF,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\nexport type ImageCrop = { x: number; y: number; width: number; height: number };\n\nfunction clampCropSide(n: number, fallback: number) {\n return typeof n === \"number\" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;\n}\n\nfunction readCrop(value: unknown): ImageCrop {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { x: 0, y: 0, width: 1, height: 1 };\n }\n const row = value as Record<string, unknown>;\n let x = clampCropSide(row.x as number, 0);\n let y = clampCropSide(row.y as number, 0);\n let width = clampCropSide(row.width as number, 1);\n let height = clampCropSide(row.height as number, 1);\n width = Math.max(0.05, Math.min(width, 1 - x));\n height = Math.max(0.05, Math.min(height, 1 - y));\n return { x, y, width, height };\n}\n\n/** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */\nexport function imageProps(value: unknown): {\n src: string;\n crop: ImageCrop;\n style: {\n position: \"absolute\";\n left: string;\n top: string;\n width: string;\n height: string;\n maxWidth: \"none\";\n maxHeight: \"none\";\n };\n} | null {\n let src = \"\";\n let crop: ImageCrop = { x: 0, y: 0, width: 1, height: 1 };\n if (typeof value === \"string\") {\n src = value;\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const row = value as Record<string, unknown>;\n if (typeof row.url !== \"string\") return null;\n src = row.url;\n if (row.crop != null) crop = readCrop(row.crop);\n } else {\n return null;\n }\n if (!src) return null;\n return {\n src,\n crop,\n style: {\n position: \"absolute\",\n left: `${(-crop.x / crop.width) * 100}%`,\n top: `${(-crop.y / crop.height) * 100}%`,\n width: `${(1 / crop.width) * 100}%`,\n height: `${(1 / crop.height) * 100}%`,\n maxWidth: \"none\",\n maxHeight: \"none\",\n },\n };\n}\n"],"mappings":";AAcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAE7B,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,WAAW,YAAoB,SAAoC;AACvF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC;AAC9E,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,SAAS,YAAoB,MAAc,SAAyC;AACxG,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC,IAAI,mBAAmB,IAAI,CAAC;AAC1G,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAGA,eAAsB,uBAAuB,OAAsC;AACjF,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,QAAM,MAAM,GAAG,iBAAiB,sBAAsB,mBAAmB,MAAM,KAAK,CAAC,CAAC;AACtF,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAIA,SAAS,cAAc,GAAW,UAAkB;AAClD,SAAO,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC,IAAI;AACrF;AAEA,SAAS,SAAS,OAA2B;AAC3C,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AAC/D,WAAO,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,GAAG,QAAQ,EAAE;AAAA,EAC3C;AACA,QAAM,MAAM;AACZ,MAAI,IAAI,cAAc,IAAI,GAAa,CAAC;AACxC,MAAI,IAAI,cAAc,IAAI,GAAa,CAAC;AACxC,MAAI,QAAQ,cAAc,IAAI,OAAiB,CAAC;AAChD,MAAI,SAAS,cAAc,IAAI,QAAkB,CAAC;AAClD,UAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,OAAO,IAAI,CAAC,CAAC;AAC7C,WAAS,KAAK,IAAI,MAAM,KAAK,IAAI,QAAQ,IAAI,CAAC,CAAC;AAC/C,SAAO,EAAE,GAAG,GAAG,OAAO,OAAO;AAC/B;AAGO,SAAS,WAAW,OAYlB;AACP,MAAI,MAAM;AACV,MAAI,OAAkB,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,GAAG,QAAQ,EAAE;AACxD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM;AAAA,EACR,WAAW,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACtE,UAAM,MAAM;AACZ,QAAI,OAAO,IAAI,QAAQ,SAAU,QAAO;AACxC,UAAM,IAAI;AACV,QAAI,IAAI,QAAQ,KAAM,QAAO,SAAS,IAAI,IAAI;AAAA,EAChD,OAAO;AACL,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,OAAO;AAAA,MACL,UAAU;AAAA,MACV,MAAM,GAAI,CAAC,KAAK,IAAI,KAAK,QAAS,GAAG;AAAA,MACrC,KAAK,GAAI,CAAC,KAAK,IAAI,KAAK,SAAU,GAAG;AAAA,MACrC,OAAO,GAAI,IAAI,KAAK,QAAS,GAAG;AAAA,MAChC,QAAQ,GAAI,IAAI,KAAK,SAAU,GAAG;AAAA,MAClC,UAAU;AAAA,MACV,WAAW;AAAA,IACb;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dispatchcms/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Fetch published entries from a Dispatch CMS in Next.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"test": "tsx test/client.cache.test.ts && tsx test/image-props.test.ts",
|
|
18
|
+
"test:manual": "tsx test/getPage.test.ts"
|
|
19
|
+
},
|
|
15
20
|
"devDependencies": {
|
|
16
21
|
"@types/node": "^22.20.1",
|
|
17
22
|
"next": "^16.3.0",
|
|
@@ -31,10 +36,5 @@
|
|
|
31
36
|
"license": "MIT",
|
|
32
37
|
"peerDependencies": {
|
|
33
38
|
"next": ">=16.0.0"
|
|
34
|
-
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build": "tsup",
|
|
37
|
-
"test": "tsx test/client.cache.test.ts",
|
|
38
|
-
"test:manual": "tsx test/getPage.test.ts"
|
|
39
39
|
}
|
|
40
|
-
}
|
|
40
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -79,3 +79,65 @@ export async function getEntryByPreviewToken(token: string): Promise<Entry | nul
|
|
|
79
79
|
}
|
|
80
80
|
return (await res.json()) as Entry;
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
export type ImageCrop = { x: number; y: number; width: number; height: number };
|
|
84
|
+
|
|
85
|
+
function clampCropSide(n: number, fallback: number) {
|
|
86
|
+
return typeof n === "number" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readCrop(value: unknown): ImageCrop {
|
|
90
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
91
|
+
return { x: 0, y: 0, width: 1, height: 1 };
|
|
92
|
+
}
|
|
93
|
+
const row = value as Record<string, unknown>;
|
|
94
|
+
let x = clampCropSide(row.x as number, 0);
|
|
95
|
+
let y = clampCropSide(row.y as number, 0);
|
|
96
|
+
let width = clampCropSide(row.width as number, 1);
|
|
97
|
+
let height = clampCropSide(row.height as number, 1);
|
|
98
|
+
width = Math.max(0.05, Math.min(width, 1 - x));
|
|
99
|
+
height = Math.max(0.05, Math.min(height, 1 - y));
|
|
100
|
+
return { x, y, width, height };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */
|
|
104
|
+
export function imageProps(value: unknown): {
|
|
105
|
+
src: string;
|
|
106
|
+
crop: ImageCrop;
|
|
107
|
+
style: {
|
|
108
|
+
position: "absolute";
|
|
109
|
+
left: string;
|
|
110
|
+
top: string;
|
|
111
|
+
width: string;
|
|
112
|
+
height: string;
|
|
113
|
+
maxWidth: "none";
|
|
114
|
+
maxHeight: "none";
|
|
115
|
+
};
|
|
116
|
+
} | null {
|
|
117
|
+
let src = "";
|
|
118
|
+
let crop: ImageCrop = { x: 0, y: 0, width: 1, height: 1 };
|
|
119
|
+
if (typeof value === "string") {
|
|
120
|
+
src = value;
|
|
121
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
122
|
+
const row = value as Record<string, unknown>;
|
|
123
|
+
if (typeof row.url !== "string") return null;
|
|
124
|
+
src = row.url;
|
|
125
|
+
if (row.crop != null) crop = readCrop(row.crop);
|
|
126
|
+
} else {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (!src) return null;
|
|
130
|
+
return {
|
|
131
|
+
src,
|
|
132
|
+
crop,
|
|
133
|
+
style: {
|
|
134
|
+
position: "absolute",
|
|
135
|
+
left: `${(-crop.x / crop.width) * 100}%`,
|
|
136
|
+
top: `${(-crop.y / crop.height) * 100}%`,
|
|
137
|
+
width: `${(1 / crop.width) * 100}%`,
|
|
138
|
+
height: `${(1 / crop.height) * 100}%`,
|
|
139
|
+
maxWidth: "none",
|
|
140
|
+
maxHeight: "none",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { imageProps } from "../src/index";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
const url = "https://cdn.example/a.jpg";
|
|
5
|
+
|
|
6
|
+
const plain = imageProps(url);
|
|
7
|
+
assert(plain?.src === url && plain.crop.width === 1 && plain.crop.height === 1, "plain url");
|
|
8
|
+
assert(plain?.style.left === "0%" && plain.style.top === "0%", "full crop origin");
|
|
9
|
+
assert(plain?.style.width === "100%" && plain.style.height === "100%", "full crop size");
|
|
10
|
+
|
|
11
|
+
const cropped = imageProps({ url, crop: { x: 0.2, y: 0.1, width: 0.5, height: 0.4 } });
|
|
12
|
+
assert(cropped?.src === url, "crop src");
|
|
13
|
+
assert(cropped?.crop.x === 0.2 && cropped.crop.width === 0.5, "crop rect");
|
|
14
|
+
assert(cropped?.style.left === "-40%" && cropped.style.top === "-25%", "crop offset");
|
|
15
|
+
assert(cropped?.style.width === "200%" && cropped.style.height === "250%", "crop scale");
|
|
16
|
+
|
|
17
|
+
const legacyFocus = imageProps({ url, focus: { x: 0.2, y: 0.8 } });
|
|
18
|
+
assert(legacyFocus?.src === url && legacyFocus.crop.width === 1, "legacy focus becomes full frame");
|
|
19
|
+
|
|
20
|
+
assert(imageProps(null) === null && imageProps("") === null && imageProps({}) === null, "bad input");
|
|
21
|
+
|
|
22
|
+
console.log("image-props: ok");
|