tell-cecil 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +90 -2
- package/dist/cecil.js +80 -0
- package/dist/cecil.mjs +500 -0
- package/dist/relay.mjs +282 -0
- package/dist/server.mjs +74 -0
- package/dist/types/element.d.ts +7 -0
- package/dist/types/github.d.ts +14 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/privacy.d.ts +9 -0
- package/dist/types/relay.d.ts +34 -0
- package/dist/types/select.d.ts +24 -0
- package/dist/types/types.d.ts +47 -0
- package/dist/types/ui.d.ts +5 -0
- package/package.json +59 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tell Cecil
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,91 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><img src="docs/logo.svg" width="96" height="96" alt="Tell Cecil logo"></p>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<h1 align="center">Tell Cecil</h1>
|
|
4
|
+
|
|
5
|
+
<p align="center"><a href="https://www.npmjs.com/package/tell-cecil"><img src="https://img.shields.io/npm/v/tell-cecil" alt="npm"></a></p>
|
|
6
|
+
|
|
7
|
+
Feedback widget for any website. Visitors pick an element, write a comment, and it lands as a GitHub issue with the selector and page context. No dependencies, about 8 KB gzipped, MIT.
|
|
8
|
+
|
|
9
|
+
> **Alpha.** GitHub only; no screenshots or area selection yet.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
Visitors click **Feedback**, pick an **Element** (mouse, or Tab and Enter) or the whole **Page**, write a comment and send. The widget posts the report to a small relay you run, which files the issue with your GitHub token. Reporters need no GitHub account, and the token never reaches the page.
|
|
16
|
+
|
|
17
|
+
> **Public repo means public reports.** Use a private repo unless anyone may read the comments and page paths.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
**1. Run the relay** (Node 20+) with a [fine-grained token](https://github.com/settings/personal-access-tokens/new) for one repo, Issues: read and write.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
export CECIL_GITHUB_TOKEN=github_pat_...
|
|
25
|
+
export CECIL_GITHUB_REPO=owner/repo
|
|
26
|
+
export CECIL_PROJECT_KEY=pk_abc # public, not a secret
|
|
27
|
+
export CECIL_ORIGINS=https://yoursite.com # exact origins, comma-separated
|
|
28
|
+
npx tell-cecil # http://localhost:8787
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional: `CECIL_LABELS`, `PORT`, and `CECIL_TRUST_PROXY=1` behind your own reverse proxy. The variables can also live in a `.env` file in the current folder.
|
|
32
|
+
|
|
33
|
+
**2. Add the widget.**
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<script src="https://cdn.jsdelivr.net/npm/tell-cecil@0.0.2/dist/cecil.js"
|
|
37
|
+
data-endpoint="https://relay.example.com"
|
|
38
|
+
data-project-key="pk_abc"></script>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or `npm i tell-cecil`, then `import { init } from "tell-cecil"` and `init({ endpoint, projectKey })`.
|
|
42
|
+
|
|
43
|
+
**Reports fail?** The browser console says why. A 403 means a wrong project key or an origin missing from `CECIL_ORIGINS`, which must match the address bar exactly: `http://127.0.0.1:5173` is not `http://localhost:5173`.
|
|
44
|
+
|
|
45
|
+
**Other hosts.** `createRelay` from `tell-cecil/relay` is a `(Request) => Response` function, so it runs in your own server, Cloudflare Workers, Vercel, Deno or Bun. See the [Worker example](examples/cloudflare-worker) and the [Dockerfile](Dockerfile).
|
|
46
|
+
|
|
47
|
+
## Security
|
|
48
|
+
|
|
49
|
+
The origin check stops other websites, not scripts, so the relay also rate limits: 10 reports a minute per IP and 60 per project, in memory. On a public site, add your host's rate limiting too. The endpoint must be https unless it is localhost. Under a strict CSP, allow the relay in `connect-src` and the script's host in `script-src`; styles need no `style-src` exception.
|
|
50
|
+
|
|
51
|
+
## Privacy
|
|
52
|
+
|
|
53
|
+
Tell Cecil never reads input values, editable content or page HTML. It sends the element's selector, role, accessible name, a short text excerpt, a few attributes, ancestors and position, and the page URL without its query string and hash.
|
|
54
|
+
|
|
55
|
+
- `data-cecil-private`: the element and its children can't be picked.
|
|
56
|
+
- `data-cecil-mask`: it can be picked, but its text and name are left out. Form fields and `contenteditable` are masked by default.
|
|
57
|
+
- Your own selectors: `data-exclude=".admin"` and `data-mask=".iban"`, or `privacy: { exclude, mask }` in `init`.
|
|
58
|
+
|
|
59
|
+
## Reference
|
|
60
|
+
|
|
61
|
+
**Widget:** `init(options)`, or data attributes on the script tag. `init` returns a function that removes the widget.
|
|
62
|
+
|
|
63
|
+
| Option | Attribute | |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `endpoint` | `data-endpoint` | Relay URL, may be relative. Required. |
|
|
66
|
+
| `projectKey` | `data-project-key` | Public key the relay maps to a repo. Required. |
|
|
67
|
+
| `user` | `data-user` | Reporter name on the issue (string or function). Unverified. |
|
|
68
|
+
| `privacy.exclude` | `data-exclude` | Selectors that can't be picked. |
|
|
69
|
+
| `privacy.mask` | `data-mask` | Selectors whose text is left out. |
|
|
70
|
+
| `privacy.url` | `data-full-url` | Rewrite the page URL; the attribute sends it in full. |
|
|
71
|
+
|
|
72
|
+
**Relay:** `createRelay(options)`.
|
|
73
|
+
|
|
74
|
+
| Option | Default | |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `projects` | required | Key to `{ origins, github: { repo, token, labels? }, exposeIssueUrl? }`. `exposeIssueUrl: false` hides the issue link from reporters. |
|
|
77
|
+
| `rateLimit` | `{ max: 10, projectMax: 60, windowMs: 60000 }` | Per IP and per project. |
|
|
78
|
+
| `clientIp` | `cf-connecting-ip`, else last `x-forwarded-for` | Client identity for rate limiting. |
|
|
79
|
+
| `fetch`, `now` | globals | For tests. |
|
|
80
|
+
|
|
81
|
+
## Develop
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npm install && npm test
|
|
85
|
+
npm run dev # demo page on http://localhost:5173
|
|
86
|
+
npm run relay # relay from source, beside it
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/dist/cecil.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
var Cecil=(function(W){"use strict";const ft=["script","style","template","input[type=hidden]","[data-cecil-private]","[data-cecil]"],J=["input","textarea","select"],ht=['[contenteditable]:not([contenteditable="false"])',"[data-cecil-mask]"],mt=["id","data-testid","data-test","name","role","type"],gt=["data-testid","data-test"],V=t=>{try{return document.createDocumentFragment().querySelector(t),!0}catch{return console.warn("cecil: ignoring invalid selector",t),!1}},F={},G=new WeakMap;function H(t){let e=G.get(t);if(!e){const n=[...ht,...(t.mask??[]).filter(V)],a=(t.exclude??[]).filter(V);e={exclude:[...ft,...a].join(","),mask:[...J,...n].join(","),hide:n.join(","),hidden:["[data-cecil-private]",...a].join(",")},G.set(t,e)}return e}const Q=(t,e)=>t.matches(e);function C(t,e){for(let n=t;n;){if(n.closest(e))return!0;const a=n.getRootNode();n=a instanceof ShadowRoot?a.host:null}return!1}function bt(t,e=F){return C(t,H(e).exclude)}const v=(t,e)=>(t==null?void 0:t.replace(/\s+/g," ").trim().slice(0,e))||void 0,z=t=>`"${t.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/[\n\r\f]/g," ")}"`,R=t=>CSS.escape(t);function q(t,e,n,a){let s="";const r=(d,l=!1)=>{if(!(s.length>=n*2)){if(d.nodeType===3)s+=` ${d.textContent??""}`;else if(d.nodeType===1){const p=d;if(p===a||!l&&(Q(p,e.exclude)||Q(p,e.mask)))return;p.childNodes.forEach(g=>r(g))}}};return r(t,!0),v(s,n)}function Z(t){var a,s,r;const e=(a=v(t.getAttribute("role"),100))==null?void 0:a.toLowerCase();if(e)return e;const n=t.tagName.toLowerCase();if(n==="a")return t.hasAttribute("href")?"link":void 0;if(/^h[1-6]$/.test(n))return"heading";if(n==="input"){const d=(t.getAttribute("type")??"text").toLowerCase();return{checkbox:"checkbox",radio:"radio",button:"button",submit:"button",reset:"button",range:"slider",search:"searchbox"}[d]??(d==="hidden"?void 0:"textbox")}return n==="section"?(s=t.getAttribute("aria-label"))!=null&&s.trim()||(r=t.getAttribute("aria-labelledby"))!=null&&r.trim()?"region":void 0:{button:"button",textarea:"textbox",select:"combobox",option:"option",img:"img",nav:"navigation",main:"main",form:"form",li:"listitem",ul:"list",ol:"list",table:"table",dialog:"dialog",header:"banner",footer:"contentinfo",aside:"complementary"}[n]}const xt=/^(button|input|meter|output|progress|select|textarea)$/;function tt(t,e){const n=t.getRootNode()instanceof ShadowRoot?t.getRootNode():t.ownerDocument,a=(l,p)=>l&&!C(l,e.exclude)&&!C(l,e.mask)?q(l,e,1e3,p):void 0,s=(t.getAttribute("aria-labelledby")??"").split(/\s+/).slice(0,8).map(l=>l?a(n.getElementById(l)):void 0).filter(Boolean).join(" "),r=t.getAttribute("id"),d=xt.test(t.tagName.toLowerCase());return v(t.getAttribute("aria-label"),1e3)??v(s,1e3)??(r?a(n.querySelector(`label[for=${z(r)}]`)):void 0)??(d?a(t.closest("label"),t):void 0)}function et(t,e){return C(t,e.mask)?J.includes(t.tagName.toLowerCase())&&!C(t,e.hide)?tt(t,e)??v(t.getAttribute("placeholder"),1e3):void 0:tt(t,e)??v(t.getAttribute("placeholder"),1e3)??v(t.getAttribute("alt"),1e3)??v(t.getAttribute("title"),1e3)??q(t,e,1e3)}function yt(t,e=F){const n=H(e);return et(t,n)||`<${t.tagName.toLowerCase()}>`}function j(t,e){try{const n=t.getRootNode(),a=("querySelectorAll"in n?n:t.ownerDocument).querySelectorAll(e);return a.length===1&&a[0]===t}catch{return!1}}function nt(t){var n;const e=[];for(let a=t;a&&e.length<16&&(e.length<8||!j(t,e.join(" > ")));a=a.parentElement){const s=a.tagName.toLowerCase();if(s==="html"||s==="body")break;const r=v(a.getAttribute("id"),500);if(r&&j(a,`#${R(r)}`)){e.unshift(`#${R(r)}`);break}const d=[...((n=a.parentElement)==null?void 0:n.children)??[]].filter(l=>l.tagName===a.tagName);e.unshift(d.length>1?`${s}:nth-of-type(${d.indexOf(a)+1})`:s)}return e.join(" > ")||t.tagName.toLowerCase()}function vt(t,e,n){const a=[];for(const d of gt){const l=v(t.getAttribute(d),500);l&&j(t,`[${d}=${z(l)}]`)&&a.push(`[${d}=${z(l)}]`)}const s=v(t.getAttribute("id"),500);s&&j(t,`#${R(s)}`)&&a.push(`#${R(s)}`),e&&n&&a.push(`role=${e} name=${z(n.slice(0,100))}`);const r=nt(t);return a.includes(r)||a.push(r),a.slice(0,8)}function wt(t){const e=[];for(let n=t.parentElement;n&&e.length<4;n=n.parentElement){const a=n.tagName.toLowerCase();if(a==="html"||a==="body")break;const s=Z(n);e.push(s&&s!==a?`${a}[role=${s}]`:a)}return e.length?e:void 0}function kt(t,e=F){const n=H(e);if(C(t,n.exclude))throw new Error("This area is private and cannot be captured.");const a=C(t,n.mask),s=Z(t),r=et(t,n),d=a?void 0:q(t,n,500),l=vt(t,s,r),p={};for(const w of mt){const $=v(t.getAttribute(w),500);$&&(p[w]=$)}const g=t.getBoundingClientRect(),L=t.querySelector(n.hidden)!==null||t.querySelector(n.hide)!==null,f=wt(t),E={selector:l.find(w=>!w.startsWith("role="))??nt(t),tag:t.tagName.toLowerCase(),locators:l,rect:{x:Math.round(g.x),y:Math.round(g.y),width:Math.round(g.width),height:Math.round(g.height)},redacted:a||L};return s&&(E.role=s),r&&(E.name=r),d&&(E.text=d),Object.keys(p).length&&(E.attributes=p),f&&(E.ancestors=f),E}function Et(t,e={}){if(e.url)return e.url(t);const n=new URL(t);return n.origin+n.pathname}function ot(t,e){let n=document.elementFromPoint(t,e);for(;n!=null&&n.shadowRoot;){const a=n.shadowRoot.elementFromPoint(t,e);if(!a||a===n)break;n=a}return n}function B(t,e,n){Object.assign(t.style,{display:"block",left:`${e.x}px`,top:`${e.y}px`,width:`${e.width}px`,height:`${e.height}px`}),t.firstElementChild.textContent=n.replace(/\s+/g," ").trim().slice(0,120)}const P=(t,e)=>Math.min(Math.max(t,0),e);function St({host:t,box:e,mode:n,privacy:a,done:s}){let r,d;const l=o=>o.composedPath().includes(t),p=o=>!!o&&o!==t&&!t.contains(o)&&!bt(o,a);function g(o){if(d=o&&p(o)?o:void 0,!d){e.style.display="none";return}const u=d.getBoundingClientRect();B(e,u,yt(d,a))}function L(o){const u=P(o.clientX,innerWidth),y=P(o.clientY,innerHeight);return{x:Math.min(r.x,u),y:Math.min(r.y,y),width:Math.abs(u-r.x),height:Math.abs(y-r.y)}}const f=o=>{l(o)||(o.preventDefault(),o.stopImmediatePropagation())},E=o=>{l(o)||(n==="element"?g(ot(o.clientX,o.clientY)):r&&B(e,L(o),"Selected area"))},w=o=>{f(o),!(n!=="area"||o.button!==0||l(o))&&(r={x:P(o.clientX,innerWidth),y:P(o.clientY,innerHeight)})},$=o=>{if(f(o),n!=="area"||!r)return;const u=L(o);if(r=void 0,Ct(),u.width<8||u.height<8){e.style.display="none";return}S(),s({area:{x:Math.round(u.x),y:Math.round(u.y),width:Math.round(u.width),height:Math.round(u.height)}})},X=o=>{if(l(o))return;if(n==="area"){o.preventDefault(),o.stopImmediatePropagation();return}const u=d??ot(o.clientX,o.clientY);p(u)&&(o.preventDefault(),o.stopImmediatePropagation(),S(),s({el:u}))},K=o=>{n==="element"&&o.target instanceof Element&&g(o.target)},T=o=>{if((o.key==="Escape"||o.key==="Enter"||o.key===" ")&&!(o.key!=="Escape"&&l(o))&&f(o),o.key==="Escape"){S(),s(null);return}const y=document.activeElement;n!=="element"||o.key!=="Enter"&&o.key!==" "||!y||y===document.body||!p(y)||o.key!==" "&&(S(),s({el:y}))},x=o=>{(o.key===" "||o.key==="Enter")&&!l(o)&&f(o)},m=o=>{l(o)||o.stopImmediatePropagation()},b=()=>n==="element"&&d&&g(d),M=o=>{const u=o?"addEventListener":"removeEventListener";document[u]("pointermove",E,!0),document[u]("pointerdown",w,!0),document[u]("pointerup",$,!0),document[u]("mousedown",f,!0),document[u]("mouseup",f,!0),document[u]("touchstart",m,!0),document[u]("touchend",m,!0),document[u]("keyup",x,!0),document[u]("click",X,!0),document[u]("focusin",K,!0),document[u]("keydown",T,!0),window[u]("scroll",b,!0),window[u]("resize",b)};function S(){M(!1),e.style.display="none"}return M(!0),S}function Ct(){const t=e=>{e.preventDefault(),e.stopImmediatePropagation()};document.addEventListener("click",t,!0),setTimeout(()=>document.removeEventListener("click",t,!0),250)}const Lt=4e3;function $t(t,e,n){return{comment:t,url:Et(location.href,n),element:(e==null?void 0:e.kind)==="element"?e.info:void 0,area:(e==null?void 0:e.kind)==="area"?e.rect:void 0,userAgent:navigator.userAgent,viewport:{width:innerWidth,height:innerHeight},timestamp:new Date().toISOString()}}const it=`
|
|
2
|
+
:host{all:initial;--c:#3e63dd;--t:#191c22;--m:#697180;--b:#e4e7ec;--sh:0 16px 42px rgba(24,29,39,.2);font-family:system-ui,-apple-system,sans-serif;color:var(--t)}
|
|
3
|
+
*,*:before,*:after{box-sizing:border-box;font-family:inherit}
|
|
4
|
+
button{font:inherit;cursor:pointer}
|
|
5
|
+
button:focus-visible,textarea:focus-visible{outline:2px solid var(--c);outline-offset:2px}
|
|
6
|
+
[hidden]{display:none!important}
|
|
7
|
+
.dock{position:fixed;z-index:2147483003;display:flex;align-items:center;gap:8px}
|
|
8
|
+
.dock[data-h=left]{flex-direction:row-reverse}
|
|
9
|
+
.fab{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:0 15px;border:0;border-radius:12px;background:#21242b;color:#fff;font-size:13px;font-weight:600;box-shadow:var(--sh);touch-action:none}
|
|
10
|
+
.fab svg{width:17px;height:17px}
|
|
11
|
+
.bar{position:fixed;right:20px;bottom:20px;z-index:2147483003;display:flex;align-items:center;gap:10px;min-height:42px;padding:6px 8px 6px 12px;border:1px solid var(--b);border-radius:12px;background:#fff;box-shadow:var(--sh);font-size:12px}
|
|
12
|
+
.hint{font-weight:600;white-space:nowrap}
|
|
13
|
+
.modes{display:flex;gap:2px;padding:2px;border-radius:8px;background:#f1f3f6}
|
|
14
|
+
.mode{height:28px;padding:0 9px;border:0;border-radius:6px;background:transparent;color:#7c8493;font-size:12px}
|
|
15
|
+
.mode:disabled{opacity:.45;cursor:not-allowed}
|
|
16
|
+
.mode.on{background:#fff;color:var(--t);box-shadow:0 1px 3px rgba(24,29,39,.14)}
|
|
17
|
+
.kbd{height:19px;padding:1px 5px;border:1px solid #d4d8df;border-bottom-width:2px;border-radius:5px;background:#fff;color:var(--m);font:10px/15px ui-monospace,monospace}
|
|
18
|
+
.x{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:0;border-radius:7px;background:transparent;color:var(--m)}
|
|
19
|
+
.x:hover{background:#f8f9fb;color:var(--t)}
|
|
20
|
+
.box{position:fixed;display:none;z-index:2147483000;border:2px solid var(--c);border-radius:3px;background:rgba(62,99,221,.1);box-shadow:0 0 0 5px rgba(62,99,221,.14);pointer-events:none}
|
|
21
|
+
.box.on{box-shadow:0 0 0 4px rgba(62,99,221,.11)}
|
|
22
|
+
.tag{position:absolute;left:-2px;top:-29px;max-width:260px;height:23px;padding:3px 7px;border-radius:5px;background:var(--c);color:#fff;font-size:11px;font-weight:600;line-height:17px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
23
|
+
.panel{position:fixed;z-index:2147483003;width:min(340px,calc(100vw - 32px));border:1px solid var(--b);border-radius:12px;background:#fff;box-shadow:var(--sh)}
|
|
24
|
+
.head{display:flex;align-items:center;justify-content:space-between;padding:12px 12px 0 14px}
|
|
25
|
+
h2{margin:0;font-size:14px;font-weight:650}
|
|
26
|
+
.body{display:flex;flex-direction:column;gap:10px;padding:10px 14px 14px}
|
|
27
|
+
.trow{display:flex;align-items:center;gap:7px;min-width:0}
|
|
28
|
+
.chip{display:inline-flex;align-items:center;min-width:0;max-width:245px;height:25px;padding:0 8px;border:1px solid rgba(62,99,221,.3);border-radius:7px;background:#eef2fd;color:var(--c);font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
29
|
+
.link{border:0;background:transparent;color:var(--m);padding:3px;font-size:12px}
|
|
30
|
+
textarea{width:100%;min-height:88px;resize:vertical;padding:9px 10px;border:1px solid #cfd4dc;border-radius:7px;color:var(--t);background:#fff;font-size:13px;line-height:1.5}
|
|
31
|
+
.status{min-height:17px;margin:0;color:var(--m);font-size:12px}
|
|
32
|
+
.status.err{color:#b42318}
|
|
33
|
+
.actions{display:flex;justify-content:flex-end;gap:7px}
|
|
34
|
+
.btn{height:32px;padding:0 11px;border:1px solid #d5d9e0;border-radius:7px;background:#fff;color:#353b45;font-size:12px;font-weight:600}
|
|
35
|
+
.btn.primary{border-color:var(--c);background:var(--c);color:#fff}
|
|
36
|
+
.btn:disabled{opacity:.55;cursor:wait}
|
|
37
|
+
.toast{display:flex;align-items:center;gap:8px;height:38px;padding:0 13px;border-radius:10px;background:#21242b;color:#f2f3f5;box-shadow:var(--sh);font-size:13px}
|
|
38
|
+
.toast .ok{color:#6ec493;font-weight:700}
|
|
39
|
+
.toast a{color:#9db4ff}
|
|
40
|
+
.sr{position:absolute!important;width:1px!important;height:1px!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important}
|
|
41
|
+
@media(max-width:640px){
|
|
42
|
+
.bar{left:12px!important;right:12px!important;bottom:12px!important;top:auto!important;flex-wrap:wrap}
|
|
43
|
+
.hint{flex:1}.kbd{display:none}
|
|
44
|
+
.dock{flex-direction:column;align-items:flex-end}
|
|
45
|
+
.dock[data-h=left]{flex-direction:column;align-items:flex-start}
|
|
46
|
+
.dock[data-v=top]{flex-direction:column-reverse}
|
|
47
|
+
.panel{left:12px!important;right:12px!important;bottom:12px!important;top:auto!important;width:auto}
|
|
48
|
+
.toast{max-width:calc(100vw - 40px)}}
|
|
49
|
+
`,Mt=`
|
|
50
|
+
<div class="box"><span class="tag"></span></div>
|
|
51
|
+
<section class="bar" aria-label="Cecil target selection" hidden>
|
|
52
|
+
<span class="hint">Select something to comment on</span>
|
|
53
|
+
<div class="modes">
|
|
54
|
+
<button class="mode" data-mode="element" type="button">Element</button>
|
|
55
|
+
<button class="mode" data-mode="page" type="button">Page</button>
|
|
56
|
+
<button class="mode" data-mode="area" type="button" disabled title="Coming soon">Area</button>
|
|
57
|
+
</div>
|
|
58
|
+
<span class="kbd" aria-hidden="true">esc</span>
|
|
59
|
+
<button class="x" data-act="close" type="button" aria-label="Exit feedback mode">✕</button>
|
|
60
|
+
</section>
|
|
61
|
+
<section class="panel" role="dialog" aria-modal="false" aria-labelledby="ttl" hidden>
|
|
62
|
+
<div class="head"><h2 id="ttl">New feedback</h2><button class="x" data-act="close" type="button" aria-label="Close feedback">✕</button></div>
|
|
63
|
+
<div class="body">
|
|
64
|
+
<div class="trow"><span class="chip"></span><button class="link" data-act="change" type="button">Change</button></div>
|
|
65
|
+
<textarea aria-label="Feedback" placeholder="What should be improved?"></textarea>
|
|
66
|
+
<p class="status" role="status"></p>
|
|
67
|
+
<div class="actions">
|
|
68
|
+
<button class="btn" data-act="close" type="button">Cancel</button>
|
|
69
|
+
<button class="btn primary" data-act="send" type="button">Send feedback</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
<div class="dock">
|
|
74
|
+
<div class="toast" hidden><span class="ok">✓</span><span class="msg">Feedback sent — thank you</span><a target="_blank" rel="noopener" hidden>View issue</a></div>
|
|
75
|
+
<button class="fab" type="button" aria-expanded="false" aria-label="Open feedback">
|
|
76
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="9" cy="12" r="7.4" fill="currentColor" stroke="none"/><circle cx="11.5" cy="10" r="1.15" fill="#21242b" stroke="none"/><path d="M10.5 14h3.5a1.75 1.75 0 0 1-3.5 0z" fill="#21242b" stroke="#21242b" stroke-width="1.2"/><path d="M18.7 10a3 3 0 0 1 0 4"/><path d="M21.7 9a6 6 0 0 1 0 6"/></svg>
|
|
77
|
+
<span>Feedback</span>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
<p class="sr" aria-live="polite"></p>`,at="cecil:corner",At=["top-left","top-right","bottom-left","bottom-right"],rt=(t,e,n)=>Math.min(Math.max(t,e),n);function Tt(t,e,n){const a=document.createElement("div");a.setAttribute("data-cecil","");const s=a.attachShadow({mode:"closed"});s.innerHTML=Mt;try{const i=new CSSStyleSheet;i.replaceSync(it),s.adoptedStyleSheets=[i]}catch{const i=document.createElement("style");i.textContent=it,s.prepend(i)}const r=i=>s.querySelector(i),d=r(".box"),l=r(".bar"),p=r(".panel"),g=r(".dock"),L=r(".toast"),f=r(".fab"),E=r(".sr"),w=r("textarea");w.maxLength=Lt;const $=r(".status"),X=r(".chip"),K=r('[data-act="change"]'),T=r('[data-act="send"]');document.body.append(a);let x="idle",m,b,M="element",S,o="bottom-right";try{const i=localStorage.getItem(at);i&&At.includes(i)&&(o=i)}catch{}const u=i=>E.textContent=i,y=(i,c=!1)=>{$.textContent=i,$.classList.toggle("err",c)},U=i=>i.kind==="element"?i.info.name??i.info.text??`<${i.info.tag}>`:i.kind==="area"?"Selected area":"Entire page";function ct(){const i=o.endsWith("left")?"left":"right",c=o.startsWith("top")?"top":"bottom";g.dataset.h=i,g.dataset.v=c;for(const h of[g,l])h.style.left=i==="left"?"20px":"auto",h.style.right=i==="right"?"20px":"auto",h.style.top=c==="top"?"20px":"auto",h.style.bottom=c==="bottom"?"20px":"auto"}function N(){if(x!=="composing"||!m)return;const i=Math.min(340,innerWidth-32),c=p.offsetHeight||248,h=m.kind==="element"?m.el.isConnected?m.el.getBoundingClientRect():m.info.rect:m.kind==="area"?m.rect:void 0;if(!h){d.style.display="none",p.style.left=o.endsWith("left")?"16px":`${Math.max(16,innerWidth-i-16)}px`,p.style.top=o.startsWith("top")?"16px":`${Math.max(16,innerHeight-c-16)}px`;return}B(d,h,U(m)),d.classList.add("on");const pt=h.y+h.height+12;p.style.left=`${rt(h.x,16,Math.max(16,innerWidth-i-16))}px`,p.style.top=`${pt+c<=innerHeight-16?pt:rt(h.y-c-12,16,Math.max(16,innerHeight-c-16))}px`}function A(){const i=x==="picking";l.hidden=!i,p.hidden=x!=="composing",L.hidden=x!=="sent",f.hidden=i||x==="composing",f.setAttribute("aria-expanded",String(x!=="idle")),r(".hint").textContent=M==="area"?"Drag to select an area":"Select something to comment on",s.querySelectorAll(".mode").forEach(c=>{const h=c.dataset.mode===M&&i;c.classList.toggle("on",h),c.setAttribute("aria-pressed",String(h))}),x==="composing"&&m?(X.textContent=U(m),K.textContent=m.kind==="page"?"Select element":"Change",y(""),N(),w.focus()):(d.classList.remove("on"),i||(d.style.display="none"))}function Y(i){clearTimeout(S),b==null||b(),M=i,x="picking",m=void 0,u(i==="area"?"Area mode. Drag a rectangle. Press Escape to exit.":"Hover or Tab to an element, then click or press Enter. Press Escape to exit."),A(),b=St({host:a,box:d,mode:i,privacy:e,done:c=>{if(b=void 0,!c)return O();try{m="el"in c?{kind:"element",el:c.el,info:kt(c.el,e)}:{kind:"area",rect:c.area}}catch(h){return dt(),y(h instanceof Error?h.message:"That could not be captured.",!0)}x="composing",u(`${U(m)} selected. Feedback form opened.`),A()}})}function dt(){b==null||b(),b=void 0,m={kind:"page"},x="composing",A()}function O(){const i=s.activeElement!==null;clearTimeout(S),b==null||b(),b=void 0,x="idle",m=void 0,A(),i&&f.focus()}async function lt(){if(T.disabled)return;const i=w.value.trim();if(!i)return y("Please enter your feedback first.",!0);T.disabled=!0,y("Sending…");try{const{url:c}=await t($t(i,m,e));w.value="";const h=L.querySelector("a");h.hidden=!c,c&&(h.href=c),x="sent",u("Feedback sent. Thank you."),A(),(c?h:f).focus(),S=setTimeout(O,c?5e3:1400)}catch(c){y(c.status===429?"Too many reports. Please try again in a minute.":"Could not send. Please try again.",!0)}finally{T.disabled=!1}}let k,_=!1;f.addEventListener("pointerdown",i=>{var c;i.button===0&&(k={id:i.pointerId,x:i.clientX,y:i.clientY,moved:!1},(c=f.setPointerCapture)==null||c.call(f,i.pointerId))}),f.addEventListener("pointermove",i=>{if(!k||k.id!==i.pointerId)return;const c=i.clientX-k.x,h=i.clientY-k.y;Math.hypot(c,h)>=6&&(k.moved=!0),k.moved&&(f.style.transform=`translate(${c}px,${h}px)`)}),f.addEventListener("pointerup",i=>{if(!k||k.id!==i.pointerId)return;const c=k.moved;if(k=void 0,f.style.transform="",!!c){_=!0,o=`${i.clientY<innerHeight/2?"top":"bottom"}-${i.clientX<innerWidth/2?"left":"right"}`;try{localStorage.setItem(at,o)}catch{}ct(),u(`Feedback button moved to the ${o.replace("-"," ")}.`)}}),f.addEventListener("pointercancel",()=>{k=void 0,f.style.transform=""}),f.addEventListener("click",()=>{_?_=!1:Y("element")}),s.addEventListener("click",i=>{const c=i.target.closest("[data-act],[data-mode]");c&&(c.dataset.act==="close"?O():c.dataset.act==="change"?Y("element"):c.dataset.act==="send"?lt():c.dataset.mode==="page"?dt():c.dataset.mode&&Y(c.dataset.mode))}),w.addEventListener("keydown",i=>{i.key==="Enter"&&(i.metaKey||i.ctrlKey)&&(i.preventDefault(),lt())});const ut=i=>{i.key!=="Escape"||x!=="composing"||!i.composedPath().includes(a)||(i.stopPropagation(),O())};return document.addEventListener("keydown",ut,!0),addEventListener("resize",N),addEventListener("scroll",N,!0),ct(),A(),()=>{O(),document.removeEventListener("keydown",ut,!0),removeEventListener("resize",N),removeEventListener("scroll",N,!0),a.remove()}}const Nt=15e3;function Ot({endpoint:t,projectKey:e}){const n=new URL(t,location.href),a=["localhost","127.0.0.1","[::1]"].includes(n.hostname);if(n.protocol!=="https:"&&!a)throw new Error("cecil: relay endpoint must use https");return async s=>{const r=new AbortController,d=setTimeout(()=>r.abort(),Nt);try{const l=await fetch(n,{method:"POST",headers:{"content-type":"text/plain"},body:JSON.stringify({projectKey:e,report:s}),signal:r.signal});if(!l.ok)throw console.warn(`cecil: relay responded ${l.status}${l.status===403?" (check the projectKey and the relay's origin allowlist)":""}`),Object.assign(new Error(`cecil: relay responded ${l.status}`),{status:l.status});return await l.json().catch(()=>({}))}finally{clearTimeout(d)}}}let D;function st(t){if(D)return console.warn("cecil: already initialised"),D;const e=Ot(t),{user:n}=t;let a,s=!1;const r=()=>{s||(a=Tt(d=>e({...d,user:typeof n=="function"?n():n}),t.privacy))};return document.body?r():document.addEventListener("DOMContentLoaded",r,{once:!0}),D=()=>{s=!0,document.removeEventListener("DOMContentLoaded",r),a==null||a(),D=void 0}}function zt(t){const{endpoint:e,projectKey:n,user:a,exclude:s,mask:r,fullUrl:d}=t.dataset;if(!e||!n)return null;const l=p=>p==null?void 0:p.split(",").map(g=>g.trim()).filter(Boolean);return{endpoint:e,projectKey:n,user:a,privacy:{exclude:l(s),mask:l(r),url:d===void 0?void 0:p=>p}}}const I=typeof document>"u"?null:document.currentScript;if(I&&"dataset"in I&&I.dataset.endpoint){const t=zt(I);t?st(t):console.warn("cecil: set data-endpoint and data-project-key")}return W.init=st,Object.defineProperty(W,Symbol.toStringTag,{value:"Module"}),W})({});
|