@dispatchcms/next 0.0.8 → 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 +6 -2
- package/dist/index.d.mts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +31 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +46 -10
- package/src/index.ts +1 -1
- package/test/image-props.test.ts +17 -8
package/README.md
CHANGED
|
@@ -151,11 +151,15 @@ In the CMS, set **Site URL** in Site settings to your site’s base URL (e.g. `h
|
|
|
151
151
|
|
|
152
152
|
## Images
|
|
153
153
|
|
|
154
|
-
An image field is a URL until an editor
|
|
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
155
|
|
|
156
156
|
```tsx
|
|
157
157
|
import { imageProps } from "@dispatchcms/next";
|
|
158
158
|
|
|
159
159
|
const photo = imageProps(entry.data.hero);
|
|
160
|
-
photo ?
|
|
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;
|
|
161
165
|
```
|
package/dist/index.d.mts
CHANGED
|
@@ -22,12 +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. */
|
|
25
32
|
declare function imageProps(value: unknown): {
|
|
26
33
|
src: string;
|
|
34
|
+
crop: ImageCrop;
|
|
27
35
|
style: {
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
position: "absolute";
|
|
37
|
+
left: string;
|
|
38
|
+
top: string;
|
|
39
|
+
width: string;
|
|
40
|
+
height: string;
|
|
41
|
+
maxWidth: "none";
|
|
42
|
+
maxHeight: "none";
|
|
30
43
|
};
|
|
31
44
|
} | null;
|
|
32
45
|
|
|
33
|
-
export { type DispatchConfig, type Entry, getConfig, getEntries, getEntry, getEntryByPreviewToken, imageProps, initDispatch };
|
|
46
|
+
export { type DispatchConfig, type Entry, type ImageCrop, getConfig, getEntries, getEntry, getEntryByPreviewToken, imageProps, initDispatch };
|
package/dist/index.d.ts
CHANGED
|
@@ -22,12 +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. */
|
|
25
32
|
declare function imageProps(value: unknown): {
|
|
26
33
|
src: string;
|
|
34
|
+
crop: ImageCrop;
|
|
27
35
|
style: {
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
position: "absolute";
|
|
37
|
+
left: string;
|
|
38
|
+
top: string;
|
|
39
|
+
width: string;
|
|
40
|
+
height: string;
|
|
41
|
+
maxWidth: "none";
|
|
42
|
+
maxHeight: "none";
|
|
30
43
|
};
|
|
31
44
|
} | null;
|
|
32
45
|
|
|
33
|
-
export { type DispatchConfig, type Entry, getConfig, getEntries, getEntry, getEntryByPreviewToken, imageProps, initDispatch };
|
|
46
|
+
export { type DispatchConfig, type Entry, type ImageCrop, getConfig, getEntries, getEntry, getEntryByPreviewToken, imageProps, initDispatch };
|
package/dist/index.js
CHANGED
|
@@ -86,26 +86,49 @@ async function getEntryByPreviewToken(token) {
|
|
|
86
86
|
}
|
|
87
87
|
return await res.json();
|
|
88
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
|
+
}
|
|
89
105
|
function imageProps(value) {
|
|
90
106
|
let src = "";
|
|
91
|
-
let
|
|
92
|
-
let y = 0.5;
|
|
107
|
+
let crop = { x: 0, y: 0, width: 1, height: 1 };
|
|
93
108
|
if (typeof value === "string") {
|
|
94
109
|
src = value;
|
|
95
110
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
96
111
|
const row = value;
|
|
97
112
|
if (typeof row.url !== "string") return null;
|
|
98
113
|
src = row.url;
|
|
99
|
-
if (row.
|
|
100
|
-
const focus = row.focus;
|
|
101
|
-
if (typeof focus.x === "number" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));
|
|
102
|
-
if (typeof focus.y === "number" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));
|
|
103
|
-
}
|
|
114
|
+
if (row.crop != null) crop = readCrop(row.crop);
|
|
104
115
|
} else {
|
|
105
116
|
return null;
|
|
106
117
|
}
|
|
107
118
|
if (!src) return null;
|
|
108
|
-
return {
|
|
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
|
+
};
|
|
109
132
|
}
|
|
110
133
|
// Annotate the CommonJS export names for ESM import in node:
|
|
111
134
|
0 && (module.exports = {
|
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 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\n
|
|
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,26 +55,49 @@ 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
|
+
}
|
|
58
74
|
function imageProps(value) {
|
|
59
75
|
let src = "";
|
|
60
|
-
let
|
|
61
|
-
let y = 0.5;
|
|
76
|
+
let crop = { x: 0, y: 0, width: 1, height: 1 };
|
|
62
77
|
if (typeof value === "string") {
|
|
63
78
|
src = value;
|
|
64
79
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
65
80
|
const row = value;
|
|
66
81
|
if (typeof row.url !== "string") return null;
|
|
67
82
|
src = row.url;
|
|
68
|
-
if (row.
|
|
69
|
-
const focus = row.focus;
|
|
70
|
-
if (typeof focus.x === "number" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));
|
|
71
|
-
if (typeof focus.y === "number" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));
|
|
72
|
-
}
|
|
83
|
+
if (row.crop != null) crop = readCrop(row.crop);
|
|
73
84
|
} else {
|
|
74
85
|
return null;
|
|
75
86
|
}
|
|
76
87
|
if (!src) return null;
|
|
77
|
-
return {
|
|
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
|
+
};
|
|
78
101
|
}
|
|
79
102
|
export {
|
|
80
103
|
getConfig,
|
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\n
|
|
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":"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","names":[]}
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -80,28 +80,64 @@ export async function getEntryByPreviewToken(token: string): Promise<Entry | nul
|
|
|
80
80
|
return (await res.json()) as Entry;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
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. */
|
|
84
104
|
export function imageProps(value: unknown): {
|
|
85
105
|
src: string;
|
|
86
|
-
|
|
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
|
+
};
|
|
87
116
|
} | null {
|
|
88
117
|
let src = "";
|
|
89
|
-
let
|
|
90
|
-
let y = 0.5;
|
|
118
|
+
let crop: ImageCrop = { x: 0, y: 0, width: 1, height: 1 };
|
|
91
119
|
if (typeof value === "string") {
|
|
92
120
|
src = value;
|
|
93
121
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
94
122
|
const row = value as Record<string, unknown>;
|
|
95
123
|
if (typeof row.url !== "string") return null;
|
|
96
124
|
src = row.url;
|
|
97
|
-
if (row.
|
|
98
|
-
const focus = row.focus as Record<string, unknown>;
|
|
99
|
-
if (typeof focus.x === "number" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));
|
|
100
|
-
if (typeof focus.y === "number" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));
|
|
101
|
-
}
|
|
125
|
+
if (row.crop != null) crop = readCrop(row.crop);
|
|
102
126
|
} else {
|
|
103
127
|
return null;
|
|
104
128
|
}
|
|
105
129
|
if (!src) return null;
|
|
106
|
-
return {
|
|
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
|
+
};
|
|
107
143
|
}
|
package/src/index.ts
CHANGED
package/test/image-props.test.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { imageProps } from "../src/index";
|
|
2
|
-
|
|
3
|
-
function assert(condition: boolean, message: string): void {
|
|
4
|
-
if (!condition) throw new Error(`Assertion failed: ${message}`);
|
|
5
|
-
}
|
|
2
|
+
import assert from "node:assert/strict";
|
|
6
3
|
|
|
7
4
|
const url = "https://cdn.example/a.jpg";
|
|
5
|
+
|
|
8
6
|
const plain = imageProps(url);
|
|
9
|
-
assert(plain?.src === url && plain.
|
|
10
|
-
|
|
11
|
-
assert(
|
|
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
|
+
|
|
12
20
|
assert(imageProps(null) === null && imageProps("") === null && imageProps({}) === null, "bad input");
|
|
13
|
-
|
|
21
|
+
|
|
22
|
+
console.log("image-props: ok");
|