@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 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 drags a point, then `{ url, focus }`. `imageProps` reads both. It only changes which part of the photo stays in view when your frame crops it.
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 ? <img src={photo.src} alt="" className="h-full w-full" style={photo.style} /> : null
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
- objectFit: "cover";
29
- objectPosition: string;
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
- objectFit: "cover";
29
- objectPosition: string;
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 x = 0.5;
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.focus && typeof row.focus === "object" && !Array.isArray(row.focus)) {
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 { src, style: { objectFit: "cover", objectPosition: `${x * 100}% ${y * 100}%` } };
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// ponytail: percentage object-position. Measure the image against the frame if a subject still clips.\nexport function imageProps(value: unknown): {\n src: string;\n style: { objectFit: \"cover\"; objectPosition: string };\n} | null {\n let src = \"\";\n let x = 0.5;\n let y = 0.5;\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.focus && typeof row.focus === \"object\" && !Array.isArray(row.focus)) {\n const focus = row.focus as Record<string, unknown>;\n if (typeof focus.x === \"number\" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));\n if (typeof focus.y === \"number\" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));\n }\n } else {\n return null;\n }\n if (!src) return null;\n return { src, style: { objectFit: \"cover\", objectPosition: `${x * 100}% ${y * 100}%` } };\n}\n"],"mappings":"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","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,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 x = 0.5;
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.focus && typeof row.focus === "object" && !Array.isArray(row.focus)) {
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 { src, style: { objectFit: "cover", objectPosition: `${x * 100}% ${y * 100}%` } };
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,
@@ -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// ponytail: percentage object-position. Measure the image against the frame if a subject still clips.\nexport function imageProps(value: unknown): {\n src: string;\n style: { objectFit: \"cover\"; objectPosition: string };\n} | null {\n let src = \"\";\n let x = 0.5;\n let y = 0.5;\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.focus && typeof row.focus === \"object\" && !Array.isArray(row.focus)) {\n const focus = row.focus as Record<string, unknown>;\n if (typeof focus.x === \"number\" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));\n if (typeof focus.y === \"number\" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));\n }\n } else {\n return null;\n }\n if (!src) return null;\n return { src, style: { objectFit: \"cover\", objectPosition: `${x * 100}% ${y * 100}%` } };\n}\n"],"mappings":"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","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.8",
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",
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
- // ponytail: percentage object-position. Measure the image against the frame if a subject still clips.
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
- style: { objectFit: "cover"; objectPosition: 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
+ };
87
116
  } | null {
88
117
  let src = "";
89
- let x = 0.5;
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.focus && typeof row.focus === "object" && !Array.isArray(row.focus)) {
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 { src, style: { objectFit: "cover", objectPosition: `${x * 100}% ${y * 100}%` } };
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
@@ -1,4 +1,4 @@
1
- export type { Entry, DispatchConfig } from "./client";
1
+ export type { Entry, DispatchConfig, ImageCrop } from "./client";
2
2
  export {
3
3
  initDispatch,
4
4
  getConfig,
@@ -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.style.objectFit === "cover" && plain.style.objectPosition === "50% 50%", "string url");
10
- const focused = imageProps({ url, focus: { x: 0.2, y: 0.8 } });
11
- assert(focused?.src === url && focused.style.objectPosition === "20% 80%", "focus object");
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
- console.log("image props ok");
21
+
22
+ console.log("image-props: ok");