@particle-academy/fancy-pixel 0.1.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 +117 -0
- package/dist/fancy-pixel.global.min.js +102 -0
- package/dist/fancy-pixel.global.min.js.map +1 -0
- package/dist/index.cjs +319 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +107 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +313 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @particle-academy/fancy-pixel
|
|
2
|
+
|
|
3
|
+
Embeddable **Fancy UI verification badge + liveness beacon**. Zero runtime
|
|
4
|
+
dependencies, vanilla TypeScript, rendered inside an **open Shadow DOM** so the
|
|
5
|
+
host page's CSS cannot hide it — visibility is part of verification.
|
|
6
|
+
|
|
7
|
+
Three styles, two placement modes:
|
|
8
|
+
|
|
9
|
+
| Style | Renders |
|
|
10
|
+
| -------- | ---------------------------------------- |
|
|
11
|
+
| `badge` | "Powered by Fancy UI" wordmark + glyph |
|
|
12
|
+
| `mark` | Logo glyph only |
|
|
13
|
+
| `beacon` | A small pulsing dot |
|
|
14
|
+
|
|
15
|
+
| Mode | Placement |
|
|
16
|
+
| ---------- | ---------------------------------------- |
|
|
17
|
+
| `placed` | Inline at a target selector/element |
|
|
18
|
+
| `floating` | `position: fixed` bottom-right corner |
|
|
19
|
+
|
|
20
|
+
Every style emits the same `data-fancy-badge` marker the Fancy UI showcase
|
|
21
|
+
scanner detects, plus a stable `data-fancy-pixel` handle and
|
|
22
|
+
`data-fancy-pixel-style`.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @particle-academy/fancy-pixel
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Embed via `<script>` (no build step)
|
|
31
|
+
|
|
32
|
+
A single tag both loads and auto-mounts, reading config from its own
|
|
33
|
+
`data-*` attributes:
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<script
|
|
37
|
+
src="https://unpkg.com/@particle-academy/fancy-pixel/dist/fancy-pixel.global.min.js"
|
|
38
|
+
data-style="badge"
|
|
39
|
+
data-mode="floating"
|
|
40
|
+
data-site="YOUR_SITE_KEY"
|
|
41
|
+
data-endpoint="https://your-host/heuristics"
|
|
42
|
+
></script>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Attributes: `data-style` (`badge`|`mark`|`beacon`), `data-mode`
|
|
46
|
+
(`placed`|`floating`), `data-site`, `data-endpoint`, optional `data-target`
|
|
47
|
+
(selector, for `placed`) and `data-href` (link target).
|
|
48
|
+
|
|
49
|
+
The global build also exposes `window.FancyPixel.mountPixel(...)` for manual
|
|
50
|
+
mounts.
|
|
51
|
+
|
|
52
|
+
## Programmatic usage
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { mountPixel } from "@particle-academy/fancy-pixel";
|
|
56
|
+
|
|
57
|
+
const pixel = mountPixel({
|
|
58
|
+
style: "badge", // "badge" | "mark" | "beacon"
|
|
59
|
+
mode: "floating", // "placed" | "floating"
|
|
60
|
+
target: "#footer", // selector or Element (placed mode)
|
|
61
|
+
siteKey: "YOUR_SITE_KEY",
|
|
62
|
+
endpoint: "https://your-host/heuristics",
|
|
63
|
+
href: "https://particle.academy", // optional link target
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// pixel.host -> the host element (open shadow root + data markers)
|
|
67
|
+
// pixel.visible -> current IntersectionObserver-confirmed visibility
|
|
68
|
+
// pixel.destroy() -> tear down observers and remove the element
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Visibility + beacon
|
|
72
|
+
|
|
73
|
+
- An **IntersectionObserver** confirms the badge is genuinely on-screen — not
|
|
74
|
+
`display:none`, off-screen, or 0-size.
|
|
75
|
+
- On first confirmed visibility a `fancy-pixel:shown` `CustomEvent` is
|
|
76
|
+
dispatched on `document` (so hosts/agents can observe it).
|
|
77
|
+
- On mount **and** on every visibility change the pixel POSTs to
|
|
78
|
+
`${endpoint}/pixel` via `navigator.sendBeacon` (falling back to
|
|
79
|
+
`fetch(..., { keepalive: true })`):
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"siteKey": "YOUR_SITE_KEY",
|
|
84
|
+
"style": "badge",
|
|
85
|
+
"mode": "floating",
|
|
86
|
+
"visible": true,
|
|
87
|
+
"path": "/some/page",
|
|
88
|
+
"ts": 1717600000000
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If `endpoint` is omitted, no network request is made.
|
|
93
|
+
|
|
94
|
+
## Human+ contract
|
|
95
|
+
|
|
96
|
+
This is a mostly-visual component, but it is **inhabitable**: it exposes a
|
|
97
|
+
stable handle (`data-fancy-pixel`), emits the `data-fancy-badge` marker for
|
|
98
|
+
server-side scanners, and dispatches `fancy-pixel:shown` so presence/coaching
|
|
99
|
+
layers can compose for free.
|
|
100
|
+
|
|
101
|
+
## Demo
|
|
102
|
+
|
|
103
|
+
`demo/index.html` mounts all styles/modes and intercepts `sendBeacon`/`fetch`
|
|
104
|
+
to log the `/heuristics/pixel` round-trip offline. Serve the package root over
|
|
105
|
+
any static server and open `/demo/index.html` after `npm run build`.
|
|
106
|
+
|
|
107
|
+
## Build
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm run build # tsup -> dist/index.js (ESM), dist/index.cjs (CJS),
|
|
111
|
+
# dist/fancy-pixel.global.min.js (IIFE, globalName FancyPixel)
|
|
112
|
+
npm test # node --test against the built ESM bundle
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
var FancyPixel=(function(exports){'use strict';function h(e,t){if(!e)return false;let n=e.replace(/\/+$/,"")+"/pixel",i=JSON.stringify(t);try{if(typeof navigator<"u"&&typeof navigator.sendBeacon=="function"){let o=new Blob([i],{type:"application/json"});if(navigator.sendBeacon(n,o))return !0}}catch{}try{if(typeof fetch=="function")return fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:i,keepalive:!0,mode:"cors",credentials:"omit"}).catch(()=>{}),!0}catch{}return false}function P(e){return `<svg width="${e}" height="${e}" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" style="display:block;flex:0 0 auto;">
|
|
2
|
+
<rect x="2" y="2" width="20" height="20" rx="6" fill="url(#fp-grad)"/>
|
|
3
|
+
<circle cx="9" cy="9" r="3" fill="#fff"/>
|
|
4
|
+
<circle cx="16.5" cy="15" r="1.6" fill="#fff" opacity="0.85"/>
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="fp-grad" x1="2" y1="2" x2="22" y2="22" gradientUnits="userSpaceOnUse">
|
|
7
|
+
<stop stop-color="#7c5cff"/>
|
|
8
|
+
<stop offset="1" stop-color="#22d3ee"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
</svg>`}var p=`
|
|
12
|
+
:host { all: initial; }
|
|
13
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
14
|
+
.fp-root {
|
|
15
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
16
|
+
line-height: 1;
|
|
17
|
+
-webkit-font-smoothing: antialiased;
|
|
18
|
+
}
|
|
19
|
+
.fp-link {
|
|
20
|
+
text-decoration: none;
|
|
21
|
+
color: inherit;
|
|
22
|
+
display: inline-flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
@keyframes fp-pulse {
|
|
27
|
+
0% { transform: scale(0.85); opacity: 0.55; }
|
|
28
|
+
50% { transform: scale(1); opacity: 1; }
|
|
29
|
+
100% { transform: scale(0.85); opacity: 0.55; }
|
|
30
|
+
}
|
|
31
|
+
@media (prefers-reduced-motion: reduce) {
|
|
32
|
+
.fp-pulse, .fp-pulse-ring { animation: none !important; }
|
|
33
|
+
}
|
|
34
|
+
`;function M(e){return e==="floating"?["position:fixed","right:16px","bottom:16px","z-index:2147483647","pointer-events:none"].join(";"):["display:inline-block","vertical-align:middle"].join(";")}function E(e,t){let n=`class="fp-link" href="${t}" target="_blank" rel="noopener noreferrer" part="link" data-fancy-pixel-link`;return e==="mark"?`<style>${p}
|
|
35
|
+
.fp-mark {
|
|
36
|
+
pointer-events:auto;
|
|
37
|
+
display:inline-flex; align-items:center; justify-content:center;
|
|
38
|
+
padding:6px; border-radius:10px;
|
|
39
|
+
background:rgba(17,17,27,0.92);
|
|
40
|
+
box-shadow:0 2px 10px rgba(0,0,0,0.25);
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
43
|
+
<div class="fp-root" part="root">
|
|
44
|
+
<a ${n} aria-label="Powered by Fancy UI">
|
|
45
|
+
<span class="fp-mark">${P(22)}</span>
|
|
46
|
+
</a>
|
|
47
|
+
</div>`:e==="beacon"?`<style>${p}
|
|
48
|
+
.fp-beacon {
|
|
49
|
+
pointer-events:auto;
|
|
50
|
+
position:relative; display:inline-flex;
|
|
51
|
+
width:14px; height:14px;
|
|
52
|
+
}
|
|
53
|
+
.fp-dot {
|
|
54
|
+
position:absolute; inset:3px;
|
|
55
|
+
border-radius:50%;
|
|
56
|
+
background:linear-gradient(135deg,#7c5cff,#22d3ee);
|
|
57
|
+
}
|
|
58
|
+
.fp-pulse-ring {
|
|
59
|
+
position:absolute; inset:0;
|
|
60
|
+
border-radius:50%;
|
|
61
|
+
background:radial-gradient(circle,#22d3ee 0%,rgba(34,211,238,0) 70%);
|
|
62
|
+
animation:fp-pulse 1.8s ease-in-out infinite;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
<div class="fp-root" part="root">
|
|
66
|
+
<a ${n} aria-label="Powered by Fancy UI">
|
|
67
|
+
<span class="fp-beacon">
|
|
68
|
+
<span class="fp-pulse-ring fp-pulse"></span>
|
|
69
|
+
<span class="fp-dot"></span>
|
|
70
|
+
</span>
|
|
71
|
+
</a>
|
|
72
|
+
</div>`:`<style>${p}
|
|
73
|
+
.fp-badge {
|
|
74
|
+
pointer-events:auto;
|
|
75
|
+
display:inline-flex; align-items:center; gap:8px;
|
|
76
|
+
padding:7px 11px; border-radius:999px;
|
|
77
|
+
background:rgba(17,17,27,0.92);
|
|
78
|
+
color:#f5f5fa;
|
|
79
|
+
font-size:12px; font-weight:600; letter-spacing:0.01em;
|
|
80
|
+
box-shadow:0 2px 12px rgba(0,0,0,0.28);
|
|
81
|
+
border:1px solid rgba(124,92,255,0.35);
|
|
82
|
+
transition:transform 120ms ease;
|
|
83
|
+
}
|
|
84
|
+
.fp-badge:hover { transform:translateY(-1px); }
|
|
85
|
+
.fp-by { opacity:0.7; font-weight:500; }
|
|
86
|
+
.fp-name {
|
|
87
|
+
background:linear-gradient(135deg,#a78bfa,#22d3ee);
|
|
88
|
+
-webkit-background-clip:text; background-clip:text;
|
|
89
|
+
-webkit-text-fill-color:transparent; color:transparent;
|
|
90
|
+
font-weight:700;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
93
|
+
<div class="fp-root" part="root">
|
|
94
|
+
<a ${n} aria-label="Powered by Fancy UI">
|
|
95
|
+
<span class="fp-badge">
|
|
96
|
+
${P(16)}
|
|
97
|
+
<span><span class="fp-by">Powered by</span> <span class="fp-name">Fancy UI</span></span>
|
|
98
|
+
</span>
|
|
99
|
+
</a>
|
|
100
|
+
</div>`}function v(e,t,n){let i=document.createElement("div");i.setAttribute("data-fancy-pixel",""),i.setAttribute("data-fancy-badge",""),i.setAttribute("data-fancy-pixel-style",e),i.setAttribute("data-fancy-pixel-mode",t),i.style.cssText=M(t);let o=i.attachShadow({mode:"open"});return o.innerHTML=E(e,n),i}var O="https://particle.academy",H=["badge","mark","beacon"],$=["placed","floating"];function I(e){if(e instanceof Element)return e;if(typeof e=="string"){let t=document.querySelector(e);if(t)return t}return document.body}function T(){return typeof location<"u"?location.pathname:"/"}function f(e={}){let t=H.includes(e.style)?e.style:"badge",n=$.includes(e.mode)?e.mode:"floating",i=e.siteKey??"",o=e.endpoint??"",u=e.href??O,y=e.target??null,a=v(t,n,u);n==="floating"?document.body.appendChild(a):I(y).appendChild(a);let l=false,x=false,c=false;function m(){o&&h(o,{siteKey:i,style:t,mode:n,visible:l,path:T(),ts:Date.now()});}function w(){if(x)return;x=true;let r={style:t,mode:n,siteKey:i,host:a};document.dispatchEvent(new CustomEvent("fancy-pixel:shown",{detail:r}));}let s=null;function g(r){c||r===l||(l=r,l&&w(),m());}return typeof IntersectionObserver=="function"?(s=new IntersectionObserver(r=>{let d=r[r.length-1];if(!d)return;let b=d.boundingClientRect,k=b.width>0&&b.height>0;g(d.isIntersecting&&d.intersectionRatio>0&&k);},{threshold:[0,.01,.5]}),s.observe(a)):g(true),m(),{host:a,get visible(){return l},options:{style:t,mode:n,siteKey:i,endpoint:o,href:u,target:y},destroy(){c||(c=true,s?.disconnect(),s=null,a.remove());}}}function j(){if(typeof document>"u")return null;let e=document.currentScript;if(!e||!e.dataset||e.dataset.site===void 0){let n=document.querySelectorAll("script[data-site][src*='fancy-pixel'], script[data-fancy-pixel]");e=n[n.length-1]??null;}if(!e)return null;let t=e.dataset;return t.style===void 0&&t.mode===void 0&&t.site===void 0&&t.endpoint===void 0&&t.fancyPixel===void 0?null:{style:t.style,mode:t.mode,siteKey:t.site,endpoint:t.endpoint,target:t.target??null,href:t.href}}function S(){let e=j();if(!e)return null;let t=()=>f(e);if(typeof document<"u"&&document.readyState==="loading"){let n=null;return document.addEventListener("DOMContentLoaded",()=>{n=t();},{once:true}),n}return t()}S();
|
|
101
|
+
exports.autoInit=S;exports.mountPixel=f;return exports;})({});//# sourceMappingURL=fancy-pixel.global.min.js.map
|
|
102
|
+
//# sourceMappingURL=fancy-pixel.global.min.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/beacon.ts","../src/render.ts","../src/mount.ts","../src/autoinit.ts","../src/global.ts"],"names":["sendPixelBeacon","endpoint","payload","url","body","blob","logoGlyph","size","RESET","hostStyleFor","mode","innerFor","style","href","linkAttrs","createPixelHost","host","root","DEFAULT_HREF","STYLES","MODES","resolveTarget","target","found","currentPath","mountPixel","opts","siteKey","visible","shownDispatched","destroyed","beacon","dispatchShownOnce","detail","observer","setVisible","next","entries","entry","rect","hasSize","readScriptConfig","el","candidates","ds","autoInit","config","run","handle"],"mappings":"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","file":"fancy-pixel.global.min.js","sourcesContent":["import type { PixelBeaconPayload } from \"./types.js\";\n\n/**\n * Send a pixel liveness/visibility beacon to `${endpoint}/pixel`.\n *\n * Prefers `navigator.sendBeacon` (survives page unload, non-blocking),\n * falling back to `fetch(..., { keepalive: true })`. Both paths POST JSON.\n * Failures are swallowed — a missing beacon must never break the host page.\n *\n * @returns `true` if the beacon was handed off to the transport.\n */\nexport function sendPixelBeacon(\n endpoint: string,\n payload: PixelBeaconPayload,\n): boolean {\n if (!endpoint) return false;\n\n const url = endpoint.replace(/\\/+$/, \"\") + \"/pixel\";\n const body = JSON.stringify(payload);\n\n try {\n if (\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\"\n ) {\n // sendBeacon with a Blob preserves the JSON content type for the server.\n const blob = new Blob([body], { type: \"application/json\" });\n if (navigator.sendBeacon(url, blob)) return true;\n // sendBeacon returns false if the UA refused to queue it — fall through.\n }\n } catch {\n /* fall through to fetch */\n }\n\n try {\n if (typeof fetch === \"function\") {\n void fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n keepalive: true,\n // Cross-origin ingestion: we never read the response, so opaque is fine.\n mode: \"cors\",\n credentials: \"omit\",\n }).catch(() => {\n /* swallow */\n });\n return true;\n }\n } catch {\n /* swallow */\n }\n\n return false;\n}\n","import type { PixelMode, PixelStyle } from \"./types.js\";\n\n/**\n * The Fancy UI logo glyph as an inline SVG string. A rounded-square mark with\n * an orbiting particle — self-contained, no external asset, scales cleanly.\n */\nfunction logoGlyph(size: number): string {\n return `<svg width=\"${size}\" height=\"${size}\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" focusable=\"false\" style=\"display:block;flex:0 0 auto;\">\n <rect x=\"2\" y=\"2\" width=\"20\" height=\"20\" rx=\"6\" fill=\"url(#fp-grad)\"/>\n <circle cx=\"9\" cy=\"9\" r=\"3\" fill=\"#fff\"/>\n <circle cx=\"16.5\" cy=\"15\" r=\"1.6\" fill=\"#fff\" opacity=\"0.85\"/>\n <defs>\n <linearGradient id=\"fp-grad\" x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#7c5cff\"/>\n <stop offset=\"1\" stop-color=\"#22d3ee\"/>\n </linearGradient>\n </defs>\n</svg>`;\n}\n\n/**\n * Reset rules applied to the shadow root host's internal wrapper. Because we\n * render inside an open Shadow DOM, host page CSS cannot reach in to hide or\n * restyle these — visibility is part of the verification contract.\n */\nconst RESET = `\n :host { all: initial; }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n .fp-root {\n font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n }\n .fp-link {\n text-decoration: none;\n color: inherit;\n display: inline-flex;\n align-items: center;\n cursor: pointer;\n }\n @keyframes fp-pulse {\n 0% { transform: scale(0.85); opacity: 0.55; }\n 50% { transform: scale(1); opacity: 1; }\n 100% { transform: scale(0.85); opacity: 0.55; }\n }\n @media (prefers-reduced-motion: reduce) {\n .fp-pulse, .fp-pulse-ring { animation: none !important; }\n }\n`;\n\n/** Positioning styles for the host element, by mode. */\nexport function hostStyleFor(mode: PixelMode): string {\n if (mode === \"floating\") {\n return [\n \"position:fixed\",\n \"right:16px\",\n \"bottom:16px\",\n \"z-index:2147483647\",\n // A fixed corner element must not capture clicks across the page; only\n // the chip itself is interactive (pointer-events re-enabled inside).\n \"pointer-events:none\",\n ].join(\";\");\n }\n // placed: inline, flows with the target. Stays interactive.\n return [\"display:inline-block\", \"vertical-align:middle\"].join(\";\");\n}\n\n/** Build the inner HTML of the shadow root for a given style. */\nfunction innerFor(style: PixelStyle, href: string): string {\n const linkAttrs = `class=\"fp-link\" href=\"${href}\" target=\"_blank\" rel=\"noopener noreferrer\" part=\"link\" data-fancy-pixel-link`;\n\n if (style === \"mark\") {\n return `<style>${RESET}\n .fp-mark {\n pointer-events:auto;\n display:inline-flex; align-items:center; justify-content:center;\n padding:6px; border-radius:10px;\n background:rgba(17,17,27,0.92);\n box-shadow:0 2px 10px rgba(0,0,0,0.25);\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-mark\">${logoGlyph(22)}</span>\n </a>\n </div>`;\n }\n\n if (style === \"beacon\") {\n return `<style>${RESET}\n .fp-beacon {\n pointer-events:auto;\n position:relative; display:inline-flex;\n width:14px; height:14px;\n }\n .fp-dot {\n position:absolute; inset:3px;\n border-radius:50%;\n background:linear-gradient(135deg,#7c5cff,#22d3ee);\n }\n .fp-pulse-ring {\n position:absolute; inset:0;\n border-radius:50%;\n background:radial-gradient(circle,#22d3ee 0%,rgba(34,211,238,0) 70%);\n animation:fp-pulse 1.8s ease-in-out infinite;\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-beacon\">\n <span class=\"fp-pulse-ring fp-pulse\"></span>\n <span class=\"fp-dot\"></span>\n </span>\n </a>\n </div>`;\n }\n\n // badge (default): wordmark + glyph chip.\n return `<style>${RESET}\n .fp-badge {\n pointer-events:auto;\n display:inline-flex; align-items:center; gap:8px;\n padding:7px 11px; border-radius:999px;\n background:rgba(17,17,27,0.92);\n color:#f5f5fa;\n font-size:12px; font-weight:600; letter-spacing:0.01em;\n box-shadow:0 2px 12px rgba(0,0,0,0.28);\n border:1px solid rgba(124,92,255,0.35);\n transition:transform 120ms ease;\n }\n .fp-badge:hover { transform:translateY(-1px); }\n .fp-by { opacity:0.7; font-weight:500; }\n .fp-name {\n background:linear-gradient(135deg,#a78bfa,#22d3ee);\n -webkit-background-clip:text; background-clip:text;\n -webkit-text-fill-color:transparent; color:transparent;\n font-weight:700;\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-badge\">\n ${logoGlyph(16)}\n <span><span class=\"fp-by\">Powered by</span> <span class=\"fp-name\">Fancy UI</span></span>\n </span>\n </a>\n </div>`;\n}\n\n/**\n * Create the host element + open shadow root for a pixel.\n *\n * The host carries the stable, host-CSS-proof markers:\n * - `data-fancy-pixel` — the stable agent/scanner handle\n * - `data-fancy-badge` — the marker ScanShowcaseSubmission detects\n * - `data-fancy-pixel-style` — the active style\n *\n * @returns the host element (shadow root already populated).\n */\nexport function createPixelHost(\n style: PixelStyle,\n mode: PixelMode,\n href: string,\n): HTMLElement {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-fancy-pixel\", \"\");\n host.setAttribute(\"data-fancy-badge\", \"\");\n host.setAttribute(\"data-fancy-pixel-style\", style);\n host.setAttribute(\"data-fancy-pixel-mode\", mode);\n host.style.cssText = hostStyleFor(mode);\n\n const root = host.attachShadow({ mode: \"open\" });\n root.innerHTML = innerFor(style, href);\n\n return host;\n}\n","import { sendPixelBeacon } from \"./beacon.js\";\nimport { createPixelHost } from \"./render.js\";\nimport type {\n MountPixelOptions,\n PixelHandle,\n PixelMode,\n PixelShownEventDetail,\n PixelStyle,\n} from \"./types.js\";\n\nconst DEFAULT_HREF = \"https://particle.academy\";\nconst STYLES: PixelStyle[] = [\"badge\", \"mark\", \"beacon\"];\nconst MODES: PixelMode[] = [\"placed\", \"floating\"];\n\nfunction resolveTarget(target: string | Element | null | undefined): Element {\n if (target instanceof Element) return target;\n if (typeof target === \"string\") {\n const found = document.querySelector(target);\n if (found) return found;\n }\n return document.body;\n}\n\nfunction currentPath(): string {\n return typeof location !== \"undefined\" ? location.pathname : \"/\";\n}\n\n/**\n * Mount a Fancy UI pixel into the page.\n *\n * Renders the chosen style inside an open Shadow DOM (host CSS cannot hide it),\n * wires an IntersectionObserver to confirm true on-screen visibility, dispatches\n * a `fancy-pixel:shown` CustomEvent on first visibility, and POSTs a liveness\n * beacon to `${endpoint}/pixel` on mount and on every visibility change.\n */\nexport function mountPixel(opts: MountPixelOptions = {}): PixelHandle {\n const style: PixelStyle = STYLES.includes(opts.style as PixelStyle)\n ? (opts.style as PixelStyle)\n : \"badge\";\n const mode: PixelMode = MODES.includes(opts.mode as PixelMode)\n ? (opts.mode as PixelMode)\n : \"floating\";\n const siteKey = opts.siteKey ?? \"\";\n const endpoint = opts.endpoint ?? \"\";\n const href = opts.href ?? DEFAULT_HREF;\n const target = opts.target ?? null;\n\n const host = createPixelHost(style, mode, href);\n\n // Mount: floating always pins to body; placed flows at the target.\n if (mode === \"floating\") {\n document.body.appendChild(host);\n } else {\n resolveTarget(target).appendChild(host);\n }\n\n let visible = false;\n let shownDispatched = false;\n let destroyed = false;\n\n function beacon(): void {\n if (!endpoint) return;\n sendPixelBeacon(endpoint, {\n siteKey,\n style,\n mode,\n visible,\n path: currentPath(),\n ts: Date.now(),\n });\n }\n\n function dispatchShownOnce(): void {\n if (shownDispatched) return;\n shownDispatched = true;\n const detail: PixelShownEventDetail = { style, mode, siteKey, host };\n document.dispatchEvent(\n new CustomEvent<PixelShownEventDetail>(\"fancy-pixel:shown\", { detail }),\n );\n }\n\n // Visibility verification. IntersectionObserver confirms the badge is truly\n // on-screen — not display:none, not 0-size, not scrolled out of view. When\n // unavailable (old/headless UAs) we optimistically assume visible so the\n // liveness beacon still fires.\n let observer: IntersectionObserver | null = null;\n\n function setVisible(next: boolean): void {\n if (destroyed || next === visible) return;\n visible = next;\n if (visible) dispatchShownOnce();\n beacon();\n }\n\n if (typeof IntersectionObserver === \"function\") {\n observer = new IntersectionObserver(\n (entries) => {\n const entry = entries[entries.length - 1];\n if (!entry) return;\n // intersectionRatio > 0 with a non-zero box = genuinely rendered.\n const rect = entry.boundingClientRect;\n const hasSize = rect.width > 0 && rect.height > 0;\n setVisible(entry.isIntersecting && entry.intersectionRatio > 0 && hasSize);\n },\n { threshold: [0, 0.01, 0.5] },\n );\n observer.observe(host);\n } else {\n // No IO support — fire an optimistic visible beacon on mount.\n setVisible(true);\n }\n\n // Always emit a mount beacon so the server logs liveness even before the\n // observer reports (visible defaults false until proven).\n beacon();\n\n const resolved: PixelHandle[\"options\"] = {\n style,\n mode,\n siteKey,\n endpoint,\n href,\n target,\n };\n\n return {\n host,\n get visible() {\n return visible;\n },\n options: resolved,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n observer?.disconnect();\n observer = null;\n host.remove();\n },\n };\n}\n","import { mountPixel } from \"./mount.js\";\nimport type { MountPixelOptions, PixelHandle } from \"./types.js\";\n\n/**\n * Read pixel config off the currently-executing <script> element's data-*\n * attributes. Used by the IIFE global bundle so a single <script> tag both\n * loads and auto-mounts the pixel:\n *\n * <script src=\".../fancy-pixel.global.min.js\"\n * data-style=\"badge\" data-mode=\"floating\"\n * data-site=\"KEY\" data-endpoint=\"https://host/heuristics\"></script>\n */\nfunction readScriptConfig(): MountPixelOptions | null {\n if (typeof document === \"undefined\") return null;\n\n // `document.currentScript` is the running script during initial parse.\n // Fall back to the last <script> carrying our data-site marker.\n let el = document.currentScript as HTMLScriptElement | null;\n if (!el || !el.dataset || el.dataset.site === undefined) {\n const candidates = document.querySelectorAll<HTMLScriptElement>(\n \"script[data-site][src*='fancy-pixel'], script[data-fancy-pixel]\",\n );\n el = candidates[candidates.length - 1] ?? null;\n }\n if (!el) return null;\n\n const ds = el.dataset;\n // Only auto-init if at least one fancy-pixel attribute is present.\n if (\n ds.style === undefined &&\n ds.mode === undefined &&\n ds.site === undefined &&\n ds.endpoint === undefined &&\n ds.fancyPixel === undefined\n ) {\n return null;\n }\n\n return {\n style: ds.style as MountPixelOptions[\"style\"],\n mode: ds.mode as MountPixelOptions[\"mode\"],\n siteKey: ds.site,\n endpoint: ds.endpoint,\n target: ds.target ?? null,\n href: ds.href,\n };\n}\n\n/**\n * Auto-mount from the script tag's data-* attributes if present.\n * Safe to call multiple times; mounts at most once per call when config exists.\n */\nexport function autoInit(): PixelHandle | null {\n const config = readScriptConfig();\n if (!config) return null;\n\n const run = (): PixelHandle => mountPixel(config);\n\n if (typeof document !== \"undefined\" && document.readyState === \"loading\") {\n // Defer to DOMContentLoaded so `placed` targets exist.\n let handle: PixelHandle | null = null;\n document.addEventListener(\n \"DOMContentLoaded\",\n () => {\n handle = run();\n },\n { once: true },\n );\n // Return null now; the handle resolves after DOMContentLoaded.\n return handle;\n }\n\n return run();\n}\n","/**\n * IIFE global entry. Built to `dist/fancy-pixel.global.min.js` with\n * `globalName: \"FancyPixel\"`. Exposes the public API on `window.FancyPixel`\n * AND auto-mounts from the script tag's data-* attributes on load.\n */\nimport { autoInit } from \"./autoinit.js\";\nimport { mountPixel } from \"./mount.js\";\n\n// Run auto-init immediately when the bundle executes (the <script> embed path).\nautoInit();\n\nexport { mountPixel, autoInit };\n"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/beacon.ts
|
|
4
|
+
function sendPixelBeacon(endpoint, payload) {
|
|
5
|
+
if (!endpoint) return false;
|
|
6
|
+
const url = endpoint.replace(/\/+$/, "") + "/pixel";
|
|
7
|
+
const body = JSON.stringify(payload);
|
|
8
|
+
try {
|
|
9
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
10
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
11
|
+
if (navigator.sendBeacon(url, blob)) return true;
|
|
12
|
+
}
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
if (typeof fetch === "function") {
|
|
17
|
+
void fetch(url, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
body,
|
|
21
|
+
keepalive: true,
|
|
22
|
+
// Cross-origin ingestion: we never read the response, so opaque is fine.
|
|
23
|
+
mode: "cors",
|
|
24
|
+
credentials: "omit"
|
|
25
|
+
}).catch(() => {
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/render.ts
|
|
35
|
+
function logoGlyph(size) {
|
|
36
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" style="display:block;flex:0 0 auto;">
|
|
37
|
+
<rect x="2" y="2" width="20" height="20" rx="6" fill="url(#fp-grad)"/>
|
|
38
|
+
<circle cx="9" cy="9" r="3" fill="#fff"/>
|
|
39
|
+
<circle cx="16.5" cy="15" r="1.6" fill="#fff" opacity="0.85"/>
|
|
40
|
+
<defs>
|
|
41
|
+
<linearGradient id="fp-grad" x1="2" y1="2" x2="22" y2="22" gradientUnits="userSpaceOnUse">
|
|
42
|
+
<stop stop-color="#7c5cff"/>
|
|
43
|
+
<stop offset="1" stop-color="#22d3ee"/>
|
|
44
|
+
</linearGradient>
|
|
45
|
+
</defs>
|
|
46
|
+
</svg>`;
|
|
47
|
+
}
|
|
48
|
+
var RESET = `
|
|
49
|
+
:host { all: initial; }
|
|
50
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
51
|
+
.fp-root {
|
|
52
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
53
|
+
line-height: 1;
|
|
54
|
+
-webkit-font-smoothing: antialiased;
|
|
55
|
+
}
|
|
56
|
+
.fp-link {
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
color: inherit;
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
@keyframes fp-pulse {
|
|
64
|
+
0% { transform: scale(0.85); opacity: 0.55; }
|
|
65
|
+
50% { transform: scale(1); opacity: 1; }
|
|
66
|
+
100% { transform: scale(0.85); opacity: 0.55; }
|
|
67
|
+
}
|
|
68
|
+
@media (prefers-reduced-motion: reduce) {
|
|
69
|
+
.fp-pulse, .fp-pulse-ring { animation: none !important; }
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
function hostStyleFor(mode) {
|
|
73
|
+
if (mode === "floating") {
|
|
74
|
+
return [
|
|
75
|
+
"position:fixed",
|
|
76
|
+
"right:16px",
|
|
77
|
+
"bottom:16px",
|
|
78
|
+
"z-index:2147483647",
|
|
79
|
+
// A fixed corner element must not capture clicks across the page; only
|
|
80
|
+
// the chip itself is interactive (pointer-events re-enabled inside).
|
|
81
|
+
"pointer-events:none"
|
|
82
|
+
].join(";");
|
|
83
|
+
}
|
|
84
|
+
return ["display:inline-block", "vertical-align:middle"].join(";");
|
|
85
|
+
}
|
|
86
|
+
function innerFor(style, href) {
|
|
87
|
+
const linkAttrs = `class="fp-link" href="${href}" target="_blank" rel="noopener noreferrer" part="link" data-fancy-pixel-link`;
|
|
88
|
+
if (style === "mark") {
|
|
89
|
+
return `<style>${RESET}
|
|
90
|
+
.fp-mark {
|
|
91
|
+
pointer-events:auto;
|
|
92
|
+
display:inline-flex; align-items:center; justify-content:center;
|
|
93
|
+
padding:6px; border-radius:10px;
|
|
94
|
+
background:rgba(17,17,27,0.92);
|
|
95
|
+
box-shadow:0 2px 10px rgba(0,0,0,0.25);
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
98
|
+
<div class="fp-root" part="root">
|
|
99
|
+
<a ${linkAttrs} aria-label="Powered by Fancy UI">
|
|
100
|
+
<span class="fp-mark">${logoGlyph(22)}</span>
|
|
101
|
+
</a>
|
|
102
|
+
</div>`;
|
|
103
|
+
}
|
|
104
|
+
if (style === "beacon") {
|
|
105
|
+
return `<style>${RESET}
|
|
106
|
+
.fp-beacon {
|
|
107
|
+
pointer-events:auto;
|
|
108
|
+
position:relative; display:inline-flex;
|
|
109
|
+
width:14px; height:14px;
|
|
110
|
+
}
|
|
111
|
+
.fp-dot {
|
|
112
|
+
position:absolute; inset:3px;
|
|
113
|
+
border-radius:50%;
|
|
114
|
+
background:linear-gradient(135deg,#7c5cff,#22d3ee);
|
|
115
|
+
}
|
|
116
|
+
.fp-pulse-ring {
|
|
117
|
+
position:absolute; inset:0;
|
|
118
|
+
border-radius:50%;
|
|
119
|
+
background:radial-gradient(circle,#22d3ee 0%,rgba(34,211,238,0) 70%);
|
|
120
|
+
animation:fp-pulse 1.8s ease-in-out infinite;
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
123
|
+
<div class="fp-root" part="root">
|
|
124
|
+
<a ${linkAttrs} aria-label="Powered by Fancy UI">
|
|
125
|
+
<span class="fp-beacon">
|
|
126
|
+
<span class="fp-pulse-ring fp-pulse"></span>
|
|
127
|
+
<span class="fp-dot"></span>
|
|
128
|
+
</span>
|
|
129
|
+
</a>
|
|
130
|
+
</div>`;
|
|
131
|
+
}
|
|
132
|
+
return `<style>${RESET}
|
|
133
|
+
.fp-badge {
|
|
134
|
+
pointer-events:auto;
|
|
135
|
+
display:inline-flex; align-items:center; gap:8px;
|
|
136
|
+
padding:7px 11px; border-radius:999px;
|
|
137
|
+
background:rgba(17,17,27,0.92);
|
|
138
|
+
color:#f5f5fa;
|
|
139
|
+
font-size:12px; font-weight:600; letter-spacing:0.01em;
|
|
140
|
+
box-shadow:0 2px 12px rgba(0,0,0,0.28);
|
|
141
|
+
border:1px solid rgba(124,92,255,0.35);
|
|
142
|
+
transition:transform 120ms ease;
|
|
143
|
+
}
|
|
144
|
+
.fp-badge:hover { transform:translateY(-1px); }
|
|
145
|
+
.fp-by { opacity:0.7; font-weight:500; }
|
|
146
|
+
.fp-name {
|
|
147
|
+
background:linear-gradient(135deg,#a78bfa,#22d3ee);
|
|
148
|
+
-webkit-background-clip:text; background-clip:text;
|
|
149
|
+
-webkit-text-fill-color:transparent; color:transparent;
|
|
150
|
+
font-weight:700;
|
|
151
|
+
}
|
|
152
|
+
</style>
|
|
153
|
+
<div class="fp-root" part="root">
|
|
154
|
+
<a ${linkAttrs} aria-label="Powered by Fancy UI">
|
|
155
|
+
<span class="fp-badge">
|
|
156
|
+
${logoGlyph(16)}
|
|
157
|
+
<span><span class="fp-by">Powered by</span> <span class="fp-name">Fancy UI</span></span>
|
|
158
|
+
</span>
|
|
159
|
+
</a>
|
|
160
|
+
</div>`;
|
|
161
|
+
}
|
|
162
|
+
function createPixelHost(style, mode, href) {
|
|
163
|
+
const host = document.createElement("div");
|
|
164
|
+
host.setAttribute("data-fancy-pixel", "");
|
|
165
|
+
host.setAttribute("data-fancy-badge", "");
|
|
166
|
+
host.setAttribute("data-fancy-pixel-style", style);
|
|
167
|
+
host.setAttribute("data-fancy-pixel-mode", mode);
|
|
168
|
+
host.style.cssText = hostStyleFor(mode);
|
|
169
|
+
const root = host.attachShadow({ mode: "open" });
|
|
170
|
+
root.innerHTML = innerFor(style, href);
|
|
171
|
+
return host;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/mount.ts
|
|
175
|
+
var DEFAULT_HREF = "https://particle.academy";
|
|
176
|
+
var STYLES = ["badge", "mark", "beacon"];
|
|
177
|
+
var MODES = ["placed", "floating"];
|
|
178
|
+
function resolveTarget(target) {
|
|
179
|
+
if (target instanceof Element) return target;
|
|
180
|
+
if (typeof target === "string") {
|
|
181
|
+
const found = document.querySelector(target);
|
|
182
|
+
if (found) return found;
|
|
183
|
+
}
|
|
184
|
+
return document.body;
|
|
185
|
+
}
|
|
186
|
+
function currentPath() {
|
|
187
|
+
return typeof location !== "undefined" ? location.pathname : "/";
|
|
188
|
+
}
|
|
189
|
+
function mountPixel(opts = {}) {
|
|
190
|
+
const style = STYLES.includes(opts.style) ? opts.style : "badge";
|
|
191
|
+
const mode = MODES.includes(opts.mode) ? opts.mode : "floating";
|
|
192
|
+
const siteKey = opts.siteKey ?? "";
|
|
193
|
+
const endpoint = opts.endpoint ?? "";
|
|
194
|
+
const href = opts.href ?? DEFAULT_HREF;
|
|
195
|
+
const target = opts.target ?? null;
|
|
196
|
+
const host = createPixelHost(style, mode, href);
|
|
197
|
+
if (mode === "floating") {
|
|
198
|
+
document.body.appendChild(host);
|
|
199
|
+
} else {
|
|
200
|
+
resolveTarget(target).appendChild(host);
|
|
201
|
+
}
|
|
202
|
+
let visible = false;
|
|
203
|
+
let shownDispatched = false;
|
|
204
|
+
let destroyed = false;
|
|
205
|
+
function beacon() {
|
|
206
|
+
if (!endpoint) return;
|
|
207
|
+
sendPixelBeacon(endpoint, {
|
|
208
|
+
siteKey,
|
|
209
|
+
style,
|
|
210
|
+
mode,
|
|
211
|
+
visible,
|
|
212
|
+
path: currentPath(),
|
|
213
|
+
ts: Date.now()
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function dispatchShownOnce() {
|
|
217
|
+
if (shownDispatched) return;
|
|
218
|
+
shownDispatched = true;
|
|
219
|
+
const detail = { style, mode, siteKey, host };
|
|
220
|
+
document.dispatchEvent(
|
|
221
|
+
new CustomEvent("fancy-pixel:shown", { detail })
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
let observer = null;
|
|
225
|
+
function setVisible(next) {
|
|
226
|
+
if (destroyed || next === visible) return;
|
|
227
|
+
visible = next;
|
|
228
|
+
if (visible) dispatchShownOnce();
|
|
229
|
+
beacon();
|
|
230
|
+
}
|
|
231
|
+
if (typeof IntersectionObserver === "function") {
|
|
232
|
+
observer = new IntersectionObserver(
|
|
233
|
+
(entries) => {
|
|
234
|
+
const entry = entries[entries.length - 1];
|
|
235
|
+
if (!entry) return;
|
|
236
|
+
const rect = entry.boundingClientRect;
|
|
237
|
+
const hasSize = rect.width > 0 && rect.height > 0;
|
|
238
|
+
setVisible(entry.isIntersecting && entry.intersectionRatio > 0 && hasSize);
|
|
239
|
+
},
|
|
240
|
+
{ threshold: [0, 0.01, 0.5] }
|
|
241
|
+
);
|
|
242
|
+
observer.observe(host);
|
|
243
|
+
} else {
|
|
244
|
+
setVisible(true);
|
|
245
|
+
}
|
|
246
|
+
beacon();
|
|
247
|
+
const resolved = {
|
|
248
|
+
style,
|
|
249
|
+
mode,
|
|
250
|
+
siteKey,
|
|
251
|
+
endpoint,
|
|
252
|
+
href,
|
|
253
|
+
target
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
host,
|
|
257
|
+
get visible() {
|
|
258
|
+
return visible;
|
|
259
|
+
},
|
|
260
|
+
options: resolved,
|
|
261
|
+
destroy() {
|
|
262
|
+
if (destroyed) return;
|
|
263
|
+
destroyed = true;
|
|
264
|
+
observer?.disconnect();
|
|
265
|
+
observer = null;
|
|
266
|
+
host.remove();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/autoinit.ts
|
|
272
|
+
function readScriptConfig() {
|
|
273
|
+
if (typeof document === "undefined") return null;
|
|
274
|
+
let el = document.currentScript;
|
|
275
|
+
if (!el || !el.dataset || el.dataset.site === void 0) {
|
|
276
|
+
const candidates = document.querySelectorAll(
|
|
277
|
+
"script[data-site][src*='fancy-pixel'], script[data-fancy-pixel]"
|
|
278
|
+
);
|
|
279
|
+
el = candidates[candidates.length - 1] ?? null;
|
|
280
|
+
}
|
|
281
|
+
if (!el) return null;
|
|
282
|
+
const ds = el.dataset;
|
|
283
|
+
if (ds.style === void 0 && ds.mode === void 0 && ds.site === void 0 && ds.endpoint === void 0 && ds.fancyPixel === void 0) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
style: ds.style,
|
|
288
|
+
mode: ds.mode,
|
|
289
|
+
siteKey: ds.site,
|
|
290
|
+
endpoint: ds.endpoint,
|
|
291
|
+
target: ds.target ?? null,
|
|
292
|
+
href: ds.href
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function autoInit() {
|
|
296
|
+
const config = readScriptConfig();
|
|
297
|
+
if (!config) return null;
|
|
298
|
+
const run = () => mountPixel(config);
|
|
299
|
+
if (typeof document !== "undefined" && document.readyState === "loading") {
|
|
300
|
+
let handle = null;
|
|
301
|
+
document.addEventListener(
|
|
302
|
+
"DOMContentLoaded",
|
|
303
|
+
() => {
|
|
304
|
+
handle = run();
|
|
305
|
+
},
|
|
306
|
+
{ once: true }
|
|
307
|
+
);
|
|
308
|
+
return handle;
|
|
309
|
+
}
|
|
310
|
+
return run();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
exports.autoInit = autoInit;
|
|
314
|
+
exports.createPixelHost = createPixelHost;
|
|
315
|
+
exports.hostStyleFor = hostStyleFor;
|
|
316
|
+
exports.mountPixel = mountPixel;
|
|
317
|
+
exports.sendPixelBeacon = sendPixelBeacon;
|
|
318
|
+
//# sourceMappingURL=index.cjs.map
|
|
319
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/beacon.ts","../src/render.ts","../src/mount.ts","../src/autoinit.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { PixelBeaconPayload } from \"./types.js\";\n\n/**\n * Send a pixel liveness/visibility beacon to `${endpoint}/pixel`.\n *\n * Prefers `navigator.sendBeacon` (survives page unload, non-blocking),\n * falling back to `fetch(..., { keepalive: true })`. Both paths POST JSON.\n * Failures are swallowed — a missing beacon must never break the host page.\n *\n * @returns `true` if the beacon was handed off to the transport.\n */\nexport function sendPixelBeacon(\n endpoint: string,\n payload: PixelBeaconPayload,\n): boolean {\n if (!endpoint) return false;\n\n const url = endpoint.replace(/\\/+$/, \"\") + \"/pixel\";\n const body = JSON.stringify(payload);\n\n try {\n if (\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\"\n ) {\n // sendBeacon with a Blob preserves the JSON content type for the server.\n const blob = new Blob([body], { type: \"application/json\" });\n if (navigator.sendBeacon(url, blob)) return true;\n // sendBeacon returns false if the UA refused to queue it — fall through.\n }\n } catch {\n /* fall through to fetch */\n }\n\n try {\n if (typeof fetch === \"function\") {\n void fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n keepalive: true,\n // Cross-origin ingestion: we never read the response, so opaque is fine.\n mode: \"cors\",\n credentials: \"omit\",\n }).catch(() => {\n /* swallow */\n });\n return true;\n }\n } catch {\n /* swallow */\n }\n\n return false;\n}\n","import type { PixelMode, PixelStyle } from \"./types.js\";\n\n/**\n * The Fancy UI logo glyph as an inline SVG string. A rounded-square mark with\n * an orbiting particle — self-contained, no external asset, scales cleanly.\n */\nfunction logoGlyph(size: number): string {\n return `<svg width=\"${size}\" height=\"${size}\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" focusable=\"false\" style=\"display:block;flex:0 0 auto;\">\n <rect x=\"2\" y=\"2\" width=\"20\" height=\"20\" rx=\"6\" fill=\"url(#fp-grad)\"/>\n <circle cx=\"9\" cy=\"9\" r=\"3\" fill=\"#fff\"/>\n <circle cx=\"16.5\" cy=\"15\" r=\"1.6\" fill=\"#fff\" opacity=\"0.85\"/>\n <defs>\n <linearGradient id=\"fp-grad\" x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#7c5cff\"/>\n <stop offset=\"1\" stop-color=\"#22d3ee\"/>\n </linearGradient>\n </defs>\n</svg>`;\n}\n\n/**\n * Reset rules applied to the shadow root host's internal wrapper. Because we\n * render inside an open Shadow DOM, host page CSS cannot reach in to hide or\n * restyle these — visibility is part of the verification contract.\n */\nconst RESET = `\n :host { all: initial; }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n .fp-root {\n font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n }\n .fp-link {\n text-decoration: none;\n color: inherit;\n display: inline-flex;\n align-items: center;\n cursor: pointer;\n }\n @keyframes fp-pulse {\n 0% { transform: scale(0.85); opacity: 0.55; }\n 50% { transform: scale(1); opacity: 1; }\n 100% { transform: scale(0.85); opacity: 0.55; }\n }\n @media (prefers-reduced-motion: reduce) {\n .fp-pulse, .fp-pulse-ring { animation: none !important; }\n }\n`;\n\n/** Positioning styles for the host element, by mode. */\nexport function hostStyleFor(mode: PixelMode): string {\n if (mode === \"floating\") {\n return [\n \"position:fixed\",\n \"right:16px\",\n \"bottom:16px\",\n \"z-index:2147483647\",\n // A fixed corner element must not capture clicks across the page; only\n // the chip itself is interactive (pointer-events re-enabled inside).\n \"pointer-events:none\",\n ].join(\";\");\n }\n // placed: inline, flows with the target. Stays interactive.\n return [\"display:inline-block\", \"vertical-align:middle\"].join(\";\");\n}\n\n/** Build the inner HTML of the shadow root for a given style. */\nfunction innerFor(style: PixelStyle, href: string): string {\n const linkAttrs = `class=\"fp-link\" href=\"${href}\" target=\"_blank\" rel=\"noopener noreferrer\" part=\"link\" data-fancy-pixel-link`;\n\n if (style === \"mark\") {\n return `<style>${RESET}\n .fp-mark {\n pointer-events:auto;\n display:inline-flex; align-items:center; justify-content:center;\n padding:6px; border-radius:10px;\n background:rgba(17,17,27,0.92);\n box-shadow:0 2px 10px rgba(0,0,0,0.25);\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-mark\">${logoGlyph(22)}</span>\n </a>\n </div>`;\n }\n\n if (style === \"beacon\") {\n return `<style>${RESET}\n .fp-beacon {\n pointer-events:auto;\n position:relative; display:inline-flex;\n width:14px; height:14px;\n }\n .fp-dot {\n position:absolute; inset:3px;\n border-radius:50%;\n background:linear-gradient(135deg,#7c5cff,#22d3ee);\n }\n .fp-pulse-ring {\n position:absolute; inset:0;\n border-radius:50%;\n background:radial-gradient(circle,#22d3ee 0%,rgba(34,211,238,0) 70%);\n animation:fp-pulse 1.8s ease-in-out infinite;\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-beacon\">\n <span class=\"fp-pulse-ring fp-pulse\"></span>\n <span class=\"fp-dot\"></span>\n </span>\n </a>\n </div>`;\n }\n\n // badge (default): wordmark + glyph chip.\n return `<style>${RESET}\n .fp-badge {\n pointer-events:auto;\n display:inline-flex; align-items:center; gap:8px;\n padding:7px 11px; border-radius:999px;\n background:rgba(17,17,27,0.92);\n color:#f5f5fa;\n font-size:12px; font-weight:600; letter-spacing:0.01em;\n box-shadow:0 2px 12px rgba(0,0,0,0.28);\n border:1px solid rgba(124,92,255,0.35);\n transition:transform 120ms ease;\n }\n .fp-badge:hover { transform:translateY(-1px); }\n .fp-by { opacity:0.7; font-weight:500; }\n .fp-name {\n background:linear-gradient(135deg,#a78bfa,#22d3ee);\n -webkit-background-clip:text; background-clip:text;\n -webkit-text-fill-color:transparent; color:transparent;\n font-weight:700;\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-badge\">\n ${logoGlyph(16)}\n <span><span class=\"fp-by\">Powered by</span> <span class=\"fp-name\">Fancy UI</span></span>\n </span>\n </a>\n </div>`;\n}\n\n/**\n * Create the host element + open shadow root for a pixel.\n *\n * The host carries the stable, host-CSS-proof markers:\n * - `data-fancy-pixel` — the stable agent/scanner handle\n * - `data-fancy-badge` — the marker ScanShowcaseSubmission detects\n * - `data-fancy-pixel-style` — the active style\n *\n * @returns the host element (shadow root already populated).\n */\nexport function createPixelHost(\n style: PixelStyle,\n mode: PixelMode,\n href: string,\n): HTMLElement {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-fancy-pixel\", \"\");\n host.setAttribute(\"data-fancy-badge\", \"\");\n host.setAttribute(\"data-fancy-pixel-style\", style);\n host.setAttribute(\"data-fancy-pixel-mode\", mode);\n host.style.cssText = hostStyleFor(mode);\n\n const root = host.attachShadow({ mode: \"open\" });\n root.innerHTML = innerFor(style, href);\n\n return host;\n}\n","import { sendPixelBeacon } from \"./beacon.js\";\nimport { createPixelHost } from \"./render.js\";\nimport type {\n MountPixelOptions,\n PixelHandle,\n PixelMode,\n PixelShownEventDetail,\n PixelStyle,\n} from \"./types.js\";\n\nconst DEFAULT_HREF = \"https://particle.academy\";\nconst STYLES: PixelStyle[] = [\"badge\", \"mark\", \"beacon\"];\nconst MODES: PixelMode[] = [\"placed\", \"floating\"];\n\nfunction resolveTarget(target: string | Element | null | undefined): Element {\n if (target instanceof Element) return target;\n if (typeof target === \"string\") {\n const found = document.querySelector(target);\n if (found) return found;\n }\n return document.body;\n}\n\nfunction currentPath(): string {\n return typeof location !== \"undefined\" ? location.pathname : \"/\";\n}\n\n/**\n * Mount a Fancy UI pixel into the page.\n *\n * Renders the chosen style inside an open Shadow DOM (host CSS cannot hide it),\n * wires an IntersectionObserver to confirm true on-screen visibility, dispatches\n * a `fancy-pixel:shown` CustomEvent on first visibility, and POSTs a liveness\n * beacon to `${endpoint}/pixel` on mount and on every visibility change.\n */\nexport function mountPixel(opts: MountPixelOptions = {}): PixelHandle {\n const style: PixelStyle = STYLES.includes(opts.style as PixelStyle)\n ? (opts.style as PixelStyle)\n : \"badge\";\n const mode: PixelMode = MODES.includes(opts.mode as PixelMode)\n ? (opts.mode as PixelMode)\n : \"floating\";\n const siteKey = opts.siteKey ?? \"\";\n const endpoint = opts.endpoint ?? \"\";\n const href = opts.href ?? DEFAULT_HREF;\n const target = opts.target ?? null;\n\n const host = createPixelHost(style, mode, href);\n\n // Mount: floating always pins to body; placed flows at the target.\n if (mode === \"floating\") {\n document.body.appendChild(host);\n } else {\n resolveTarget(target).appendChild(host);\n }\n\n let visible = false;\n let shownDispatched = false;\n let destroyed = false;\n\n function beacon(): void {\n if (!endpoint) return;\n sendPixelBeacon(endpoint, {\n siteKey,\n style,\n mode,\n visible,\n path: currentPath(),\n ts: Date.now(),\n });\n }\n\n function dispatchShownOnce(): void {\n if (shownDispatched) return;\n shownDispatched = true;\n const detail: PixelShownEventDetail = { style, mode, siteKey, host };\n document.dispatchEvent(\n new CustomEvent<PixelShownEventDetail>(\"fancy-pixel:shown\", { detail }),\n );\n }\n\n // Visibility verification. IntersectionObserver confirms the badge is truly\n // on-screen — not display:none, not 0-size, not scrolled out of view. When\n // unavailable (old/headless UAs) we optimistically assume visible so the\n // liveness beacon still fires.\n let observer: IntersectionObserver | null = null;\n\n function setVisible(next: boolean): void {\n if (destroyed || next === visible) return;\n visible = next;\n if (visible) dispatchShownOnce();\n beacon();\n }\n\n if (typeof IntersectionObserver === \"function\") {\n observer = new IntersectionObserver(\n (entries) => {\n const entry = entries[entries.length - 1];\n if (!entry) return;\n // intersectionRatio > 0 with a non-zero box = genuinely rendered.\n const rect = entry.boundingClientRect;\n const hasSize = rect.width > 0 && rect.height > 0;\n setVisible(entry.isIntersecting && entry.intersectionRatio > 0 && hasSize);\n },\n { threshold: [0, 0.01, 0.5] },\n );\n observer.observe(host);\n } else {\n // No IO support — fire an optimistic visible beacon on mount.\n setVisible(true);\n }\n\n // Always emit a mount beacon so the server logs liveness even before the\n // observer reports (visible defaults false until proven).\n beacon();\n\n const resolved: PixelHandle[\"options\"] = {\n style,\n mode,\n siteKey,\n endpoint,\n href,\n target,\n };\n\n return {\n host,\n get visible() {\n return visible;\n },\n options: resolved,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n observer?.disconnect();\n observer = null;\n host.remove();\n },\n };\n}\n","import { mountPixel } from \"./mount.js\";\nimport type { MountPixelOptions, PixelHandle } from \"./types.js\";\n\n/**\n * Read pixel config off the currently-executing <script> element's data-*\n * attributes. Used by the IIFE global bundle so a single <script> tag both\n * loads and auto-mounts the pixel:\n *\n * <script src=\".../fancy-pixel.global.min.js\"\n * data-style=\"badge\" data-mode=\"floating\"\n * data-site=\"KEY\" data-endpoint=\"https://host/heuristics\"></script>\n */\nfunction readScriptConfig(): MountPixelOptions | null {\n if (typeof document === \"undefined\") return null;\n\n // `document.currentScript` is the running script during initial parse.\n // Fall back to the last <script> carrying our data-site marker.\n let el = document.currentScript as HTMLScriptElement | null;\n if (!el || !el.dataset || el.dataset.site === undefined) {\n const candidates = document.querySelectorAll<HTMLScriptElement>(\n \"script[data-site][src*='fancy-pixel'], script[data-fancy-pixel]\",\n );\n el = candidates[candidates.length - 1] ?? null;\n }\n if (!el) return null;\n\n const ds = el.dataset;\n // Only auto-init if at least one fancy-pixel attribute is present.\n if (\n ds.style === undefined &&\n ds.mode === undefined &&\n ds.site === undefined &&\n ds.endpoint === undefined &&\n ds.fancyPixel === undefined\n ) {\n return null;\n }\n\n return {\n style: ds.style as MountPixelOptions[\"style\"],\n mode: ds.mode as MountPixelOptions[\"mode\"],\n siteKey: ds.site,\n endpoint: ds.endpoint,\n target: ds.target ?? null,\n href: ds.href,\n };\n}\n\n/**\n * Auto-mount from the script tag's data-* attributes if present.\n * Safe to call multiple times; mounts at most once per call when config exists.\n */\nexport function autoInit(): PixelHandle | null {\n const config = readScriptConfig();\n if (!config) return null;\n\n const run = (): PixelHandle => mountPixel(config);\n\n if (typeof document !== \"undefined\" && document.readyState === \"loading\") {\n // Defer to DOMContentLoaded so `placed` targets exist.\n let handle: PixelHandle | null = null;\n document.addEventListener(\n \"DOMContentLoaded\",\n () => {\n handle = run();\n },\n { once: true },\n );\n // Return null now; the handle resolves after DOMContentLoaded.\n return handle;\n }\n\n return run();\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for @particle-academy/fancy-pixel.
|
|
3
|
+
*
|
|
4
|
+
* The pixel is an embeddable verification badge + liveness beacon. Three
|
|
5
|
+
* visual styles, two placement modes. All styles emit the same
|
|
6
|
+
* `data-fancy-badge` marker the showcase scanner already detects.
|
|
7
|
+
*/
|
|
8
|
+
/** Visual style of the pixel. */
|
|
9
|
+
type PixelStyle = "badge" | "mark" | "beacon";
|
|
10
|
+
/** Placement mode. `placed` mounts inline at a target; `floating` pins a fixed corner. */
|
|
11
|
+
type PixelMode = "placed" | "floating";
|
|
12
|
+
/** Options accepted by {@link mountPixel}. */
|
|
13
|
+
interface MountPixelOptions {
|
|
14
|
+
/** Visual style. Default `"badge"`. */
|
|
15
|
+
style?: PixelStyle;
|
|
16
|
+
/** Placement mode. Default `"floating"`. */
|
|
17
|
+
mode?: PixelMode;
|
|
18
|
+
/**
|
|
19
|
+
* For `placed` mode: a CSS selector string or an Element to mount inside.
|
|
20
|
+
* Ignored in `floating` mode. Defaults to `document.body`.
|
|
21
|
+
*/
|
|
22
|
+
target?: string | Element | null;
|
|
23
|
+
/** Site identifier sent with every beacon. Required for meaningful beacons. */
|
|
24
|
+
siteKey?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Base URL of the fancy-heuristics ingestion host. The pixel POSTs to
|
|
27
|
+
* `${endpoint}/pixel`. If omitted, no beacon is sent.
|
|
28
|
+
*/
|
|
29
|
+
endpoint?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Wordmark/link target. Defaults to `https://particle.academy`.
|
|
32
|
+
* Applies to `badge` (whole chip) and `mark` (glyph).
|
|
33
|
+
*/
|
|
34
|
+
href?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The payload POSTed to `${endpoint}/pixel`. Matches the frozen wire contract. */
|
|
37
|
+
interface PixelBeaconPayload {
|
|
38
|
+
siteKey: string;
|
|
39
|
+
style: PixelStyle;
|
|
40
|
+
mode: PixelMode;
|
|
41
|
+
visible: boolean;
|
|
42
|
+
path: string;
|
|
43
|
+
ts: number;
|
|
44
|
+
}
|
|
45
|
+
/** Handle returned by {@link mountPixel} for programmatic control. */
|
|
46
|
+
interface PixelHandle {
|
|
47
|
+
/** The host element carrying the open shadow root and data markers. */
|
|
48
|
+
readonly host: HTMLElement;
|
|
49
|
+
/** Whether the pixel is currently confirmed visible by the IntersectionObserver. */
|
|
50
|
+
readonly visible: boolean;
|
|
51
|
+
/** Resolved options. */
|
|
52
|
+
readonly options: Required<Omit<MountPixelOptions, "target">> & {
|
|
53
|
+
target: string | Element | null;
|
|
54
|
+
};
|
|
55
|
+
/** Tear down observers and remove the host element. */
|
|
56
|
+
destroy(): void;
|
|
57
|
+
}
|
|
58
|
+
/** Detail of the `fancy-pixel:shown` CustomEvent dispatched on `document`. */
|
|
59
|
+
interface PixelShownEventDetail {
|
|
60
|
+
style: PixelStyle;
|
|
61
|
+
mode: PixelMode;
|
|
62
|
+
siteKey: string;
|
|
63
|
+
host: HTMLElement;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mount a Fancy UI pixel into the page.
|
|
68
|
+
*
|
|
69
|
+
* Renders the chosen style inside an open Shadow DOM (host CSS cannot hide it),
|
|
70
|
+
* wires an IntersectionObserver to confirm true on-screen visibility, dispatches
|
|
71
|
+
* a `fancy-pixel:shown` CustomEvent on first visibility, and POSTs a liveness
|
|
72
|
+
* beacon to `${endpoint}/pixel` on mount and on every visibility change.
|
|
73
|
+
*/
|
|
74
|
+
declare function mountPixel(opts?: MountPixelOptions): PixelHandle;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Auto-mount from the script tag's data-* attributes if present.
|
|
78
|
+
* Safe to call multiple times; mounts at most once per call when config exists.
|
|
79
|
+
*/
|
|
80
|
+
declare function autoInit(): PixelHandle | null;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Send a pixel liveness/visibility beacon to `${endpoint}/pixel`.
|
|
84
|
+
*
|
|
85
|
+
* Prefers `navigator.sendBeacon` (survives page unload, non-blocking),
|
|
86
|
+
* falling back to `fetch(..., { keepalive: true })`. Both paths POST JSON.
|
|
87
|
+
* Failures are swallowed — a missing beacon must never break the host page.
|
|
88
|
+
*
|
|
89
|
+
* @returns `true` if the beacon was handed off to the transport.
|
|
90
|
+
*/
|
|
91
|
+
declare function sendPixelBeacon(endpoint: string, payload: PixelBeaconPayload): boolean;
|
|
92
|
+
|
|
93
|
+
/** Positioning styles for the host element, by mode. */
|
|
94
|
+
declare function hostStyleFor(mode: PixelMode): string;
|
|
95
|
+
/**
|
|
96
|
+
* Create the host element + open shadow root for a pixel.
|
|
97
|
+
*
|
|
98
|
+
* The host carries the stable, host-CSS-proof markers:
|
|
99
|
+
* - `data-fancy-pixel` — the stable agent/scanner handle
|
|
100
|
+
* - `data-fancy-badge` — the marker ScanShowcaseSubmission detects
|
|
101
|
+
* - `data-fancy-pixel-style` — the active style
|
|
102
|
+
*
|
|
103
|
+
* @returns the host element (shadow root already populated).
|
|
104
|
+
*/
|
|
105
|
+
declare function createPixelHost(style: PixelStyle, mode: PixelMode, href: string): HTMLElement;
|
|
106
|
+
|
|
107
|
+
export { type MountPixelOptions, type PixelBeaconPayload, type PixelHandle, type PixelMode, type PixelShownEventDetail, type PixelStyle, autoInit, createPixelHost, hostStyleFor, mountPixel, sendPixelBeacon };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for @particle-academy/fancy-pixel.
|
|
3
|
+
*
|
|
4
|
+
* The pixel is an embeddable verification badge + liveness beacon. Three
|
|
5
|
+
* visual styles, two placement modes. All styles emit the same
|
|
6
|
+
* `data-fancy-badge` marker the showcase scanner already detects.
|
|
7
|
+
*/
|
|
8
|
+
/** Visual style of the pixel. */
|
|
9
|
+
type PixelStyle = "badge" | "mark" | "beacon";
|
|
10
|
+
/** Placement mode. `placed` mounts inline at a target; `floating` pins a fixed corner. */
|
|
11
|
+
type PixelMode = "placed" | "floating";
|
|
12
|
+
/** Options accepted by {@link mountPixel}. */
|
|
13
|
+
interface MountPixelOptions {
|
|
14
|
+
/** Visual style. Default `"badge"`. */
|
|
15
|
+
style?: PixelStyle;
|
|
16
|
+
/** Placement mode. Default `"floating"`. */
|
|
17
|
+
mode?: PixelMode;
|
|
18
|
+
/**
|
|
19
|
+
* For `placed` mode: a CSS selector string or an Element to mount inside.
|
|
20
|
+
* Ignored in `floating` mode. Defaults to `document.body`.
|
|
21
|
+
*/
|
|
22
|
+
target?: string | Element | null;
|
|
23
|
+
/** Site identifier sent with every beacon. Required for meaningful beacons. */
|
|
24
|
+
siteKey?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Base URL of the fancy-heuristics ingestion host. The pixel POSTs to
|
|
27
|
+
* `${endpoint}/pixel`. If omitted, no beacon is sent.
|
|
28
|
+
*/
|
|
29
|
+
endpoint?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Wordmark/link target. Defaults to `https://particle.academy`.
|
|
32
|
+
* Applies to `badge` (whole chip) and `mark` (glyph).
|
|
33
|
+
*/
|
|
34
|
+
href?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The payload POSTed to `${endpoint}/pixel`. Matches the frozen wire contract. */
|
|
37
|
+
interface PixelBeaconPayload {
|
|
38
|
+
siteKey: string;
|
|
39
|
+
style: PixelStyle;
|
|
40
|
+
mode: PixelMode;
|
|
41
|
+
visible: boolean;
|
|
42
|
+
path: string;
|
|
43
|
+
ts: number;
|
|
44
|
+
}
|
|
45
|
+
/** Handle returned by {@link mountPixel} for programmatic control. */
|
|
46
|
+
interface PixelHandle {
|
|
47
|
+
/** The host element carrying the open shadow root and data markers. */
|
|
48
|
+
readonly host: HTMLElement;
|
|
49
|
+
/** Whether the pixel is currently confirmed visible by the IntersectionObserver. */
|
|
50
|
+
readonly visible: boolean;
|
|
51
|
+
/** Resolved options. */
|
|
52
|
+
readonly options: Required<Omit<MountPixelOptions, "target">> & {
|
|
53
|
+
target: string | Element | null;
|
|
54
|
+
};
|
|
55
|
+
/** Tear down observers and remove the host element. */
|
|
56
|
+
destroy(): void;
|
|
57
|
+
}
|
|
58
|
+
/** Detail of the `fancy-pixel:shown` CustomEvent dispatched on `document`. */
|
|
59
|
+
interface PixelShownEventDetail {
|
|
60
|
+
style: PixelStyle;
|
|
61
|
+
mode: PixelMode;
|
|
62
|
+
siteKey: string;
|
|
63
|
+
host: HTMLElement;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mount a Fancy UI pixel into the page.
|
|
68
|
+
*
|
|
69
|
+
* Renders the chosen style inside an open Shadow DOM (host CSS cannot hide it),
|
|
70
|
+
* wires an IntersectionObserver to confirm true on-screen visibility, dispatches
|
|
71
|
+
* a `fancy-pixel:shown` CustomEvent on first visibility, and POSTs a liveness
|
|
72
|
+
* beacon to `${endpoint}/pixel` on mount and on every visibility change.
|
|
73
|
+
*/
|
|
74
|
+
declare function mountPixel(opts?: MountPixelOptions): PixelHandle;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Auto-mount from the script tag's data-* attributes if present.
|
|
78
|
+
* Safe to call multiple times; mounts at most once per call when config exists.
|
|
79
|
+
*/
|
|
80
|
+
declare function autoInit(): PixelHandle | null;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Send a pixel liveness/visibility beacon to `${endpoint}/pixel`.
|
|
84
|
+
*
|
|
85
|
+
* Prefers `navigator.sendBeacon` (survives page unload, non-blocking),
|
|
86
|
+
* falling back to `fetch(..., { keepalive: true })`. Both paths POST JSON.
|
|
87
|
+
* Failures are swallowed — a missing beacon must never break the host page.
|
|
88
|
+
*
|
|
89
|
+
* @returns `true` if the beacon was handed off to the transport.
|
|
90
|
+
*/
|
|
91
|
+
declare function sendPixelBeacon(endpoint: string, payload: PixelBeaconPayload): boolean;
|
|
92
|
+
|
|
93
|
+
/** Positioning styles for the host element, by mode. */
|
|
94
|
+
declare function hostStyleFor(mode: PixelMode): string;
|
|
95
|
+
/**
|
|
96
|
+
* Create the host element + open shadow root for a pixel.
|
|
97
|
+
*
|
|
98
|
+
* The host carries the stable, host-CSS-proof markers:
|
|
99
|
+
* - `data-fancy-pixel` — the stable agent/scanner handle
|
|
100
|
+
* - `data-fancy-badge` — the marker ScanShowcaseSubmission detects
|
|
101
|
+
* - `data-fancy-pixel-style` — the active style
|
|
102
|
+
*
|
|
103
|
+
* @returns the host element (shadow root already populated).
|
|
104
|
+
*/
|
|
105
|
+
declare function createPixelHost(style: PixelStyle, mode: PixelMode, href: string): HTMLElement;
|
|
106
|
+
|
|
107
|
+
export { type MountPixelOptions, type PixelBeaconPayload, type PixelHandle, type PixelMode, type PixelShownEventDetail, type PixelStyle, autoInit, createPixelHost, hostStyleFor, mountPixel, sendPixelBeacon };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// src/beacon.ts
|
|
2
|
+
function sendPixelBeacon(endpoint, payload) {
|
|
3
|
+
if (!endpoint) return false;
|
|
4
|
+
const url = endpoint.replace(/\/+$/, "") + "/pixel";
|
|
5
|
+
const body = JSON.stringify(payload);
|
|
6
|
+
try {
|
|
7
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
8
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
9
|
+
if (navigator.sendBeacon(url, blob)) return true;
|
|
10
|
+
}
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
if (typeof fetch === "function") {
|
|
15
|
+
void fetch(url, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
body,
|
|
19
|
+
keepalive: true,
|
|
20
|
+
// Cross-origin ingestion: we never read the response, so opaque is fine.
|
|
21
|
+
mode: "cors",
|
|
22
|
+
credentials: "omit"
|
|
23
|
+
}).catch(() => {
|
|
24
|
+
});
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/render.ts
|
|
33
|
+
function logoGlyph(size) {
|
|
34
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" style="display:block;flex:0 0 auto;">
|
|
35
|
+
<rect x="2" y="2" width="20" height="20" rx="6" fill="url(#fp-grad)"/>
|
|
36
|
+
<circle cx="9" cy="9" r="3" fill="#fff"/>
|
|
37
|
+
<circle cx="16.5" cy="15" r="1.6" fill="#fff" opacity="0.85"/>
|
|
38
|
+
<defs>
|
|
39
|
+
<linearGradient id="fp-grad" x1="2" y1="2" x2="22" y2="22" gradientUnits="userSpaceOnUse">
|
|
40
|
+
<stop stop-color="#7c5cff"/>
|
|
41
|
+
<stop offset="1" stop-color="#22d3ee"/>
|
|
42
|
+
</linearGradient>
|
|
43
|
+
</defs>
|
|
44
|
+
</svg>`;
|
|
45
|
+
}
|
|
46
|
+
var RESET = `
|
|
47
|
+
:host { all: initial; }
|
|
48
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
49
|
+
.fp-root {
|
|
50
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
51
|
+
line-height: 1;
|
|
52
|
+
-webkit-font-smoothing: antialiased;
|
|
53
|
+
}
|
|
54
|
+
.fp-link {
|
|
55
|
+
text-decoration: none;
|
|
56
|
+
color: inherit;
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
@keyframes fp-pulse {
|
|
62
|
+
0% { transform: scale(0.85); opacity: 0.55; }
|
|
63
|
+
50% { transform: scale(1); opacity: 1; }
|
|
64
|
+
100% { transform: scale(0.85); opacity: 0.55; }
|
|
65
|
+
}
|
|
66
|
+
@media (prefers-reduced-motion: reduce) {
|
|
67
|
+
.fp-pulse, .fp-pulse-ring { animation: none !important; }
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
function hostStyleFor(mode) {
|
|
71
|
+
if (mode === "floating") {
|
|
72
|
+
return [
|
|
73
|
+
"position:fixed",
|
|
74
|
+
"right:16px",
|
|
75
|
+
"bottom:16px",
|
|
76
|
+
"z-index:2147483647",
|
|
77
|
+
// A fixed corner element must not capture clicks across the page; only
|
|
78
|
+
// the chip itself is interactive (pointer-events re-enabled inside).
|
|
79
|
+
"pointer-events:none"
|
|
80
|
+
].join(";");
|
|
81
|
+
}
|
|
82
|
+
return ["display:inline-block", "vertical-align:middle"].join(";");
|
|
83
|
+
}
|
|
84
|
+
function innerFor(style, href) {
|
|
85
|
+
const linkAttrs = `class="fp-link" href="${href}" target="_blank" rel="noopener noreferrer" part="link" data-fancy-pixel-link`;
|
|
86
|
+
if (style === "mark") {
|
|
87
|
+
return `<style>${RESET}
|
|
88
|
+
.fp-mark {
|
|
89
|
+
pointer-events:auto;
|
|
90
|
+
display:inline-flex; align-items:center; justify-content:center;
|
|
91
|
+
padding:6px; border-radius:10px;
|
|
92
|
+
background:rgba(17,17,27,0.92);
|
|
93
|
+
box-shadow:0 2px 10px rgba(0,0,0,0.25);
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
96
|
+
<div class="fp-root" part="root">
|
|
97
|
+
<a ${linkAttrs} aria-label="Powered by Fancy UI">
|
|
98
|
+
<span class="fp-mark">${logoGlyph(22)}</span>
|
|
99
|
+
</a>
|
|
100
|
+
</div>`;
|
|
101
|
+
}
|
|
102
|
+
if (style === "beacon") {
|
|
103
|
+
return `<style>${RESET}
|
|
104
|
+
.fp-beacon {
|
|
105
|
+
pointer-events:auto;
|
|
106
|
+
position:relative; display:inline-flex;
|
|
107
|
+
width:14px; height:14px;
|
|
108
|
+
}
|
|
109
|
+
.fp-dot {
|
|
110
|
+
position:absolute; inset:3px;
|
|
111
|
+
border-radius:50%;
|
|
112
|
+
background:linear-gradient(135deg,#7c5cff,#22d3ee);
|
|
113
|
+
}
|
|
114
|
+
.fp-pulse-ring {
|
|
115
|
+
position:absolute; inset:0;
|
|
116
|
+
border-radius:50%;
|
|
117
|
+
background:radial-gradient(circle,#22d3ee 0%,rgba(34,211,238,0) 70%);
|
|
118
|
+
animation:fp-pulse 1.8s ease-in-out infinite;
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
121
|
+
<div class="fp-root" part="root">
|
|
122
|
+
<a ${linkAttrs} aria-label="Powered by Fancy UI">
|
|
123
|
+
<span class="fp-beacon">
|
|
124
|
+
<span class="fp-pulse-ring fp-pulse"></span>
|
|
125
|
+
<span class="fp-dot"></span>
|
|
126
|
+
</span>
|
|
127
|
+
</a>
|
|
128
|
+
</div>`;
|
|
129
|
+
}
|
|
130
|
+
return `<style>${RESET}
|
|
131
|
+
.fp-badge {
|
|
132
|
+
pointer-events:auto;
|
|
133
|
+
display:inline-flex; align-items:center; gap:8px;
|
|
134
|
+
padding:7px 11px; border-radius:999px;
|
|
135
|
+
background:rgba(17,17,27,0.92);
|
|
136
|
+
color:#f5f5fa;
|
|
137
|
+
font-size:12px; font-weight:600; letter-spacing:0.01em;
|
|
138
|
+
box-shadow:0 2px 12px rgba(0,0,0,0.28);
|
|
139
|
+
border:1px solid rgba(124,92,255,0.35);
|
|
140
|
+
transition:transform 120ms ease;
|
|
141
|
+
}
|
|
142
|
+
.fp-badge:hover { transform:translateY(-1px); }
|
|
143
|
+
.fp-by { opacity:0.7; font-weight:500; }
|
|
144
|
+
.fp-name {
|
|
145
|
+
background:linear-gradient(135deg,#a78bfa,#22d3ee);
|
|
146
|
+
-webkit-background-clip:text; background-clip:text;
|
|
147
|
+
-webkit-text-fill-color:transparent; color:transparent;
|
|
148
|
+
font-weight:700;
|
|
149
|
+
}
|
|
150
|
+
</style>
|
|
151
|
+
<div class="fp-root" part="root">
|
|
152
|
+
<a ${linkAttrs} aria-label="Powered by Fancy UI">
|
|
153
|
+
<span class="fp-badge">
|
|
154
|
+
${logoGlyph(16)}
|
|
155
|
+
<span><span class="fp-by">Powered by</span> <span class="fp-name">Fancy UI</span></span>
|
|
156
|
+
</span>
|
|
157
|
+
</a>
|
|
158
|
+
</div>`;
|
|
159
|
+
}
|
|
160
|
+
function createPixelHost(style, mode, href) {
|
|
161
|
+
const host = document.createElement("div");
|
|
162
|
+
host.setAttribute("data-fancy-pixel", "");
|
|
163
|
+
host.setAttribute("data-fancy-badge", "");
|
|
164
|
+
host.setAttribute("data-fancy-pixel-style", style);
|
|
165
|
+
host.setAttribute("data-fancy-pixel-mode", mode);
|
|
166
|
+
host.style.cssText = hostStyleFor(mode);
|
|
167
|
+
const root = host.attachShadow({ mode: "open" });
|
|
168
|
+
root.innerHTML = innerFor(style, href);
|
|
169
|
+
return host;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/mount.ts
|
|
173
|
+
var DEFAULT_HREF = "https://particle.academy";
|
|
174
|
+
var STYLES = ["badge", "mark", "beacon"];
|
|
175
|
+
var MODES = ["placed", "floating"];
|
|
176
|
+
function resolveTarget(target) {
|
|
177
|
+
if (target instanceof Element) return target;
|
|
178
|
+
if (typeof target === "string") {
|
|
179
|
+
const found = document.querySelector(target);
|
|
180
|
+
if (found) return found;
|
|
181
|
+
}
|
|
182
|
+
return document.body;
|
|
183
|
+
}
|
|
184
|
+
function currentPath() {
|
|
185
|
+
return typeof location !== "undefined" ? location.pathname : "/";
|
|
186
|
+
}
|
|
187
|
+
function mountPixel(opts = {}) {
|
|
188
|
+
const style = STYLES.includes(opts.style) ? opts.style : "badge";
|
|
189
|
+
const mode = MODES.includes(opts.mode) ? opts.mode : "floating";
|
|
190
|
+
const siteKey = opts.siteKey ?? "";
|
|
191
|
+
const endpoint = opts.endpoint ?? "";
|
|
192
|
+
const href = opts.href ?? DEFAULT_HREF;
|
|
193
|
+
const target = opts.target ?? null;
|
|
194
|
+
const host = createPixelHost(style, mode, href);
|
|
195
|
+
if (mode === "floating") {
|
|
196
|
+
document.body.appendChild(host);
|
|
197
|
+
} else {
|
|
198
|
+
resolveTarget(target).appendChild(host);
|
|
199
|
+
}
|
|
200
|
+
let visible = false;
|
|
201
|
+
let shownDispatched = false;
|
|
202
|
+
let destroyed = false;
|
|
203
|
+
function beacon() {
|
|
204
|
+
if (!endpoint) return;
|
|
205
|
+
sendPixelBeacon(endpoint, {
|
|
206
|
+
siteKey,
|
|
207
|
+
style,
|
|
208
|
+
mode,
|
|
209
|
+
visible,
|
|
210
|
+
path: currentPath(),
|
|
211
|
+
ts: Date.now()
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function dispatchShownOnce() {
|
|
215
|
+
if (shownDispatched) return;
|
|
216
|
+
shownDispatched = true;
|
|
217
|
+
const detail = { style, mode, siteKey, host };
|
|
218
|
+
document.dispatchEvent(
|
|
219
|
+
new CustomEvent("fancy-pixel:shown", { detail })
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
let observer = null;
|
|
223
|
+
function setVisible(next) {
|
|
224
|
+
if (destroyed || next === visible) return;
|
|
225
|
+
visible = next;
|
|
226
|
+
if (visible) dispatchShownOnce();
|
|
227
|
+
beacon();
|
|
228
|
+
}
|
|
229
|
+
if (typeof IntersectionObserver === "function") {
|
|
230
|
+
observer = new IntersectionObserver(
|
|
231
|
+
(entries) => {
|
|
232
|
+
const entry = entries[entries.length - 1];
|
|
233
|
+
if (!entry) return;
|
|
234
|
+
const rect = entry.boundingClientRect;
|
|
235
|
+
const hasSize = rect.width > 0 && rect.height > 0;
|
|
236
|
+
setVisible(entry.isIntersecting && entry.intersectionRatio > 0 && hasSize);
|
|
237
|
+
},
|
|
238
|
+
{ threshold: [0, 0.01, 0.5] }
|
|
239
|
+
);
|
|
240
|
+
observer.observe(host);
|
|
241
|
+
} else {
|
|
242
|
+
setVisible(true);
|
|
243
|
+
}
|
|
244
|
+
beacon();
|
|
245
|
+
const resolved = {
|
|
246
|
+
style,
|
|
247
|
+
mode,
|
|
248
|
+
siteKey,
|
|
249
|
+
endpoint,
|
|
250
|
+
href,
|
|
251
|
+
target
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
host,
|
|
255
|
+
get visible() {
|
|
256
|
+
return visible;
|
|
257
|
+
},
|
|
258
|
+
options: resolved,
|
|
259
|
+
destroy() {
|
|
260
|
+
if (destroyed) return;
|
|
261
|
+
destroyed = true;
|
|
262
|
+
observer?.disconnect();
|
|
263
|
+
observer = null;
|
|
264
|
+
host.remove();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/autoinit.ts
|
|
270
|
+
function readScriptConfig() {
|
|
271
|
+
if (typeof document === "undefined") return null;
|
|
272
|
+
let el = document.currentScript;
|
|
273
|
+
if (!el || !el.dataset || el.dataset.site === void 0) {
|
|
274
|
+
const candidates = document.querySelectorAll(
|
|
275
|
+
"script[data-site][src*='fancy-pixel'], script[data-fancy-pixel]"
|
|
276
|
+
);
|
|
277
|
+
el = candidates[candidates.length - 1] ?? null;
|
|
278
|
+
}
|
|
279
|
+
if (!el) return null;
|
|
280
|
+
const ds = el.dataset;
|
|
281
|
+
if (ds.style === void 0 && ds.mode === void 0 && ds.site === void 0 && ds.endpoint === void 0 && ds.fancyPixel === void 0) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
style: ds.style,
|
|
286
|
+
mode: ds.mode,
|
|
287
|
+
siteKey: ds.site,
|
|
288
|
+
endpoint: ds.endpoint,
|
|
289
|
+
target: ds.target ?? null,
|
|
290
|
+
href: ds.href
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function autoInit() {
|
|
294
|
+
const config = readScriptConfig();
|
|
295
|
+
if (!config) return null;
|
|
296
|
+
const run = () => mountPixel(config);
|
|
297
|
+
if (typeof document !== "undefined" && document.readyState === "loading") {
|
|
298
|
+
let handle = null;
|
|
299
|
+
document.addEventListener(
|
|
300
|
+
"DOMContentLoaded",
|
|
301
|
+
() => {
|
|
302
|
+
handle = run();
|
|
303
|
+
},
|
|
304
|
+
{ once: true }
|
|
305
|
+
);
|
|
306
|
+
return handle;
|
|
307
|
+
}
|
|
308
|
+
return run();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export { autoInit, createPixelHost, hostStyleFor, mountPixel, sendPixelBeacon };
|
|
312
|
+
//# sourceMappingURL=index.js.map
|
|
313
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/beacon.ts","../src/render.ts","../src/mount.ts","../src/autoinit.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PixelBeaconPayload } from \"./types.js\";\n\n/**\n * Send a pixel liveness/visibility beacon to `${endpoint}/pixel`.\n *\n * Prefers `navigator.sendBeacon` (survives page unload, non-blocking),\n * falling back to `fetch(..., { keepalive: true })`. Both paths POST JSON.\n * Failures are swallowed — a missing beacon must never break the host page.\n *\n * @returns `true` if the beacon was handed off to the transport.\n */\nexport function sendPixelBeacon(\n endpoint: string,\n payload: PixelBeaconPayload,\n): boolean {\n if (!endpoint) return false;\n\n const url = endpoint.replace(/\\/+$/, \"\") + \"/pixel\";\n const body = JSON.stringify(payload);\n\n try {\n if (\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\"\n ) {\n // sendBeacon with a Blob preserves the JSON content type for the server.\n const blob = new Blob([body], { type: \"application/json\" });\n if (navigator.sendBeacon(url, blob)) return true;\n // sendBeacon returns false if the UA refused to queue it — fall through.\n }\n } catch {\n /* fall through to fetch */\n }\n\n try {\n if (typeof fetch === \"function\") {\n void fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n keepalive: true,\n // Cross-origin ingestion: we never read the response, so opaque is fine.\n mode: \"cors\",\n credentials: \"omit\",\n }).catch(() => {\n /* swallow */\n });\n return true;\n }\n } catch {\n /* swallow */\n }\n\n return false;\n}\n","import type { PixelMode, PixelStyle } from \"./types.js\";\n\n/**\n * The Fancy UI logo glyph as an inline SVG string. A rounded-square mark with\n * an orbiting particle — self-contained, no external asset, scales cleanly.\n */\nfunction logoGlyph(size: number): string {\n return `<svg width=\"${size}\" height=\"${size}\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" focusable=\"false\" style=\"display:block;flex:0 0 auto;\">\n <rect x=\"2\" y=\"2\" width=\"20\" height=\"20\" rx=\"6\" fill=\"url(#fp-grad)\"/>\n <circle cx=\"9\" cy=\"9\" r=\"3\" fill=\"#fff\"/>\n <circle cx=\"16.5\" cy=\"15\" r=\"1.6\" fill=\"#fff\" opacity=\"0.85\"/>\n <defs>\n <linearGradient id=\"fp-grad\" x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#7c5cff\"/>\n <stop offset=\"1\" stop-color=\"#22d3ee\"/>\n </linearGradient>\n </defs>\n</svg>`;\n}\n\n/**\n * Reset rules applied to the shadow root host's internal wrapper. Because we\n * render inside an open Shadow DOM, host page CSS cannot reach in to hide or\n * restyle these — visibility is part of the verification contract.\n */\nconst RESET = `\n :host { all: initial; }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n .fp-root {\n font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n }\n .fp-link {\n text-decoration: none;\n color: inherit;\n display: inline-flex;\n align-items: center;\n cursor: pointer;\n }\n @keyframes fp-pulse {\n 0% { transform: scale(0.85); opacity: 0.55; }\n 50% { transform: scale(1); opacity: 1; }\n 100% { transform: scale(0.85); opacity: 0.55; }\n }\n @media (prefers-reduced-motion: reduce) {\n .fp-pulse, .fp-pulse-ring { animation: none !important; }\n }\n`;\n\n/** Positioning styles for the host element, by mode. */\nexport function hostStyleFor(mode: PixelMode): string {\n if (mode === \"floating\") {\n return [\n \"position:fixed\",\n \"right:16px\",\n \"bottom:16px\",\n \"z-index:2147483647\",\n // A fixed corner element must not capture clicks across the page; only\n // the chip itself is interactive (pointer-events re-enabled inside).\n \"pointer-events:none\",\n ].join(\";\");\n }\n // placed: inline, flows with the target. Stays interactive.\n return [\"display:inline-block\", \"vertical-align:middle\"].join(\";\");\n}\n\n/** Build the inner HTML of the shadow root for a given style. */\nfunction innerFor(style: PixelStyle, href: string): string {\n const linkAttrs = `class=\"fp-link\" href=\"${href}\" target=\"_blank\" rel=\"noopener noreferrer\" part=\"link\" data-fancy-pixel-link`;\n\n if (style === \"mark\") {\n return `<style>${RESET}\n .fp-mark {\n pointer-events:auto;\n display:inline-flex; align-items:center; justify-content:center;\n padding:6px; border-radius:10px;\n background:rgba(17,17,27,0.92);\n box-shadow:0 2px 10px rgba(0,0,0,0.25);\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-mark\">${logoGlyph(22)}</span>\n </a>\n </div>`;\n }\n\n if (style === \"beacon\") {\n return `<style>${RESET}\n .fp-beacon {\n pointer-events:auto;\n position:relative; display:inline-flex;\n width:14px; height:14px;\n }\n .fp-dot {\n position:absolute; inset:3px;\n border-radius:50%;\n background:linear-gradient(135deg,#7c5cff,#22d3ee);\n }\n .fp-pulse-ring {\n position:absolute; inset:0;\n border-radius:50%;\n background:radial-gradient(circle,#22d3ee 0%,rgba(34,211,238,0) 70%);\n animation:fp-pulse 1.8s ease-in-out infinite;\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-beacon\">\n <span class=\"fp-pulse-ring fp-pulse\"></span>\n <span class=\"fp-dot\"></span>\n </span>\n </a>\n </div>`;\n }\n\n // badge (default): wordmark + glyph chip.\n return `<style>${RESET}\n .fp-badge {\n pointer-events:auto;\n display:inline-flex; align-items:center; gap:8px;\n padding:7px 11px; border-radius:999px;\n background:rgba(17,17,27,0.92);\n color:#f5f5fa;\n font-size:12px; font-weight:600; letter-spacing:0.01em;\n box-shadow:0 2px 12px rgba(0,0,0,0.28);\n border:1px solid rgba(124,92,255,0.35);\n transition:transform 120ms ease;\n }\n .fp-badge:hover { transform:translateY(-1px); }\n .fp-by { opacity:0.7; font-weight:500; }\n .fp-name {\n background:linear-gradient(135deg,#a78bfa,#22d3ee);\n -webkit-background-clip:text; background-clip:text;\n -webkit-text-fill-color:transparent; color:transparent;\n font-weight:700;\n }\n </style>\n <div class=\"fp-root\" part=\"root\">\n <a ${linkAttrs} aria-label=\"Powered by Fancy UI\">\n <span class=\"fp-badge\">\n ${logoGlyph(16)}\n <span><span class=\"fp-by\">Powered by</span> <span class=\"fp-name\">Fancy UI</span></span>\n </span>\n </a>\n </div>`;\n}\n\n/**\n * Create the host element + open shadow root for a pixel.\n *\n * The host carries the stable, host-CSS-proof markers:\n * - `data-fancy-pixel` — the stable agent/scanner handle\n * - `data-fancy-badge` — the marker ScanShowcaseSubmission detects\n * - `data-fancy-pixel-style` — the active style\n *\n * @returns the host element (shadow root already populated).\n */\nexport function createPixelHost(\n style: PixelStyle,\n mode: PixelMode,\n href: string,\n): HTMLElement {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-fancy-pixel\", \"\");\n host.setAttribute(\"data-fancy-badge\", \"\");\n host.setAttribute(\"data-fancy-pixel-style\", style);\n host.setAttribute(\"data-fancy-pixel-mode\", mode);\n host.style.cssText = hostStyleFor(mode);\n\n const root = host.attachShadow({ mode: \"open\" });\n root.innerHTML = innerFor(style, href);\n\n return host;\n}\n","import { sendPixelBeacon } from \"./beacon.js\";\nimport { createPixelHost } from \"./render.js\";\nimport type {\n MountPixelOptions,\n PixelHandle,\n PixelMode,\n PixelShownEventDetail,\n PixelStyle,\n} from \"./types.js\";\n\nconst DEFAULT_HREF = \"https://particle.academy\";\nconst STYLES: PixelStyle[] = [\"badge\", \"mark\", \"beacon\"];\nconst MODES: PixelMode[] = [\"placed\", \"floating\"];\n\nfunction resolveTarget(target: string | Element | null | undefined): Element {\n if (target instanceof Element) return target;\n if (typeof target === \"string\") {\n const found = document.querySelector(target);\n if (found) return found;\n }\n return document.body;\n}\n\nfunction currentPath(): string {\n return typeof location !== \"undefined\" ? location.pathname : \"/\";\n}\n\n/**\n * Mount a Fancy UI pixel into the page.\n *\n * Renders the chosen style inside an open Shadow DOM (host CSS cannot hide it),\n * wires an IntersectionObserver to confirm true on-screen visibility, dispatches\n * a `fancy-pixel:shown` CustomEvent on first visibility, and POSTs a liveness\n * beacon to `${endpoint}/pixel` on mount and on every visibility change.\n */\nexport function mountPixel(opts: MountPixelOptions = {}): PixelHandle {\n const style: PixelStyle = STYLES.includes(opts.style as PixelStyle)\n ? (opts.style as PixelStyle)\n : \"badge\";\n const mode: PixelMode = MODES.includes(opts.mode as PixelMode)\n ? (opts.mode as PixelMode)\n : \"floating\";\n const siteKey = opts.siteKey ?? \"\";\n const endpoint = opts.endpoint ?? \"\";\n const href = opts.href ?? DEFAULT_HREF;\n const target = opts.target ?? null;\n\n const host = createPixelHost(style, mode, href);\n\n // Mount: floating always pins to body; placed flows at the target.\n if (mode === \"floating\") {\n document.body.appendChild(host);\n } else {\n resolveTarget(target).appendChild(host);\n }\n\n let visible = false;\n let shownDispatched = false;\n let destroyed = false;\n\n function beacon(): void {\n if (!endpoint) return;\n sendPixelBeacon(endpoint, {\n siteKey,\n style,\n mode,\n visible,\n path: currentPath(),\n ts: Date.now(),\n });\n }\n\n function dispatchShownOnce(): void {\n if (shownDispatched) return;\n shownDispatched = true;\n const detail: PixelShownEventDetail = { style, mode, siteKey, host };\n document.dispatchEvent(\n new CustomEvent<PixelShownEventDetail>(\"fancy-pixel:shown\", { detail }),\n );\n }\n\n // Visibility verification. IntersectionObserver confirms the badge is truly\n // on-screen — not display:none, not 0-size, not scrolled out of view. When\n // unavailable (old/headless UAs) we optimistically assume visible so the\n // liveness beacon still fires.\n let observer: IntersectionObserver | null = null;\n\n function setVisible(next: boolean): void {\n if (destroyed || next === visible) return;\n visible = next;\n if (visible) dispatchShownOnce();\n beacon();\n }\n\n if (typeof IntersectionObserver === \"function\") {\n observer = new IntersectionObserver(\n (entries) => {\n const entry = entries[entries.length - 1];\n if (!entry) return;\n // intersectionRatio > 0 with a non-zero box = genuinely rendered.\n const rect = entry.boundingClientRect;\n const hasSize = rect.width > 0 && rect.height > 0;\n setVisible(entry.isIntersecting && entry.intersectionRatio > 0 && hasSize);\n },\n { threshold: [0, 0.01, 0.5] },\n );\n observer.observe(host);\n } else {\n // No IO support — fire an optimistic visible beacon on mount.\n setVisible(true);\n }\n\n // Always emit a mount beacon so the server logs liveness even before the\n // observer reports (visible defaults false until proven).\n beacon();\n\n const resolved: PixelHandle[\"options\"] = {\n style,\n mode,\n siteKey,\n endpoint,\n href,\n target,\n };\n\n return {\n host,\n get visible() {\n return visible;\n },\n options: resolved,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n observer?.disconnect();\n observer = null;\n host.remove();\n },\n };\n}\n","import { mountPixel } from \"./mount.js\";\nimport type { MountPixelOptions, PixelHandle } from \"./types.js\";\n\n/**\n * Read pixel config off the currently-executing <script> element's data-*\n * attributes. Used by the IIFE global bundle so a single <script> tag both\n * loads and auto-mounts the pixel:\n *\n * <script src=\".../fancy-pixel.global.min.js\"\n * data-style=\"badge\" data-mode=\"floating\"\n * data-site=\"KEY\" data-endpoint=\"https://host/heuristics\"></script>\n */\nfunction readScriptConfig(): MountPixelOptions | null {\n if (typeof document === \"undefined\") return null;\n\n // `document.currentScript` is the running script during initial parse.\n // Fall back to the last <script> carrying our data-site marker.\n let el = document.currentScript as HTMLScriptElement | null;\n if (!el || !el.dataset || el.dataset.site === undefined) {\n const candidates = document.querySelectorAll<HTMLScriptElement>(\n \"script[data-site][src*='fancy-pixel'], script[data-fancy-pixel]\",\n );\n el = candidates[candidates.length - 1] ?? null;\n }\n if (!el) return null;\n\n const ds = el.dataset;\n // Only auto-init if at least one fancy-pixel attribute is present.\n if (\n ds.style === undefined &&\n ds.mode === undefined &&\n ds.site === undefined &&\n ds.endpoint === undefined &&\n ds.fancyPixel === undefined\n ) {\n return null;\n }\n\n return {\n style: ds.style as MountPixelOptions[\"style\"],\n mode: ds.mode as MountPixelOptions[\"mode\"],\n siteKey: ds.site,\n endpoint: ds.endpoint,\n target: ds.target ?? null,\n href: ds.href,\n };\n}\n\n/**\n * Auto-mount from the script tag's data-* attributes if present.\n * Safe to call multiple times; mounts at most once per call when config exists.\n */\nexport function autoInit(): PixelHandle | null {\n const config = readScriptConfig();\n if (!config) return null;\n\n const run = (): PixelHandle => mountPixel(config);\n\n if (typeof document !== \"undefined\" && document.readyState === \"loading\") {\n // Defer to DOMContentLoaded so `placed` targets exist.\n let handle: PixelHandle | null = null;\n document.addEventListener(\n \"DOMContentLoaded\",\n () => {\n handle = run();\n },\n { once: true },\n );\n // Return null now; the handle resolves after DOMContentLoaded.\n return handle;\n }\n\n return run();\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@particle-academy/fancy-pixel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Embeddable Fancy UI verification badge + liveness beacon. Zero-dependency vanilla TS, Shadow-DOM rendered (host CSS can't hide it), with an IntersectionObserver visibility check and a sendBeacon liveness ping. Badge / Mark / Beacon styles, placed / floating modes.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Particle-Academy/fancy-pixel.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/Particle-Academy/fancy-pixel#readme",
|
|
10
|
+
"bugs": "https://github.com/Particle-Academy/fancy-pixel/issues",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"unpkg": "./dist/fancy-pixel.global.min.js",
|
|
16
|
+
"jsdelivr": "./dist/fancy-pixel.global.min.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"require": {
|
|
24
|
+
"types": "./dist/index.d.cts",
|
|
25
|
+
"default": "./dist/index.cjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"./global": "./dist/fancy-pixel.global.min.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"lint": "tsc --noEmit",
|
|
39
|
+
"test": "node --test",
|
|
40
|
+
"clean": "rm -rf dist",
|
|
41
|
+
"prepublishOnly": "tsup"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"fancy-ui",
|
|
45
|
+
"badge",
|
|
46
|
+
"verification",
|
|
47
|
+
"beacon",
|
|
48
|
+
"pixel",
|
|
49
|
+
"shadow-dom",
|
|
50
|
+
"human-plus",
|
|
51
|
+
"agent",
|
|
52
|
+
"analytics"
|
|
53
|
+
],
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"tsup": "^8.5.0",
|
|
56
|
+
"typescript": "^5.8.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"license": "MIT"
|
|
65
|
+
}
|