@grizzlee/react 0.1.1 → 0.2.0

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 CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.2.0
2
+
3
+ - Add `onShare` to `GrizzleeEmbed` and `mountGrizzleeEmbed`, with the exported `GrizzleeShare` type. A callback opts in through a validated host message. It receives the program ID, referral URL and localized invitation when the partner taps Share. Hosts without a callback keep sharing inside the frame.
4
+
1
5
  ## 0.1.1
2
6
 
3
7
  - Published from CI through npm trusted publishing. No code changes.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Grizzlee embed hosts
2
2
 
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.
3
+ `@grizzlee/react` exports `GrizzleeEmbed`, `mountGrizzleeEmbed`, `GrizzleeEmbedProps`, `GrizzleeEmbedOptions` and `GrizzleeShare`. 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
4
 
5
5
  ```tsx
6
6
  import { GrizzleeEmbed } from "@grizzlee/react";
@@ -16,10 +16,21 @@ async function newSession() {
16
16
  <GrizzleeEmbed url={startUrl} onSessionExpired={newSession} />;
17
17
  ```
18
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`.
19
+ `url` is an absolute HTTP or HTTPS URL minted by your server. Optional props are `onSessionExpired`, `onReady`, `onShare`, `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
20
 
21
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
22
 
23
+ `onShare` receives `{ programId, url, text }` when the partner taps **Share**. Supplying it opts in through a `grizzlee:configure` message after the frame reports ready. `text` is an invitation in the session's language. The listener validates field types and accepts only HTTP or HTTPS URLs without credentials. Without a callback, sharing stays in the frame. React updates the opt-in when the callback changes. Both hosts configure each renewed session.
24
+
25
+ ```tsx
26
+ <GrizzleeEmbed
27
+ url={startUrl}
28
+ onShare={({ url, text }) => openShareSheet({ url, text })}
29
+ />
30
+ ```
31
+
32
+ `openShareSheet` is your app's callback. `mountGrizzleeEmbed` accepts the same `onShare` option. For React Native or Expo, use an authenticated merchant wrapper page and forward `grizzlee:share` to the WebView bridge after origin and iframe checks. See [the WebView recipe](../../docs/embed.md#mobile-webviews).
33
+
23
34
  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
35
 
25
36
  For a host without React:
package/embed.cjs CHANGED
@@ -51,8 +51,29 @@ function listenToEmbed(frame, options) {
51
51
  if (!message || typeof message !== "object" || !("type" in message)) return;
52
52
  switch (message.type) {
53
53
  case "grizzlee:ready":
54
+ frame.contentWindow.postMessage(
55
+ { type: "grizzlee:configure", share: !!options().onShare },
56
+ origin
57
+ );
54
58
  options().onReady?.();
55
59
  break;
60
+ case "grizzlee:share": {
61
+ if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
62
+ return;
63
+ try {
64
+ const url = new URL(message.url);
65
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
66
+ return;
67
+ } catch {
68
+ return;
69
+ }
70
+ options().onShare?.({
71
+ programId: message.programId,
72
+ url: message.url,
73
+ text: message.text
74
+ });
75
+ break;
76
+ }
56
77
  case "grizzlee:resize": {
57
78
  if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
58
79
  const height = Math.min(
package/embed.cjs.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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;",
4
+ "sourcesContent": ["import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions, GrizzleeShare } 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 GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\n\nexport 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 /** Supplying this callback asks the frame to send share actions to the host. */\n onShare?: (share: GrizzleeShare) => 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 frame.contentWindow.postMessage(\n { type: \"grizzlee:configure\", share: !!options().onShare },\n origin,\n );\n options().onReady?.();\n break;\n case \"grizzlee:share\": {\n if (\n !(\"programId\" in message) ||\n typeof message.programId !== \"string\" ||\n !message.programId ||\n !(\"url\" in message) ||\n typeof message.url !== \"string\" ||\n !(\"text\" in message) ||\n typeof message.text !== \"string\"\n )\n return;\n try {\n const url = new URL(message.url);\n if (\n ![\"https:\", \"http:\"].includes(url.protocol) ||\n url.username ||\n url.password\n )\n return;\n } catch {\n return;\n }\n options().onShare?.({\n programId: message.programId,\n url: message.url,\n text: message.text,\n });\n break;\n }\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": "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",
6
6
  "names": []
7
7
  }
package/embed.js CHANGED
@@ -25,8 +25,29 @@ function listenToEmbed(frame, options) {
25
25
  if (!message || typeof message !== "object" || !("type" in message)) return;
26
26
  switch (message.type) {
27
27
  case "grizzlee:ready":
28
+ frame.contentWindow.postMessage(
29
+ { type: "grizzlee:configure", share: !!options().onShare },
30
+ origin
31
+ );
28
32
  options().onReady?.();
29
33
  break;
34
+ case "grizzlee:share": {
35
+ if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
36
+ return;
37
+ try {
38
+ const url = new URL(message.url);
39
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
40
+ return;
41
+ } catch {
42
+ return;
43
+ }
44
+ options().onShare?.({
45
+ programId: message.programId,
46
+ url: message.url,
47
+ text: message.text
48
+ });
49
+ break;
50
+ }
30
51
  case "grizzlee:resize": {
31
52
  if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
32
53
  const height = Math.min(
package/embed.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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;",
4
+ "sourcesContent": ["export interface GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\n\nexport 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 /** Supplying this callback asks the frame to send share actions to the host. */\n onShare?: (share: GrizzleeShare) => 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 frame.contentWindow.postMessage(\n { type: \"grizzlee:configure\", share: !!options().onShare },\n origin,\n );\n options().onReady?.();\n break;\n case \"grizzlee:share\": {\n if (\n !(\"programId\" in message) ||\n typeof message.programId !== \"string\" ||\n !message.programId ||\n !(\"url\" in message) ||\n typeof message.url !== \"string\" ||\n !(\"text\" in message) ||\n typeof message.text !== \"string\"\n )\n return;\n try {\n const url = new URL(message.url);\n if (\n ![\"https:\", \"http:\"].includes(url.protocol) ||\n url.username ||\n url.password\n )\n return;\n } catch {\n return;\n }\n options().onShare?.({\n programId: message.programId,\n url: message.url,\n text: message.text,\n });\n break;\n }\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, GrizzleeShare } 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
6
  "names": []
7
7
  }
package/index.cjs CHANGED
@@ -54,8 +54,29 @@ function listenToEmbed(frame, options) {
54
54
  if (!message || typeof message !== "object" || !("type" in message)) return;
55
55
  switch (message.type) {
56
56
  case "grizzlee:ready":
57
+ frame.contentWindow.postMessage(
58
+ { type: "grizzlee:configure", share: !!options().onShare },
59
+ origin
60
+ );
57
61
  options().onReady?.();
58
62
  break;
63
+ case "grizzlee:share": {
64
+ if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
65
+ return;
66
+ try {
67
+ const url = new URL(message.url);
68
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
69
+ return;
70
+ } catch {
71
+ return;
72
+ }
73
+ options().onShare?.({
74
+ programId: message.programId,
75
+ url: message.url,
76
+ text: message.text
77
+ });
78
+ break;
79
+ }
59
80
  case "grizzlee:resize": {
60
81
  if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
61
82
  const height = Math.min(
@@ -127,6 +148,12 @@ function GrizzleeEmbed(props) {
127
148
  (0, import_react.useEffect)(() => {
128
149
  current.current = props;
129
150
  });
151
+ (0, import_react.useEffect)(() => {
152
+ frame.current?.contentWindow?.postMessage(
153
+ { type: "grizzlee:configure", share: !!props.onShare },
154
+ embedUrl(url).origin
155
+ );
156
+ }, [url, props.onShare]);
130
157
  (0, import_react.useEffect)(() => {
131
158
  if (frame.current)
132
159
  return listenToEmbed(frame.current, () => current.current);
package/index.cjs.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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;",
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, GrizzleeShare } 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 frame.current?.contentWindow?.postMessage(\n { type: \"grizzlee:configure\", share: !!props.onShare },\n embedUrl(url).origin,\n );\n }, [url, props.onShare]);\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 GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\n\nexport 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 /** Supplying this callback asks the frame to send share actions to the host. */\n onShare?: (share: GrizzleeShare) => 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 frame.contentWindow.postMessage(\n { type: \"grizzlee:configure\", share: !!options().onShare },\n origin,\n );\n options().onReady?.();\n break;\n case \"grizzlee:share\": {\n if (\n !(\"programId\" in message) ||\n typeof message.programId !== \"string\" ||\n !message.programId ||\n !(\"url\" in message) ||\n typeof message.url !== \"string\" ||\n !(\"text\" in message) ||\n typeof message.text !== \"string\"\n )\n return;\n try {\n const url = new URL(message.url);\n if (\n ![\"https:\", \"http:\"].includes(url.protocol) ||\n url.username ||\n url.password\n )\n return;\n } catch {\n return;\n }\n options().onShare?.({\n programId: message.programId,\n url: message.url,\n text: message.text,\n });\n break;\n }\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, GrizzleeShare } 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
6
  "names": []
7
7
  }
package/index.js CHANGED
@@ -34,8 +34,29 @@ function listenToEmbed(frame, options) {
34
34
  if (!message || typeof message !== "object" || !("type" in message)) return;
35
35
  switch (message.type) {
36
36
  case "grizzlee:ready":
37
+ frame.contentWindow.postMessage(
38
+ { type: "grizzlee:configure", share: !!options().onShare },
39
+ origin
40
+ );
37
41
  options().onReady?.();
38
42
  break;
43
+ case "grizzlee:share": {
44
+ if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
45
+ return;
46
+ try {
47
+ const url = new URL(message.url);
48
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
49
+ return;
50
+ } catch {
51
+ return;
52
+ }
53
+ options().onShare?.({
54
+ programId: message.programId,
55
+ url: message.url,
56
+ text: message.text
57
+ });
58
+ break;
59
+ }
39
60
  case "grizzlee:resize": {
40
61
  if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
41
62
  const height = Math.min(
@@ -107,6 +128,12 @@ function GrizzleeEmbed(props) {
107
128
  useEffect(() => {
108
129
  current.current = props;
109
130
  });
131
+ useEffect(() => {
132
+ frame.current?.contentWindow?.postMessage(
133
+ { type: "grizzlee:configure", share: !!props.onShare },
134
+ embedUrl(url).origin
135
+ );
136
+ }, [url, props.onShare]);
110
137
  useEffect(() => {
111
138
  if (frame.current)
112
139
  return listenToEmbed(frame.current, () => current.current);
package/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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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",
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, GrizzleeShare } 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 frame.current?.contentWindow?.postMessage(\n { type: \"grizzlee:configure\", share: !!props.onShare },\n embedUrl(url).origin,\n );\n }, [url, props.onShare]);\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 GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\n\nexport 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 /** Supplying this callback asks the frame to send share actions to the host. */\n onShare?: (share: GrizzleeShare) => 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 frame.contentWindow.postMessage(\n { type: \"grizzlee:configure\", share: !!options().onShare },\n origin,\n );\n options().onReady?.();\n break;\n case \"grizzlee:share\": {\n if (\n !(\"programId\" in message) ||\n typeof message.programId !== \"string\" ||\n !message.programId ||\n !(\"url\" in message) ||\n typeof message.url !== \"string\" ||\n !(\"text\" in message) ||\n typeof message.text !== \"string\"\n )\n return;\n try {\n const url = new URL(message.url);\n if (\n ![\"https:\", \"http:\"].includes(url.protocol) ||\n url.username ||\n url.password\n )\n return;\n } catch {\n return;\n }\n options().onShare?.({\n programId: message.programId,\n url: message.url,\n text: message.text,\n });\n break;\n }\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, GrizzleeShare } 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
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grizzlee/react",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/types/embed.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type GrizzleeEmbedOptions } from "./host.cjs";
2
- export type { GrizzleeEmbedOptions } from "./host.cjs";
2
+ export type { GrizzleeEmbedOptions, GrizzleeShare } from "./host.cjs";
3
3
  /** Append a dashboard iframe. Destroy removes its listener and the iframe. */
4
4
  export declare function mountGrizzleeEmbed(container: HTMLElement, options: GrizzleeEmbedOptions): {
5
5
  destroy(): void;
package/types/embed.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type GrizzleeEmbedOptions } from "./host.js";
2
- export type { GrizzleeEmbedOptions } from "./host.js";
2
+ export type { GrizzleeEmbedOptions, GrizzleeShare } from "./host.js";
3
3
  /** Append a dashboard iframe. Destroy removes its listener and the iframe. */
4
4
  export declare function mountGrizzleeEmbed(container: HTMLElement, options: GrizzleeEmbedOptions): {
5
5
  destroy(): void;
package/types/host.d.cts CHANGED
@@ -1,9 +1,16 @@
1
+ export interface GrizzleeShare {
2
+ programId: string;
3
+ url: string;
4
+ text: string;
5
+ }
1
6
  export interface GrizzleeEmbedOptions {
2
7
  /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */
3
8
  url: string;
4
9
  /** Return a newly minted URL to reopen the dashboard. Handle errors here. */
5
10
  onSessionExpired?: () => string | void | Promise<string | void>;
6
11
  onReady?: () => void;
12
+ /** Supplying this callback asks the frame to send share actions to the host. */
13
+ onShare?: (share: GrizzleeShare) => void;
7
14
  title?: string;
8
15
  className?: string;
9
16
  /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */
package/types/host.d.ts CHANGED
@@ -1,9 +1,16 @@
1
+ export interface GrizzleeShare {
2
+ programId: string;
3
+ url: string;
4
+ text: string;
5
+ }
1
6
  export interface GrizzleeEmbedOptions {
2
7
  /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */
3
8
  url: string;
4
9
  /** Return a newly minted URL to reopen the dashboard. Handle errors here. */
5
10
  onSessionExpired?: () => string | void | Promise<string | void>;
6
11
  onReady?: () => void;
12
+ /** Supplying this callback asks the frame to send share actions to the host. */
13
+ onShare?: (share: GrizzleeShare) => void;
7
14
  title?: string;
8
15
  className?: string;
9
16
  /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */
package/types/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type CSSProperties, type ReactElement } from "react";
2
2
  import { type GrizzleeEmbedOptions } from "./host.cjs";
3
3
  export { mountGrizzleeEmbed } from "./embed.cjs";
4
- export type { GrizzleeEmbedOptions } from "./host.cjs";
4
+ export type { GrizzleeEmbedOptions, GrizzleeShare } from "./host.cjs";
5
5
  export interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {
6
6
  style?: CSSProperties;
7
7
  }
package/types/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type CSSProperties, type ReactElement } from "react";
2
2
  import { type GrizzleeEmbedOptions } from "./host.js";
3
3
  export { mountGrizzleeEmbed } from "./embed.js";
4
- export type { GrizzleeEmbedOptions } from "./host.js";
4
+ export type { GrizzleeEmbedOptions, GrizzleeShare } from "./host.js";
5
5
  export interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {
6
6
  style?: CSSProperties;
7
7
  }