@konvert7/promoot 0.2.1 → 0.3.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/README.md CHANGED
@@ -16,6 +16,17 @@ import { PromootBlock } from "@konvert7/promoot";
16
16
  The `url` is the embed URL from your Promoot dashboard — the same string the iframe
17
17
  snippet uses, so one value serves either method.
18
18
 
19
+ ## What it renders
20
+
21
+ Whatever the sponsor bought, without you choosing:
22
+
23
+ - **An image ad** — their artwork, cropped to fill the slot.
24
+ - **A text ad** — their favicon, a headline, a description and their domain, centred.
25
+ Headline and description are both optional; a link on its own renders the mark and
26
+ the bare domain. If no favicon could be captured, a monogram of the domain's first
27
+ letter is shown instead — never both, so a transparent icon can't show the letter
28
+ through it.
29
+
19
30
  ## Why inline
20
31
 
21
32
  An iframe fails closed. If a content filter ever blocks the frame, your sponsor's ad
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { PromootBlock, type PromootBlockProps } from "./promoot-block.js";
2
- export type { BlockPayload, BlockRender } from "./types.js";
2
+ export type { BlockPayload, BlockRender, Creative } from "./types.js";
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  export function blockEndpoint(embedUrl) {
3
3
  return embedUrl.replace("/embed/", "/api/block/");
4
4
  }
@@ -21,7 +21,15 @@ export function scopedCss(payload) {
21
21
  return `${scope} { ${payload.paletteVars}; position: relative; display: block; width: 100%; max-width: ${payload.slot.widthPx}px; height: auto; max-height: ${payload.slot.heightPx}px; aspect-ratio: ${payload.slot.widthPx} / ${payload.slot.heightPx}; }
22
22
  ${scope} a { text-decoration: none; }
23
23
  ${scope} .promoot-ad { display: block; position: relative; height: 100%; width: 100%; }
24
- ${scope} .promoot-ad img { display: block; height: 100%; width: 100%; object-fit: cover; }
24
+ ${scope} .promoot-ad > img { display: block; height: 100%; width: 100%; object-fit: cover; }
25
+ ${scope} .promoot-text { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; width: 100%; height: 100%; box-sizing: border-box; padding: 6px 12px; border: 1px solid var(--edge); border-radius: 10px; background: var(--surface); overflow: hidden; text-align: center; font-family: system-ui, -apple-system, sans-serif; }
26
+ ${scope} .promoot-ad:hover .promoot-text { border-color: var(--edge-hover); background: var(--surface-hover); }
27
+ ${scope} .promoot-mark { position: relative; flex: none; display: grid; place-items: center; width: 24px; height: 24px; margin-bottom: 1px; border-radius: 6px; background: var(--edge); color: var(--surface); font-weight: 700; font-size: 12px; overflow: hidden; }
28
+ ${scope} .promoot-mark img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
29
+ ${scope} .promoot-text > span:not(.promoot-mark) { max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
30
+ ${scope} .promoot-text .promoot-headline { font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.25; }
31
+ ${scope} .promoot-text .promoot-desc { font-size: 12px; color: var(--muted); line-height: 1.25; }
32
+ ${scope} .promoot-text .promoot-host { font-size: 11px; color: var(--muted); line-height: 1.25; }
25
33
  ${scope} .promoot-cta { display: flex; flex-direction: column; align-items: center; gap: 3px; width: 100%; height: 100%; justify-content: center; padding: 0 10px; text-align: center; border: 1px dashed var(--edge); border-radius: 10px; box-sizing: border-box; color: var(--muted); background: var(--surface); font-family: system-ui, -apple-system, sans-serif; }
26
34
  ${scope} .promoot-cta:hover { border-color: var(--edge-hover); background: var(--surface-hover); }
27
35
  ${scope} .promoot-cta strong { font-size: 14px; color: var(--ink); }
@@ -43,10 +51,22 @@ const labelStyle = {
43
51
  borderRadius: "4px",
44
52
  padding: "1px 5px",
45
53
  };
54
+ // One or the other, never layered: a favicon with transparent corners would let
55
+ // the monogram show through it, and a broken image painted over the letter
56
+ // hides it anyway.
57
+ function Mark({ creative }) {
58
+ return (_jsx("span", { className: "promoot-mark", children: creative.faviconUrl ? (_jsx("img", { src: creative.faviconUrl, alt: "" })) : (creative.monogram) }));
59
+ }
60
+ function AdCreative({ creative }) {
61
+ if (creative.kind === "IMAGE") {
62
+ return _jsx("img", { src: creative.imageUrl, alt: creative.title });
63
+ }
64
+ return (_jsxs("span", { className: "promoot-text", children: [_jsx(Mark, { creative: creative }), creative.headline && _jsx("span", { className: "promoot-headline", children: creative.headline }), creative.body && _jsx("span", { className: "promoot-desc", children: creative.body }), _jsx("span", { className: "promoot-host", children: creative.host })] }));
65
+ }
46
66
  export async function PromootBlock({ url, className, style, fallback = null, }) {
47
67
  const block = await loadBlock(url);
48
68
  if (!block || block.render === "blank") {
49
69
  return _jsx(_Fragment, { children: fallback });
50
70
  }
51
- return (_jsxs("div", { "data-promoot-slot": block.slot.id, className: className, style: style, children: [_jsx("style", { dangerouslySetInnerHTML: { __html: scopedCss(block) } }), block.ad && (_jsxs("a", { className: "promoot-ad", href: block.ad.clickUrl, target: "_blank", rel: "noopener nofollow sponsored", children: [_jsx("img", { src: block.ad.imageUrl, alt: block.ad.title }), block.slot.sponsoredLabel && (_jsx("span", { style: labelStyle, children: block.slot.sponsoredLabel }))] })), block.pitch && (_jsxs("a", { className: "promoot-cta", href: block.pitch.purchaseUrl, target: "_blank", rel: "noopener", children: [_jsx("strong", { children: block.pitch.headline }), block.pitch.pitch && _jsx("span", { className: "promoot-pitch", children: block.pitch.pitch }), block.pitch.proof && _jsx("span", { className: "promoot-proof", children: block.pitch.proof }), _jsx("span", { className: "promoot-terms", children: block.pitch.terms })] })), _jsx("img", { className: "promoot-beacon", src: block.beaconUrl, width: 1, height: 1, alt: "", "aria-hidden": "true" })] }));
71
+ return (_jsxs("div", { "data-promoot-slot": block.slot.id, className: className, style: style, children: [_jsx("style", { dangerouslySetInnerHTML: { __html: scopedCss(block) } }), block.ad && (_jsxs("a", { className: "promoot-ad", href: block.ad.clickUrl, target: "_blank", rel: "noopener nofollow sponsored", children: [_jsx(AdCreative, { creative: block.ad }), block.slot.sponsoredLabel && (_jsx("span", { style: labelStyle, children: block.slot.sponsoredLabel }))] })), block.pitch && (_jsxs("a", { className: "promoot-cta", href: block.pitch.purchaseUrl, target: "_blank", rel: "noopener", children: [_jsx("strong", { children: block.pitch.headline }), block.pitch.pitch && _jsx("span", { className: "promoot-pitch", children: block.pitch.pitch }), block.pitch.proof && _jsx("span", { className: "promoot-proof", children: block.pitch.proof }), _jsx("span", { className: "promoot-terms", children: block.pitch.terms })] })), _jsx("img", { className: "promoot-beacon", src: block.beaconUrl, width: 1, height: 1, alt: "", "aria-hidden": "true" })] }));
52
72
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,19 @@
1
1
  export type BlockRender = "ad" | "pitch" | "blank";
2
+ /** An ad is either artwork the sponsor uploaded, or words they wrote. */
3
+ export type Creative = {
4
+ kind: "IMAGE";
5
+ title: string;
6
+ imageUrl: string;
7
+ } | {
8
+ kind: "TEXT";
9
+ /** Optional: a link alone is a complete ad, and the host stands in. */
10
+ headline: string | null;
11
+ body: string | null;
12
+ /** Null when the sponsor's site has none, or it could not be captured. */
13
+ faviconUrl: string | null;
14
+ host: string;
15
+ monogram: string;
16
+ };
2
17
  /** What GET /api/block/:slotId answers with. */
3
18
  export type BlockPayload = {
4
19
  slot: {
@@ -8,12 +23,10 @@ export type BlockPayload = {
8
23
  sponsoredLabel: string | null;
9
24
  };
10
25
  render: BlockRender;
11
- ad: {
12
- title: string;
13
- imageUrl: string;
26
+ ad: ({
14
27
  clickUrl: string;
15
28
  expiresAt: string | null;
16
- } | null;
29
+ } & Creative) | null;
17
30
  pitch: {
18
31
  headline: string;
19
32
  pitch: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konvert7/promoot",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Render a Promoot sponsor slot inline as a React server component, with views counted by the visitor's own browser.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",