@grizzlee/react 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +54 -2
- package/embed.cjs +117 -0
- package/embed.cjs.map +7 -0
- package/embed.js +94 -0
- package/embed.js.map +7 -0
- package/index.cjs +151 -0
- package/index.cjs.map +7 -0
- package/index.js +135 -0
- package/index.js.map +7 -0
- package/package.json +64 -4
- package/types/embed.d.cts +6 -0
- package/types/embed.d.ts +6 -0
- package/types/host.d.cts +15 -0
- package/types/host.d.ts +15 -0
- package/types/index.d.cts +9 -0
- package/types/index.d.ts +9 -0
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Grizzlee embed hosts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@grizzlee/react` exports `GrizzleeEmbed`, `mountGrizzleeEmbed`, `GrizzleeEmbedProps` and `GrizzleeEmbedOptions`. React 18 or later is a peer dependency. Install React in a component host. The peer is optional so a host using only `@grizzlee/react/embed` does not need React. There are no other runtime dependencies.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { GrizzleeEmbed } from "@grizzlee/react";
|
|
7
|
+
|
|
8
|
+
async function newSession() {
|
|
9
|
+
const response = await fetch("/partner/embed-session", { method: "POST" });
|
|
10
|
+
if (!response.ok) throw new Error("Session creation failed");
|
|
11
|
+
const { url } = await response.json();
|
|
12
|
+
return url as string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Get startUrl from newSession before rendering.
|
|
16
|
+
<GrizzleeEmbed url={startUrl} onSessionExpired={newSession} />;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`url` is an absolute HTTP or HTTPS URL minted by your server. Optional props are `onSessionExpired`, `onReady`, `title`, `className`, `style` and `minHeight`. `title` defaults to `"Partner dashboard"`. `minHeight` defaults to 320 CSS pixels and stays between 120 and 10,000. Resize messages must contain a positive safe integer. The iframe height stays between the minimum and 10,000 pixels. These bounds override `style.height` and `style.minHeight`.
|
|
20
|
+
|
|
21
|
+
`onReady` runs when the partner summary loads, including after renewal. `onSessionExpired` can return a new URL directly or through a promise, or return nothing if you handle recovery yourself. Catch request errors in that callback to display them in your app. A rejected promise leaves the expired dashboard visible. Only one renewal runs at a time. Changing `url` loads a new session, and unmounting removes the listener and ignores pending renewal results.
|
|
22
|
+
|
|
23
|
+
The component renders during server-side rendering without accessing `window` or `document`. It attaches its message listener after mounting. In Next.js App Router, render it from a client component when passing callbacks.
|
|
24
|
+
|
|
25
|
+
For a host without React:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { mountGrizzleeEmbed } from "@grizzlee/react/embed";
|
|
29
|
+
|
|
30
|
+
const dashboard = mountGrizzleeEmbed(document.getElementById("dashboard")!, {
|
|
31
|
+
url: startUrl,
|
|
32
|
+
onSessionExpired: newSession,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// When the host view closes:
|
|
36
|
+
dashboard.destroy();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The helper accepts the same options except `style`. It appends an iframe and returns `{ destroy() }`. Both hosts use `allow="clipboard-write"`, `referrerPolicy="no-referrer"` and `loading="lazy"`. They accept messages only from the current URL's origin and their own iframe window. Neither sets `sandbox`. If you add it to a plain iframe, include `allow-scripts allow-same-origin allow-downloads`. Without `allow-same-origin`, the exchange sends `Origin: null` and fails. Without `allow-downloads`, QR downloads fail.
|
|
40
|
+
|
|
41
|
+
Create sessions on your backend with [@grizzlee/sdk](https://www.npmjs.com/package/@grizzlee/sdk) and pass the returned URL to the browser host. Browser hosts never need the server key or schema dependencies.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install @grizzlee/react
|
|
47
|
+
pnpm add @grizzlee/react
|
|
48
|
+
yarn add @grizzlee/react
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Node >= 22. Both entry points support ESM and CommonJS with source maps and matching TypeScript declarations.
|
|
52
|
+
|
|
53
|
+
## Versioning
|
|
54
|
+
|
|
55
|
+
The package follows semver. While the version is 0.x, a minor bump may include breaking changes. See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
package/embed.cjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/react/src/embed.ts
|
|
21
|
+
var embed_exports = {};
|
|
22
|
+
__export(embed_exports, {
|
|
23
|
+
mountGrizzleeEmbed: () => mountGrizzleeEmbed
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(embed_exports);
|
|
26
|
+
|
|
27
|
+
// packages/react/src/host.ts
|
|
28
|
+
var MAX_HEIGHT = 1e4;
|
|
29
|
+
function minimumHeight(height = 320) {
|
|
30
|
+
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
31
|
+
}
|
|
32
|
+
function embedUrl(url) {
|
|
33
|
+
const parsed = new URL(url);
|
|
34
|
+
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
37
|
+
);
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function listenToEmbed(frame, options) {
|
|
41
|
+
const hostWindow = frame.ownerDocument.defaultView;
|
|
42
|
+
if (!hostWindow)
|
|
43
|
+
throw new Error("The iframe must belong to a browser document");
|
|
44
|
+
let origin = embedUrl(options().url).origin;
|
|
45
|
+
let active = true;
|
|
46
|
+
let renewing = false;
|
|
47
|
+
const receive = (event) => {
|
|
48
|
+
if (!active || event.origin !== origin || !frame.contentWindow || event.source !== frame.contentWindow)
|
|
49
|
+
return;
|
|
50
|
+
const message = event.data;
|
|
51
|
+
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
52
|
+
switch (message.type) {
|
|
53
|
+
case "grizzlee:ready":
|
|
54
|
+
options().onReady?.();
|
|
55
|
+
break;
|
|
56
|
+
case "grizzlee:resize": {
|
|
57
|
+
if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
|
|
58
|
+
const height = Math.min(
|
|
59
|
+
MAX_HEIGHT,
|
|
60
|
+
Math.max(minimumHeight(options().minHeight), message.height)
|
|
61
|
+
);
|
|
62
|
+
frame.style.height = `${height}px`;
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "grizzlee:session_expired": {
|
|
67
|
+
const renew = options().onSessionExpired;
|
|
68
|
+
if (!renew || renewing) return;
|
|
69
|
+
renewing = true;
|
|
70
|
+
void (async () => {
|
|
71
|
+
try {
|
|
72
|
+
const url = await renew();
|
|
73
|
+
if (!active || !url) return;
|
|
74
|
+
const next = embedUrl(url);
|
|
75
|
+
origin = next.origin;
|
|
76
|
+
frame.style.height = `${minimumHeight(options().minHeight)}px`;
|
|
77
|
+
frame.src = next.href;
|
|
78
|
+
} catch {
|
|
79
|
+
} finally {
|
|
80
|
+
renewing = false;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
hostWindow.addEventListener("message", receive);
|
|
88
|
+
return () => {
|
|
89
|
+
active = false;
|
|
90
|
+
hostWindow.removeEventListener("message", receive);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// packages/react/src/embed.ts
|
|
95
|
+
function mountGrizzleeEmbed(container, options) {
|
|
96
|
+
const url = embedUrl(options.url);
|
|
97
|
+
const frame = container.ownerDocument.createElement("iframe");
|
|
98
|
+
frame.title = options.title ?? "Partner dashboard";
|
|
99
|
+
if (options.className) frame.className = options.className;
|
|
100
|
+
frame.allow = "clipboard-write";
|
|
101
|
+
frame.referrerPolicy = "no-referrer";
|
|
102
|
+
frame.loading = "lazy";
|
|
103
|
+
frame.style.width = "100%";
|
|
104
|
+
frame.style.border = "0";
|
|
105
|
+
frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;
|
|
106
|
+
frame.style.height = frame.style.minHeight;
|
|
107
|
+
const stop = listenToEmbed(frame, () => options);
|
|
108
|
+
frame.src = url.href;
|
|
109
|
+
container.appendChild(frame);
|
|
110
|
+
return {
|
|
111
|
+
destroy() {
|
|
112
|
+
stop();
|
|
113
|
+
frame.remove();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=embed.cjs.map
|
package/embed.cjs.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/embed.ts", "../src/host.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n", "export interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\nexport function embedUrl(url: string): URL {\n const parsed = new URL(url);\n if (\n ![\"https:\", \"http:\"].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password\n )\n throw new Error(\n \"Use an absolute HTTP or HTTPS embed URL without credentials\",\n );\n return parsed;\n}\n\n/** Both hosts use this listener so origin checks and renewal behave the same way. */\nexport function listenToEmbed(\n frame: HTMLIFrameElement,\n options: () => GrizzleeEmbedOptions,\n): () => void {\n const hostWindow = frame.ownerDocument.defaultView;\n if (!hostWindow)\n throw new Error(\"The iframe must belong to a browser document\");\n let origin = embedUrl(options().url).origin;\n let active = true;\n let renewing = false;\n\n const receive = (event: MessageEvent<unknown>) => {\n if (\n !active ||\n event.origin !== origin ||\n !frame.contentWindow ||\n event.source !== frame.contentWindow\n )\n return;\n const message = event.data;\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n\n switch (message.type) {\n case \"grizzlee:ready\":\n options().onReady?.();\n break;\n case \"grizzlee:resize\": {\n if (\n \"height\" in message &&\n typeof message.height === \"number\" &&\n Number.isSafeInteger(message.height) &&\n message.height > 0\n ) {\n const height = Math.min(\n MAX_HEIGHT,\n Math.max(minimumHeight(options().minHeight), message.height),\n );\n frame.style.height = `${height}px`;\n }\n break;\n }\n case \"grizzlee:session_expired\": {\n const renew = options().onSessionExpired;\n if (!renew || renewing) return;\n renewing = true;\n void (async () => {\n try {\n const url = await renew();\n if (!active || !url) return;\n const next = embedUrl(url);\n origin = next.origin;\n frame.style.height = `${minimumHeight(options().minHeight)}px`;\n frame.src = next.href;\n } catch {\n // Leave the expired dashboard in place. The merchant handles errors in its callback.\n } finally {\n renewing = false;\n }\n })();\n break;\n }\n }\n };\n\n hostWindow.addEventListener(\"message\", receive);\n return () => {\n active = false;\n hostWindow.removeEventListener(\"message\", receive);\n };\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACYA,IAAM,aAAa;AAEZ,SAAS,cAAc,SAAS,KAAa;AAClD,SAAO,OAAO,SAAS,MAAM,IACzB,KAAK,IAAI,YAAY,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,CAAC,CAAC,IACrD;AACN;AAEO,SAAS,SAAS,KAAkB;AACzC,QAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,MACE,CAAC,CAAC,UAAU,OAAO,EAAE,SAAS,OAAO,QAAQ,KAC7C,OAAO,YACP,OAAO;AAEP,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AACF,SAAO;AACT;AAGO,SAAS,cACd,OACA,SACY;AACZ,QAAM,aAAa,MAAM,cAAc;AACvC,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,8CAA8C;AAChE,MAAI,SAAS,SAAS,QAAQ,EAAE,GAAG,EAAE;AACrC,MAAI,SAAS;AACb,MAAI,WAAW;AAEf,QAAM,UAAU,CAAC,UAAiC;AAChD,QACE,CAAC,UACD,MAAM,WAAW,UACjB,CAAC,MAAM,iBACP,MAAM,WAAW,MAAM;AAEvB;AACF,UAAM,UAAU,MAAM;AACtB,QAAI,CAAC,WAAW,OAAO,YAAY,YAAY,EAAE,UAAU,SAAU;AAErE,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,gBAAQ,EAAE,UAAU;AACpB;AAAA,MACF,KAAK,mBAAmB;AACtB,YACE,YAAY,WACZ,OAAO,QAAQ,WAAW,YAC1B,OAAO,cAAc,QAAQ,MAAM,KACnC,QAAQ,SAAS,GACjB;AACA,gBAAM,SAAS,KAAK;AAAA,YAClB;AAAA,YACA,KAAK,IAAI,cAAc,QAAQ,EAAE,SAAS,GAAG,QAAQ,MAAM;AAAA,UAC7D;AACA,gBAAM,MAAM,SAAS,GAAG,MAAM;AAAA,QAChC;AACA;AAAA,MACF;AAAA,MACA,KAAK,4BAA4B;AAC/B,cAAM,QAAQ,QAAQ,EAAE;AACxB,YAAI,CAAC,SAAS,SAAU;AACxB,mBAAW;AACX,cAAM,YAAY;AAChB,cAAI;AACF,kBAAM,MAAM,MAAM,MAAM;AACxB,gBAAI,CAAC,UAAU,CAAC,IAAK;AACrB,kBAAM,OAAO,SAAS,GAAG;AACzB,qBAAS,KAAK;AACd,kBAAM,MAAM,SAAS,GAAG,cAAc,QAAQ,EAAE,SAAS,CAAC;AAC1D,kBAAM,MAAM,KAAK;AAAA,UACnB,QAAQ;AAAA,UAER,UAAE;AACA,uBAAW;AAAA,UACb;AAAA,QACF,GAAG;AACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,aAAW,iBAAiB,WAAW,OAAO;AAC9C,SAAO,MAAM;AACX,aAAS;AACT,eAAW,oBAAoB,WAAW,OAAO;AAAA,EACnD;AACF;;;AD7FO,SAAS,mBACd,WACA,SACqB;AACrB,QAAM,MAAM,SAAS,QAAQ,GAAG;AAChC,QAAM,QAAQ,UAAU,cAAc,cAAc,QAAQ;AAC5D,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI,QAAQ,UAAW,OAAM,YAAY,QAAQ;AACjD,QAAM,QAAQ;AACd,QAAM,iBAAiB;AACvB,QAAM,UAAU;AAChB,QAAM,MAAM,QAAQ;AACpB,QAAM,MAAM,SAAS;AACrB,QAAM,MAAM,YAAY,GAAG,cAAc,QAAQ,SAAS,CAAC;AAC3D,QAAM,MAAM,SAAS,MAAM,MAAM;AACjC,QAAM,OAAO,cAAc,OAAO,MAAM,OAAO;AAC/C,QAAM,MAAM,IAAI;AAChB,YAAU,YAAY,KAAK;AAC3B,SAAO;AAAA,IACL,UAAU;AACR,WAAK;AACL,YAAM,OAAO;AAAA,IACf;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/embed.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// packages/react/src/host.ts
|
|
2
|
+
var MAX_HEIGHT = 1e4;
|
|
3
|
+
function minimumHeight(height = 320) {
|
|
4
|
+
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
5
|
+
}
|
|
6
|
+
function embedUrl(url) {
|
|
7
|
+
const parsed = new URL(url);
|
|
8
|
+
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
9
|
+
throw new Error(
|
|
10
|
+
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
11
|
+
);
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
function listenToEmbed(frame, options) {
|
|
15
|
+
const hostWindow = frame.ownerDocument.defaultView;
|
|
16
|
+
if (!hostWindow)
|
|
17
|
+
throw new Error("The iframe must belong to a browser document");
|
|
18
|
+
let origin = embedUrl(options().url).origin;
|
|
19
|
+
let active = true;
|
|
20
|
+
let renewing = false;
|
|
21
|
+
const receive = (event) => {
|
|
22
|
+
if (!active || event.origin !== origin || !frame.contentWindow || event.source !== frame.contentWindow)
|
|
23
|
+
return;
|
|
24
|
+
const message = event.data;
|
|
25
|
+
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
26
|
+
switch (message.type) {
|
|
27
|
+
case "grizzlee:ready":
|
|
28
|
+
options().onReady?.();
|
|
29
|
+
break;
|
|
30
|
+
case "grizzlee:resize": {
|
|
31
|
+
if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
|
|
32
|
+
const height = Math.min(
|
|
33
|
+
MAX_HEIGHT,
|
|
34
|
+
Math.max(minimumHeight(options().minHeight), message.height)
|
|
35
|
+
);
|
|
36
|
+
frame.style.height = `${height}px`;
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "grizzlee:session_expired": {
|
|
41
|
+
const renew = options().onSessionExpired;
|
|
42
|
+
if (!renew || renewing) return;
|
|
43
|
+
renewing = true;
|
|
44
|
+
void (async () => {
|
|
45
|
+
try {
|
|
46
|
+
const url = await renew();
|
|
47
|
+
if (!active || !url) return;
|
|
48
|
+
const next = embedUrl(url);
|
|
49
|
+
origin = next.origin;
|
|
50
|
+
frame.style.height = `${minimumHeight(options().minHeight)}px`;
|
|
51
|
+
frame.src = next.href;
|
|
52
|
+
} catch {
|
|
53
|
+
} finally {
|
|
54
|
+
renewing = false;
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
hostWindow.addEventListener("message", receive);
|
|
62
|
+
return () => {
|
|
63
|
+
active = false;
|
|
64
|
+
hostWindow.removeEventListener("message", receive);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// packages/react/src/embed.ts
|
|
69
|
+
function mountGrizzleeEmbed(container, options) {
|
|
70
|
+
const url = embedUrl(options.url);
|
|
71
|
+
const frame = container.ownerDocument.createElement("iframe");
|
|
72
|
+
frame.title = options.title ?? "Partner dashboard";
|
|
73
|
+
if (options.className) frame.className = options.className;
|
|
74
|
+
frame.allow = "clipboard-write";
|
|
75
|
+
frame.referrerPolicy = "no-referrer";
|
|
76
|
+
frame.loading = "lazy";
|
|
77
|
+
frame.style.width = "100%";
|
|
78
|
+
frame.style.border = "0";
|
|
79
|
+
frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;
|
|
80
|
+
frame.style.height = frame.style.minHeight;
|
|
81
|
+
const stop = listenToEmbed(frame, () => options);
|
|
82
|
+
frame.src = url.href;
|
|
83
|
+
container.appendChild(frame);
|
|
84
|
+
return {
|
|
85
|
+
destroy() {
|
|
86
|
+
stop();
|
|
87
|
+
frame.remove();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
mountGrizzleeEmbed
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=embed.js.map
|
package/embed.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/host.ts", "../src/embed.ts"],
|
|
4
|
+
"sourcesContent": ["export interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\nexport function embedUrl(url: string): URL {\n const parsed = new URL(url);\n if (\n ![\"https:\", \"http:\"].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password\n )\n throw new Error(\n \"Use an absolute HTTP or HTTPS embed URL without credentials\",\n );\n return parsed;\n}\n\n/** Both hosts use this listener so origin checks and renewal behave the same way. */\nexport function listenToEmbed(\n frame: HTMLIFrameElement,\n options: () => GrizzleeEmbedOptions,\n): () => void {\n const hostWindow = frame.ownerDocument.defaultView;\n if (!hostWindow)\n throw new Error(\"The iframe must belong to a browser document\");\n let origin = embedUrl(options().url).origin;\n let active = true;\n let renewing = false;\n\n const receive = (event: MessageEvent<unknown>) => {\n if (\n !active ||\n event.origin !== origin ||\n !frame.contentWindow ||\n event.source !== frame.contentWindow\n )\n return;\n const message = event.data;\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n\n switch (message.type) {\n case \"grizzlee:ready\":\n options().onReady?.();\n break;\n case \"grizzlee:resize\": {\n if (\n \"height\" in message &&\n typeof message.height === \"number\" &&\n Number.isSafeInteger(message.height) &&\n message.height > 0\n ) {\n const height = Math.min(\n MAX_HEIGHT,\n Math.max(minimumHeight(options().minHeight), message.height),\n );\n frame.style.height = `${height}px`;\n }\n break;\n }\n case \"grizzlee:session_expired\": {\n const renew = options().onSessionExpired;\n if (!renew || renewing) return;\n renewing = true;\n void (async () => {\n try {\n const url = await renew();\n if (!active || !url) return;\n const next = embedUrl(url);\n origin = next.origin;\n frame.style.height = `${minimumHeight(options().minHeight)}px`;\n frame.src = next.href;\n } catch {\n // Leave the expired dashboard in place. The merchant handles errors in its callback.\n } finally {\n renewing = false;\n }\n })();\n break;\n }\n }\n };\n\n hostWindow.addEventListener(\"message\", receive);\n return () => {\n active = false;\n hostWindow.removeEventListener(\"message\", receive);\n };\n}\n", "import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": ";AAYA,IAAM,aAAa;AAEZ,SAAS,cAAc,SAAS,KAAa;AAClD,SAAO,OAAO,SAAS,MAAM,IACzB,KAAK,IAAI,YAAY,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,CAAC,CAAC,IACrD;AACN;AAEO,SAAS,SAAS,KAAkB;AACzC,QAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,MACE,CAAC,CAAC,UAAU,OAAO,EAAE,SAAS,OAAO,QAAQ,KAC7C,OAAO,YACP,OAAO;AAEP,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AACF,SAAO;AACT;AAGO,SAAS,cACd,OACA,SACY;AACZ,QAAM,aAAa,MAAM,cAAc;AACvC,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,8CAA8C;AAChE,MAAI,SAAS,SAAS,QAAQ,EAAE,GAAG,EAAE;AACrC,MAAI,SAAS;AACb,MAAI,WAAW;AAEf,QAAM,UAAU,CAAC,UAAiC;AAChD,QACE,CAAC,UACD,MAAM,WAAW,UACjB,CAAC,MAAM,iBACP,MAAM,WAAW,MAAM;AAEvB;AACF,UAAM,UAAU,MAAM;AACtB,QAAI,CAAC,WAAW,OAAO,YAAY,YAAY,EAAE,UAAU,SAAU;AAErE,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,gBAAQ,EAAE,UAAU;AACpB;AAAA,MACF,KAAK,mBAAmB;AACtB,YACE,YAAY,WACZ,OAAO,QAAQ,WAAW,YAC1B,OAAO,cAAc,QAAQ,MAAM,KACnC,QAAQ,SAAS,GACjB;AACA,gBAAM,SAAS,KAAK;AAAA,YAClB;AAAA,YACA,KAAK,IAAI,cAAc,QAAQ,EAAE,SAAS,GAAG,QAAQ,MAAM;AAAA,UAC7D;AACA,gBAAM,MAAM,SAAS,GAAG,MAAM;AAAA,QAChC;AACA;AAAA,MACF;AAAA,MACA,KAAK,4BAA4B;AAC/B,cAAM,QAAQ,QAAQ,EAAE;AACxB,YAAI,CAAC,SAAS,SAAU;AACxB,mBAAW;AACX,cAAM,YAAY;AAChB,cAAI;AACF,kBAAM,MAAM,MAAM,MAAM;AACxB,gBAAI,CAAC,UAAU,CAAC,IAAK;AACrB,kBAAM,OAAO,SAAS,GAAG;AACzB,qBAAS,KAAK;AACd,kBAAM,MAAM,SAAS,GAAG,cAAc,QAAQ,EAAE,SAAS,CAAC;AAC1D,kBAAM,MAAM,KAAK;AAAA,UACnB,QAAQ;AAAA,UAER,UAAE;AACA,uBAAW;AAAA,UACb;AAAA,QACF,GAAG;AACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,aAAW,iBAAiB,WAAW,OAAO;AAC9C,SAAO,MAAM;AACX,aAAS;AACT,eAAW,oBAAoB,WAAW,OAAO;AAAA,EACnD;AACF;;;AC7FO,SAAS,mBACd,WACA,SACqB;AACrB,QAAM,MAAM,SAAS,QAAQ,GAAG;AAChC,QAAM,QAAQ,UAAU,cAAc,cAAc,QAAQ;AAC5D,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI,QAAQ,UAAW,OAAM,YAAY,QAAQ;AACjD,QAAM,QAAQ;AACd,QAAM,iBAAiB;AACvB,QAAM,UAAU;AAChB,QAAM,MAAM,QAAQ;AACpB,QAAM,MAAM,SAAS;AACrB,QAAM,MAAM,YAAY,GAAG,cAAc,QAAQ,SAAS,CAAC;AAC3D,QAAM,MAAM,SAAS,MAAM,MAAM;AACjC,QAAM,OAAO,cAAc,OAAO,MAAM,OAAO;AAC/C,QAAM,MAAM,IAAI;AAChB,YAAU,YAAY,KAAK;AAC3B,SAAO;AAAA,IACL,UAAU;AACR,WAAK;AACL,YAAM,OAAO;AAAA,IACf;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/index.cjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// packages/react/src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
GrizzleeEmbed: () => GrizzleeEmbed,
|
|
25
|
+
mountGrizzleeEmbed: () => mountGrizzleeEmbed
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
|
|
30
|
+
// packages/react/src/host.ts
|
|
31
|
+
var MAX_HEIGHT = 1e4;
|
|
32
|
+
function minimumHeight(height = 320) {
|
|
33
|
+
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
34
|
+
}
|
|
35
|
+
function embedUrl(url) {
|
|
36
|
+
const parsed = new URL(url);
|
|
37
|
+
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
40
|
+
);
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
function listenToEmbed(frame, options) {
|
|
44
|
+
const hostWindow = frame.ownerDocument.defaultView;
|
|
45
|
+
if (!hostWindow)
|
|
46
|
+
throw new Error("The iframe must belong to a browser document");
|
|
47
|
+
let origin = embedUrl(options().url).origin;
|
|
48
|
+
let active = true;
|
|
49
|
+
let renewing = false;
|
|
50
|
+
const receive = (event) => {
|
|
51
|
+
if (!active || event.origin !== origin || !frame.contentWindow || event.source !== frame.contentWindow)
|
|
52
|
+
return;
|
|
53
|
+
const message = event.data;
|
|
54
|
+
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
55
|
+
switch (message.type) {
|
|
56
|
+
case "grizzlee:ready":
|
|
57
|
+
options().onReady?.();
|
|
58
|
+
break;
|
|
59
|
+
case "grizzlee:resize": {
|
|
60
|
+
if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
|
|
61
|
+
const height = Math.min(
|
|
62
|
+
MAX_HEIGHT,
|
|
63
|
+
Math.max(minimumHeight(options().minHeight), message.height)
|
|
64
|
+
);
|
|
65
|
+
frame.style.height = `${height}px`;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "grizzlee:session_expired": {
|
|
70
|
+
const renew = options().onSessionExpired;
|
|
71
|
+
if (!renew || renewing) return;
|
|
72
|
+
renewing = true;
|
|
73
|
+
void (async () => {
|
|
74
|
+
try {
|
|
75
|
+
const url = await renew();
|
|
76
|
+
if (!active || !url) return;
|
|
77
|
+
const next = embedUrl(url);
|
|
78
|
+
origin = next.origin;
|
|
79
|
+
frame.style.height = `${minimumHeight(options().minHeight)}px`;
|
|
80
|
+
frame.src = next.href;
|
|
81
|
+
} catch {
|
|
82
|
+
} finally {
|
|
83
|
+
renewing = false;
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
hostWindow.addEventListener("message", receive);
|
|
91
|
+
return () => {
|
|
92
|
+
active = false;
|
|
93
|
+
hostWindow.removeEventListener("message", receive);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// packages/react/src/embed.ts
|
|
98
|
+
function mountGrizzleeEmbed(container, options) {
|
|
99
|
+
const url = embedUrl(options.url);
|
|
100
|
+
const frame = container.ownerDocument.createElement("iframe");
|
|
101
|
+
frame.title = options.title ?? "Partner dashboard";
|
|
102
|
+
if (options.className) frame.className = options.className;
|
|
103
|
+
frame.allow = "clipboard-write";
|
|
104
|
+
frame.referrerPolicy = "no-referrer";
|
|
105
|
+
frame.loading = "lazy";
|
|
106
|
+
frame.style.width = "100%";
|
|
107
|
+
frame.style.border = "0";
|
|
108
|
+
frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;
|
|
109
|
+
frame.style.height = frame.style.minHeight;
|
|
110
|
+
const stop = listenToEmbed(frame, () => options);
|
|
111
|
+
frame.src = url.href;
|
|
112
|
+
container.appendChild(frame);
|
|
113
|
+
return {
|
|
114
|
+
destroy() {
|
|
115
|
+
stop();
|
|
116
|
+
frame.remove();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// packages/react/src/index.ts
|
|
122
|
+
function GrizzleeEmbed(props) {
|
|
123
|
+
const frame = (0, import_react.useRef)(null);
|
|
124
|
+
const current = (0, import_react.useRef)(props);
|
|
125
|
+
const url = embedUrl(props.url).href;
|
|
126
|
+
const height = minimumHeight(props.minHeight);
|
|
127
|
+
(0, import_react.useEffect)(() => {
|
|
128
|
+
current.current = props;
|
|
129
|
+
});
|
|
130
|
+
(0, import_react.useEffect)(() => {
|
|
131
|
+
if (frame.current)
|
|
132
|
+
return listenToEmbed(frame.current, () => current.current);
|
|
133
|
+
}, [url]);
|
|
134
|
+
return (0, import_react.createElement)("iframe", {
|
|
135
|
+
ref: frame,
|
|
136
|
+
src: url,
|
|
137
|
+
title: props.title ?? "Partner dashboard",
|
|
138
|
+
className: props.className,
|
|
139
|
+
style: {
|
|
140
|
+
width: "100%",
|
|
141
|
+
border: 0,
|
|
142
|
+
...props.style,
|
|
143
|
+
height,
|
|
144
|
+
minHeight: height
|
|
145
|
+
},
|
|
146
|
+
allow: "clipboard-write",
|
|
147
|
+
referrerPolicy: "no-referrer",
|
|
148
|
+
loading: "lazy"
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=index.cjs.map
|
package/index.cjs.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/host.ts", "../src/embed.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport {\n createElement,\n useEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from \"react\";\nimport {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport { mountGrizzleeEmbed } from \"./embed\";\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\nexport interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {\n style?: CSSProperties;\n}\n\n/** Render a dashboard without putting a server key in the browser. */\nexport function GrizzleeEmbed(props: GrizzleeEmbedProps): ReactElement {\n const frame = useRef<HTMLIFrameElement>(null);\n const current = useRef(props);\n const url = embedUrl(props.url).href;\n const height = minimumHeight(props.minHeight);\n\n useEffect(() => {\n current.current = props;\n });\n useEffect(() => {\n if (frame.current)\n return listenToEmbed(frame.current, () => current.current);\n }, [url]);\n\n return createElement(\"iframe\", {\n ref: frame,\n src: url,\n title: props.title ?? \"Partner dashboard\",\n className: props.className,\n style: {\n width: \"100%\",\n border: 0,\n ...props.style,\n height,\n minHeight: height,\n },\n allow: \"clipboard-write\",\n referrerPolicy: \"no-referrer\",\n loading: \"lazy\",\n });\n}\n", "export interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\nexport function embedUrl(url: string): URL {\n const parsed = new URL(url);\n if (\n ![\"https:\", \"http:\"].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password\n )\n throw new Error(\n \"Use an absolute HTTP or HTTPS embed URL without credentials\",\n );\n return parsed;\n}\n\n/** Both hosts use this listener so origin checks and renewal behave the same way. */\nexport function listenToEmbed(\n frame: HTMLIFrameElement,\n options: () => GrizzleeEmbedOptions,\n): () => void {\n const hostWindow = frame.ownerDocument.defaultView;\n if (!hostWindow)\n throw new Error(\"The iframe must belong to a browser document\");\n let origin = embedUrl(options().url).origin;\n let active = true;\n let renewing = false;\n\n const receive = (event: MessageEvent<unknown>) => {\n if (\n !active ||\n event.origin !== origin ||\n !frame.contentWindow ||\n event.source !== frame.contentWindow\n )\n return;\n const message = event.data;\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n\n switch (message.type) {\n case \"grizzlee:ready\":\n options().onReady?.();\n break;\n case \"grizzlee:resize\": {\n if (\n \"height\" in message &&\n typeof message.height === \"number\" &&\n Number.isSafeInteger(message.height) &&\n message.height > 0\n ) {\n const height = Math.min(\n MAX_HEIGHT,\n Math.max(minimumHeight(options().minHeight), message.height),\n );\n frame.style.height = `${height}px`;\n }\n break;\n }\n case \"grizzlee:session_expired\": {\n const renew = options().onSessionExpired;\n if (!renew || renewing) return;\n renewing = true;\n void (async () => {\n try {\n const url = await renew();\n if (!active || !url) return;\n const next = embedUrl(url);\n origin = next.origin;\n frame.style.height = `${minimumHeight(options().minHeight)}px`;\n frame.src = next.href;\n } catch {\n // Leave the expired dashboard in place. The merchant handles errors in its callback.\n } finally {\n renewing = false;\n }\n })();\n break;\n }\n }\n };\n\n hostWindow.addEventListener(\"message\", receive);\n return () => {\n active = false;\n hostWindow.removeEventListener(\"message\", receive);\n };\n}\n", "import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAMO;;;ACIP,IAAM,aAAa;AAEZ,SAAS,cAAc,SAAS,KAAa;AAClD,SAAO,OAAO,SAAS,MAAM,IACzB,KAAK,IAAI,YAAY,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,CAAC,CAAC,IACrD;AACN;AAEO,SAAS,SAAS,KAAkB;AACzC,QAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,MACE,CAAC,CAAC,UAAU,OAAO,EAAE,SAAS,OAAO,QAAQ,KAC7C,OAAO,YACP,OAAO;AAEP,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AACF,SAAO;AACT;AAGO,SAAS,cACd,OACA,SACY;AACZ,QAAM,aAAa,MAAM,cAAc;AACvC,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,8CAA8C;AAChE,MAAI,SAAS,SAAS,QAAQ,EAAE,GAAG,EAAE;AACrC,MAAI,SAAS;AACb,MAAI,WAAW;AAEf,QAAM,UAAU,CAAC,UAAiC;AAChD,QACE,CAAC,UACD,MAAM,WAAW,UACjB,CAAC,MAAM,iBACP,MAAM,WAAW,MAAM;AAEvB;AACF,UAAM,UAAU,MAAM;AACtB,QAAI,CAAC,WAAW,OAAO,YAAY,YAAY,EAAE,UAAU,SAAU;AAErE,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,gBAAQ,EAAE,UAAU;AACpB;AAAA,MACF,KAAK,mBAAmB;AACtB,YACE,YAAY,WACZ,OAAO,QAAQ,WAAW,YAC1B,OAAO,cAAc,QAAQ,MAAM,KACnC,QAAQ,SAAS,GACjB;AACA,gBAAM,SAAS,KAAK;AAAA,YAClB;AAAA,YACA,KAAK,IAAI,cAAc,QAAQ,EAAE,SAAS,GAAG,QAAQ,MAAM;AAAA,UAC7D;AACA,gBAAM,MAAM,SAAS,GAAG,MAAM;AAAA,QAChC;AACA;AAAA,MACF;AAAA,MACA,KAAK,4BAA4B;AAC/B,cAAM,QAAQ,QAAQ,EAAE;AACxB,YAAI,CAAC,SAAS,SAAU;AACxB,mBAAW;AACX,cAAM,YAAY;AAChB,cAAI;AACF,kBAAM,MAAM,MAAM,MAAM;AACxB,gBAAI,CAAC,UAAU,CAAC,IAAK;AACrB,kBAAM,OAAO,SAAS,GAAG;AACzB,qBAAS,KAAK;AACd,kBAAM,MAAM,SAAS,GAAG,cAAc,QAAQ,EAAE,SAAS,CAAC;AAC1D,kBAAM,MAAM,KAAK;AAAA,UACnB,QAAQ;AAAA,UAER,UAAE;AACA,uBAAW;AAAA,UACb;AAAA,QACF,GAAG;AACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,aAAW,iBAAiB,WAAW,OAAO;AAC9C,SAAO,MAAM;AACX,aAAS;AACT,eAAW,oBAAoB,WAAW,OAAO;AAAA,EACnD;AACF;;;AC7FO,SAAS,mBACd,WACA,SACqB;AACrB,QAAM,MAAM,SAAS,QAAQ,GAAG;AAChC,QAAM,QAAQ,UAAU,cAAc,cAAc,QAAQ;AAC5D,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI,QAAQ,UAAW,OAAM,YAAY,QAAQ;AACjD,QAAM,QAAQ;AACd,QAAM,iBAAiB;AACvB,QAAM,UAAU;AAChB,QAAM,MAAM,QAAQ;AACpB,QAAM,MAAM,SAAS;AACrB,QAAM,MAAM,YAAY,GAAG,cAAc,QAAQ,SAAS,CAAC;AAC3D,QAAM,MAAM,SAAS,MAAM,MAAM;AACjC,QAAM,OAAO,cAAc,OAAO,MAAM,OAAO;AAC/C,QAAM,MAAM,IAAI;AAChB,YAAU,YAAY,KAAK;AAC3B,SAAO;AAAA,IACL,UAAU;AACR,WAAK;AACL,YAAM,OAAO;AAAA,IACf;AAAA,EACF;AACF;;;AFVO,SAAS,cAAc,OAAyC;AACrE,QAAM,YAAQ,qBAA0B,IAAI;AAC5C,QAAM,cAAU,qBAAO,KAAK;AAC5B,QAAM,MAAM,SAAS,MAAM,GAAG,EAAE;AAChC,QAAM,SAAS,cAAc,MAAM,SAAS;AAE5C,8BAAU,MAAM;AACd,YAAQ,UAAU;AAAA,EACpB,CAAC;AACD,8BAAU,MAAM;AACd,QAAI,MAAM;AACR,aAAO,cAAc,MAAM,SAAS,MAAM,QAAQ,OAAO;AAAA,EAC7D,GAAG,CAAC,GAAG,CAAC;AAER,aAAO,4BAAc,UAAU;AAAA,IAC7B,KAAK;AAAA,IACL,KAAK;AAAA,IACL,OAAO,MAAM,SAAS;AAAA,IACtB,WAAW,MAAM;AAAA,IACjB,OAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,GAAG,MAAM;AAAA,MACT;AAAA,MACA,WAAW;AAAA,IACb;AAAA,IACA,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,SAAS;AAAA,EACX,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// packages/react/src/index.ts
|
|
4
|
+
import {
|
|
5
|
+
createElement,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
// packages/react/src/host.ts
|
|
11
|
+
var MAX_HEIGHT = 1e4;
|
|
12
|
+
function minimumHeight(height = 320) {
|
|
13
|
+
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
14
|
+
}
|
|
15
|
+
function embedUrl(url) {
|
|
16
|
+
const parsed = new URL(url);
|
|
17
|
+
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
20
|
+
);
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
function listenToEmbed(frame, options) {
|
|
24
|
+
const hostWindow = frame.ownerDocument.defaultView;
|
|
25
|
+
if (!hostWindow)
|
|
26
|
+
throw new Error("The iframe must belong to a browser document");
|
|
27
|
+
let origin = embedUrl(options().url).origin;
|
|
28
|
+
let active = true;
|
|
29
|
+
let renewing = false;
|
|
30
|
+
const receive = (event) => {
|
|
31
|
+
if (!active || event.origin !== origin || !frame.contentWindow || event.source !== frame.contentWindow)
|
|
32
|
+
return;
|
|
33
|
+
const message = event.data;
|
|
34
|
+
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
35
|
+
switch (message.type) {
|
|
36
|
+
case "grizzlee:ready":
|
|
37
|
+
options().onReady?.();
|
|
38
|
+
break;
|
|
39
|
+
case "grizzlee:resize": {
|
|
40
|
+
if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
|
|
41
|
+
const height = Math.min(
|
|
42
|
+
MAX_HEIGHT,
|
|
43
|
+
Math.max(minimumHeight(options().minHeight), message.height)
|
|
44
|
+
);
|
|
45
|
+
frame.style.height = `${height}px`;
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case "grizzlee:session_expired": {
|
|
50
|
+
const renew = options().onSessionExpired;
|
|
51
|
+
if (!renew || renewing) return;
|
|
52
|
+
renewing = true;
|
|
53
|
+
void (async () => {
|
|
54
|
+
try {
|
|
55
|
+
const url = await renew();
|
|
56
|
+
if (!active || !url) return;
|
|
57
|
+
const next = embedUrl(url);
|
|
58
|
+
origin = next.origin;
|
|
59
|
+
frame.style.height = `${minimumHeight(options().minHeight)}px`;
|
|
60
|
+
frame.src = next.href;
|
|
61
|
+
} catch {
|
|
62
|
+
} finally {
|
|
63
|
+
renewing = false;
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
hostWindow.addEventListener("message", receive);
|
|
71
|
+
return () => {
|
|
72
|
+
active = false;
|
|
73
|
+
hostWindow.removeEventListener("message", receive);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// packages/react/src/embed.ts
|
|
78
|
+
function mountGrizzleeEmbed(container, options) {
|
|
79
|
+
const url = embedUrl(options.url);
|
|
80
|
+
const frame = container.ownerDocument.createElement("iframe");
|
|
81
|
+
frame.title = options.title ?? "Partner dashboard";
|
|
82
|
+
if (options.className) frame.className = options.className;
|
|
83
|
+
frame.allow = "clipboard-write";
|
|
84
|
+
frame.referrerPolicy = "no-referrer";
|
|
85
|
+
frame.loading = "lazy";
|
|
86
|
+
frame.style.width = "100%";
|
|
87
|
+
frame.style.border = "0";
|
|
88
|
+
frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;
|
|
89
|
+
frame.style.height = frame.style.minHeight;
|
|
90
|
+
const stop = listenToEmbed(frame, () => options);
|
|
91
|
+
frame.src = url.href;
|
|
92
|
+
container.appendChild(frame);
|
|
93
|
+
return {
|
|
94
|
+
destroy() {
|
|
95
|
+
stop();
|
|
96
|
+
frame.remove();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// packages/react/src/index.ts
|
|
102
|
+
function GrizzleeEmbed(props) {
|
|
103
|
+
const frame = useRef(null);
|
|
104
|
+
const current = useRef(props);
|
|
105
|
+
const url = embedUrl(props.url).href;
|
|
106
|
+
const height = minimumHeight(props.minHeight);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
current.current = props;
|
|
109
|
+
});
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (frame.current)
|
|
112
|
+
return listenToEmbed(frame.current, () => current.current);
|
|
113
|
+
}, [url]);
|
|
114
|
+
return createElement("iframe", {
|
|
115
|
+
ref: frame,
|
|
116
|
+
src: url,
|
|
117
|
+
title: props.title ?? "Partner dashboard",
|
|
118
|
+
className: props.className,
|
|
119
|
+
style: {
|
|
120
|
+
width: "100%",
|
|
121
|
+
border: 0,
|
|
122
|
+
...props.style,
|
|
123
|
+
height,
|
|
124
|
+
minHeight: height
|
|
125
|
+
},
|
|
126
|
+
allow: "clipboard-write",
|
|
127
|
+
referrerPolicy: "no-referrer",
|
|
128
|
+
loading: "lazy"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
GrizzleeEmbed,
|
|
133
|
+
mountGrizzleeEmbed
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/host.ts", "../src/embed.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport {\n createElement,\n useEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from \"react\";\nimport {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport { mountGrizzleeEmbed } from \"./embed\";\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\nexport interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {\n style?: CSSProperties;\n}\n\n/** Render a dashboard without putting a server key in the browser. */\nexport function GrizzleeEmbed(props: GrizzleeEmbedProps): ReactElement {\n const frame = useRef<HTMLIFrameElement>(null);\n const current = useRef(props);\n const url = embedUrl(props.url).href;\n const height = minimumHeight(props.minHeight);\n\n useEffect(() => {\n current.current = props;\n });\n useEffect(() => {\n if (frame.current)\n return listenToEmbed(frame.current, () => current.current);\n }, [url]);\n\n return createElement(\"iframe\", {\n ref: frame,\n src: url,\n title: props.title ?? \"Partner dashboard\",\n className: props.className,\n style: {\n width: \"100%\",\n border: 0,\n ...props.style,\n height,\n minHeight: height,\n },\n allow: \"clipboard-write\",\n referrerPolicy: \"no-referrer\",\n loading: \"lazy\",\n });\n}\n", "export interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\nexport function embedUrl(url: string): URL {\n const parsed = new URL(url);\n if (\n ![\"https:\", \"http:\"].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password\n )\n throw new Error(\n \"Use an absolute HTTP or HTTPS embed URL without credentials\",\n );\n return parsed;\n}\n\n/** Both hosts use this listener so origin checks and renewal behave the same way. */\nexport function listenToEmbed(\n frame: HTMLIFrameElement,\n options: () => GrizzleeEmbedOptions,\n): () => void {\n const hostWindow = frame.ownerDocument.defaultView;\n if (!hostWindow)\n throw new Error(\"The iframe must belong to a browser document\");\n let origin = embedUrl(options().url).origin;\n let active = true;\n let renewing = false;\n\n const receive = (event: MessageEvent<unknown>) => {\n if (\n !active ||\n event.origin !== origin ||\n !frame.contentWindow ||\n event.source !== frame.contentWindow\n )\n return;\n const message = event.data;\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n\n switch (message.type) {\n case \"grizzlee:ready\":\n options().onReady?.();\n break;\n case \"grizzlee:resize\": {\n if (\n \"height\" in message &&\n typeof message.height === \"number\" &&\n Number.isSafeInteger(message.height) &&\n message.height > 0\n ) {\n const height = Math.min(\n MAX_HEIGHT,\n Math.max(minimumHeight(options().minHeight), message.height),\n );\n frame.style.height = `${height}px`;\n }\n break;\n }\n case \"grizzlee:session_expired\": {\n const renew = options().onSessionExpired;\n if (!renew || renewing) return;\n renewing = true;\n void (async () => {\n try {\n const url = await renew();\n if (!active || !url) return;\n const next = embedUrl(url);\n origin = next.origin;\n frame.style.height = `${minimumHeight(options().minHeight)}px`;\n frame.src = next.href;\n } catch {\n // Leave the expired dashboard in place. The merchant handles errors in its callback.\n } finally {\n renewing = false;\n }\n })();\n break;\n }\n }\n };\n\n hostWindow.addEventListener(\"message\", receive);\n return () => {\n active = false;\n hostWindow.removeEventListener(\"message\", receive);\n };\n}\n", "import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grizzlee/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://grizzlee.ca",
|
|
9
|
+
"description": "Embed Grizzlee partner dashboards in React or plain JavaScript apps.",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
15
|
+
"main": "./index.cjs",
|
|
16
|
+
"module": "./index.js",
|
|
17
|
+
"types": "./types/index.d.cts",
|
|
18
|
+
"typesVersions": {
|
|
19
|
+
"*": {
|
|
20
|
+
"embed": [
|
|
21
|
+
"./types/embed.d.cts"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./types/index.d.ts",
|
|
29
|
+
"default": "./index.js"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./types/index.d.cts",
|
|
33
|
+
"default": "./index.cjs"
|
|
34
|
+
},
|
|
35
|
+
"default": "./index.js"
|
|
36
|
+
},
|
|
37
|
+
"./embed": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./types/embed.d.ts",
|
|
40
|
+
"default": "./embed.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./types/embed.d.cts",
|
|
44
|
+
"default": "./embed.cjs"
|
|
45
|
+
},
|
|
46
|
+
"default": "./embed.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"*.js",
|
|
51
|
+
"*.js.map",
|
|
52
|
+
"*.cjs",
|
|
53
|
+
"*.cjs.map",
|
|
54
|
+
"types",
|
|
55
|
+
"README.md",
|
|
56
|
+
"CHANGELOG.md"
|
|
57
|
+
],
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"react": ">=18"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"react": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type GrizzleeEmbedOptions } from "./host.cjs";
|
|
2
|
+
export type { GrizzleeEmbedOptions } from "./host.cjs";
|
|
3
|
+
/** Append a dashboard iframe. Destroy removes its listener and the iframe. */
|
|
4
|
+
export declare function mountGrizzleeEmbed(container: HTMLElement, options: GrizzleeEmbedOptions): {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
package/types/embed.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type GrizzleeEmbedOptions } from "./host.js";
|
|
2
|
+
export type { GrizzleeEmbedOptions } from "./host.js";
|
|
3
|
+
/** Append a dashboard iframe. Destroy removes its listener and the iframe. */
|
|
4
|
+
export declare function mountGrizzleeEmbed(container: HTMLElement, options: GrizzleeEmbedOptions): {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
package/types/host.d.cts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface GrizzleeEmbedOptions {
|
|
2
|
+
/** An absolute HTTP or HTTPS start URL minted by the merchant's server. */
|
|
3
|
+
url: string;
|
|
4
|
+
/** Return a newly minted URL to reopen the dashboard. Handle errors here. */
|
|
5
|
+
onSessionExpired?: () => string | void | Promise<string | void>;
|
|
6
|
+
onReady?: () => void;
|
|
7
|
+
title?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */
|
|
10
|
+
minHeight?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function minimumHeight(height?: number): number;
|
|
13
|
+
export declare function embedUrl(url: string): URL;
|
|
14
|
+
/** Both hosts use this listener so origin checks and renewal behave the same way. */
|
|
15
|
+
export declare function listenToEmbed(frame: HTMLIFrameElement, options: () => GrizzleeEmbedOptions): () => void;
|
package/types/host.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface GrizzleeEmbedOptions {
|
|
2
|
+
/** An absolute HTTP or HTTPS start URL minted by the merchant's server. */
|
|
3
|
+
url: string;
|
|
4
|
+
/** Return a newly minted URL to reopen the dashboard. Handle errors here. */
|
|
5
|
+
onSessionExpired?: () => string | void | Promise<string | void>;
|
|
6
|
+
onReady?: () => void;
|
|
7
|
+
title?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */
|
|
10
|
+
minHeight?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function minimumHeight(height?: number): number;
|
|
13
|
+
export declare function embedUrl(url: string): URL;
|
|
14
|
+
/** Both hosts use this listener so origin checks and renewal behave the same way. */
|
|
15
|
+
export declare function listenToEmbed(frame: HTMLIFrameElement, options: () => GrizzleeEmbedOptions): () => void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactElement } from "react";
|
|
2
|
+
import { type GrizzleeEmbedOptions } from "./host.cjs";
|
|
3
|
+
export { mountGrizzleeEmbed } from "./embed.cjs";
|
|
4
|
+
export type { GrizzleeEmbedOptions } from "./host.cjs";
|
|
5
|
+
export interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {
|
|
6
|
+
style?: CSSProperties;
|
|
7
|
+
}
|
|
8
|
+
/** Render a dashboard without putting a server key in the browser. */
|
|
9
|
+
export declare function GrizzleeEmbed(props: GrizzleeEmbedProps): ReactElement;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactElement } from "react";
|
|
2
|
+
import { type GrizzleeEmbedOptions } from "./host.js";
|
|
3
|
+
export { mountGrizzleeEmbed } from "./embed.js";
|
|
4
|
+
export type { GrizzleeEmbedOptions } from "./host.js";
|
|
5
|
+
export interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {
|
|
6
|
+
style?: CSSProperties;
|
|
7
|
+
}
|
|
8
|
+
/** Render a dashboard without putting a server key in the browser. */
|
|
9
|
+
export declare function GrizzleeEmbed(props: GrizzleeEmbedProps): ReactElement;
|