tell-cecil 0.0.1 → 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/README.md +47 -82
- package/dist/cecil.js +3 -3
- package/dist/cecil.mjs +1 -1
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,126 +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
4
|
|
|
5
|
-
|
|
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
6
|
|
|
7
|
-
|
|
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
8
|
|
|
9
|
-
> **
|
|
9
|
+
> **Alpha.** GitHub only; no screenshots or area selection yet.
|
|
10
|
+
|
|
11
|
+

|
|
10
12
|
|
|
11
13
|
## How it works
|
|
12
14
|
|
|
13
|
-
|
|
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.
|
|
14
18
|
|
|
15
19
|
## Quick start
|
|
16
20
|
|
|
17
|
-
**1. Run the relay
|
|
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.
|
|
18
22
|
|
|
19
23
|
```sh
|
|
20
|
-
export CECIL_GITHUB_TOKEN=github_pat_...
|
|
24
|
+
export CECIL_GITHUB_TOKEN=github_pat_...
|
|
21
25
|
export CECIL_GITHUB_REPO=owner/repo
|
|
22
|
-
export CECIL_PROJECT_KEY=pk_abc
|
|
23
|
-
export CECIL_ORIGINS=https://yoursite.com
|
|
24
|
-
npx tell-cecil
|
|
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
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
To run the relay inside your own app instead, `npm i tell-cecil` and call `createRelay` from `tell-cecil/relay`. It is a `(Request) => Response` function, so it also runs on Cloudflare Workers, Vercel, Deno or Bun. The source repo has a Cloudflare Worker example (`examples/cloudflare-worker`) and a `Dockerfile` for the Node runner (`docker build -t tell-cecil . && docker run --env-file .env -p 8787:8787 tell-cecil`).
|
|
30
|
-
|
|
31
|
-
From source: `npm install && npm run build` produces `dist/cecil.js` (script tag), `dist/cecil.mjs` (ES module), `dist/relay.mjs`, `dist/server.mjs` and type declarations in `dist/types`. `npm run relay` runs the relay from `src/` for development.
|
|
32
|
-
|
|
33
|
-
**Rate limiting and proxies.** The relay limits per project key and client IP (default 10 per minute), plus a per-project cap (`rateLimit.projectMax`, default 60 per minute), and answers 429 with a `retry-after` header. By default the Node runner ignores any client-supplied `x-forwarded-for` and `cf-connecting-ip` and uses the socket address. If you run it behind a reverse proxy you control, set `CECIL_TRUST_PROXY=1` to use the proxy's `x-forwarded-for` (the last entry is the one your proxy added). When calling `createRelay` yourself, pass `clientIp: (req) => string` to choose the client identity; the default prefers `cf-connecting-ip`, else the last `x-forwarded-for` entry, else `"unknown"`.
|
|
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.
|
|
34
32
|
|
|
35
|
-
**
|
|
36
|
-
|
|
37
|
-
**2. Add the widget.** Load it from a CDN (or copy `dist/cecil.js` to your site):
|
|
33
|
+
**2. Add the widget.**
|
|
38
34
|
|
|
39
35
|
```html
|
|
40
|
-
<script src="https://cdn.jsdelivr.net/npm/tell-cecil@0.0.
|
|
36
|
+
<script src="https://cdn.jsdelivr.net/npm/tell-cecil@0.0.2/dist/cecil.js"
|
|
41
37
|
data-endpoint="https://relay.example.com"
|
|
42
38
|
data-project-key="pk_abc"></script>
|
|
43
39
|
```
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
If reports fail, the browser console says why. A 403 means the project key is wrong or the page's origin isn't in the relay's `CECIL_ORIGINS`. The relay refuses to start on a malformed origin (trailing slash or path), a repo that isn't `owner/repo`, an empty token or no projects.
|
|
41
|
+
Or `npm i tell-cecil`, then `import { init } from "tell-cecil"` and `init({ endpoint, projectKey })`.
|
|
48
42
|
|
|
49
|
-
**
|
|
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`.
|
|
50
44
|
|
|
51
|
-
|
|
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).
|
|
52
46
|
|
|
53
|
-
##
|
|
47
|
+
## Security
|
|
54
48
|
|
|
55
|
-
|
|
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.
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
| --- | --- | --- |
|
|
59
|
-
| `endpoint` (required) | `data-endpoint` | Relay URL. Relative is allowed; must be https unless localhost, 127.0.0.1 or [::1]. |
|
|
60
|
-
| `projectKey` (required) | `data-project-key` | Public key the relay maps to a repo and origin allowlist. |
|
|
61
|
-
| `user` | `data-user` | Self-reported reporter, a string or a function called on each send. Unverified. |
|
|
62
|
-
| `privacy.exclude` | `data-exclude` | Selectors that can't be picked (comma-separated in the attribute). |
|
|
63
|
-
| `privacy.mask` | `data-mask` | Selectors whose text and name are left out. |
|
|
64
|
-
| `privacy.url` | `data-full-url` (presence) | Rewrites the page URL before sending. Default: origin and path only. The attribute sends the full URL. |
|
|
51
|
+
## Privacy
|
|
65
52
|
|
|
66
|
-
|
|
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.
|
|
67
54
|
|
|
68
|
-
|
|
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`.
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
| --- | --- | --- |
|
|
72
|
-
| `CECIL_GITHUB_TOKEN` (required) | | Fine-grained token, issues read/write on one repo. |
|
|
73
|
-
| `CECIL_GITHUB_REPO` (required) | | `owner/repo`. |
|
|
74
|
-
| `CECIL_PROJECT_KEY` | `dev` | Public project key. |
|
|
75
|
-
| `CECIL_ORIGINS` | `http://localhost:5173` | Comma-separated exact origins allowed to submit. |
|
|
76
|
-
| `CECIL_LABELS` | none | Comma-separated labels added to each issue. |
|
|
77
|
-
| `CECIL_TRUST_PROXY` | off | `1` uses the proxy's `x-forwarded-for` for rate limiting. |
|
|
78
|
-
| `PORT` | `8787` | Listen port. |
|
|
59
|
+
## Reference
|
|
79
60
|
|
|
80
|
-
|
|
61
|
+
**Widget:** `init(options)`, or data attributes on the script tag. `init` returns a function that removes the widget.
|
|
81
62
|
|
|
82
|
-
| Option |
|
|
63
|
+
| Option | Attribute | |
|
|
83
64
|
| --- | --- | --- |
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
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. |
|
|
89
71
|
|
|
90
|
-
|
|
72
|
+
**Relay:** `createRelay(options)`.
|
|
91
73
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Tell Cecil never reads input values, editable content or page HTML. It sends the selector, role, accessible name, a short text excerpt, a few attributes, up to four ancestors, and the element's position.
|
|
99
|
-
|
|
100
|
-
- `data-cecil-private` on an element: it and its children can't be picked.
|
|
101
|
-
- `data-cecil-mask` on an element: it can be picked, but its text and name are left out.
|
|
102
|
-
- All `input`, `textarea`, `select` and `contenteditable` content is masked by default: the element is reported, but never its value, text or selected option. Form controls keep their label (`aria-label`, `label`, or `placeholder`) as the name; `contenteditable` and `data-cecil-mask` content gets no name. A container is marked redacted only if it holds `data-cecil-private`, your own `exclude` matches, `data-cecil-mask`/your `mask` matches or `contenteditable` content; form fields, scripts and hidden inputs don't count.
|
|
103
|
-
- The page URL is sent without its query string and hash. To send the full URL, use `data-full-url` on the script tag, or `privacy: { url: (href) => ... }` in `init` to rewrite it yourself.
|
|
104
|
-
- Add your own selectors: `data-exclude=".admin, #billing"` and `data-mask=".account-number"` (comma-separated), or `privacy: { exclude: [...], mask: [...] }` in `init`.
|
|
105
|
-
|
|
106
|
-
## Content Security Policy
|
|
107
|
-
|
|
108
|
-
The widget adds its styles as a constructable stylesheet (falling back to a `<style>` element in older browsers), so it works under a strict `style-src`. Your `connect-src` must include the relay's origin, and `script-src` must allow wherever you host `cecil.js`.
|
|
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. |
|
|
109
80
|
|
|
110
81
|
## Develop
|
|
111
82
|
|
|
112
83
|
```sh
|
|
113
|
-
npm install
|
|
114
|
-
npm
|
|
115
|
-
npm run
|
|
116
|
-
npm run dev # demo page (index.html) on http://localhost:5173; run `npm run relay` beside it
|
|
84
|
+
npm install && npm test
|
|
85
|
+
npm run dev # demo page on http://localhost:5173
|
|
86
|
+
npm run relay # relay from source, beside it
|
|
117
87
|
```
|
|
118
88
|
|
|
119
|
-
## Limits
|
|
120
|
-
|
|
121
|
-
- GitHub only; no screenshots; the rate limit is in memory per instance.
|
|
122
|
-
- Not yet: area selection, screenshots, other trackers.
|
|
123
|
-
|
|
124
89
|
## License
|
|
125
90
|
|
|
126
91
|
MIT
|
package/dist/cecil.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
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
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
3
|
*,*:before,*:after{box-sizing:border-box;font-family:inherit}
|
|
4
4
|
button{font:inherit;cursor:pointer}
|
|
@@ -73,8 +73,8 @@ textarea{width:100%;min-height:88px;resize:vertical;padding:9px 10px;border:1px
|
|
|
73
73
|
<div class="dock">
|
|
74
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
75
|
<button class="fab" type="button" aria-expanded="false" aria-label="Open feedback">
|
|
76
|
-
<svg viewBox="0 0
|
|
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
77
|
<span>Feedback</span>
|
|
78
78
|
</button>
|
|
79
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",
|
|
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})({});
|
package/dist/cecil.mjs
CHANGED
|
@@ -319,7 +319,7 @@ textarea{width:100%;min-height:88px;resize:vertical;padding:9px 10px;border:1px
|
|
|
319
319
|
<div class="dock">
|
|
320
320
|
<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>
|
|
321
321
|
<button class="fab" type="button" aria-expanded="false" aria-label="Open feedback">
|
|
322
|
-
<svg viewBox="0 0
|
|
322
|
+
<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>
|
|
323
323
|
<span>Feedback</span>
|
|
324
324
|
</button>
|
|
325
325
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tell-cecil",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Embeddable feedback widget that files issues in GitHub.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/Tell-Cecil/tell-cecil#readme",
|
|
11
11
|
"bugs": "https://github.com/Tell-Cecil/tell-cecil/issues",
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"feedback",
|
|
14
|
+
"widget",
|
|
15
|
+
"bug-report",
|
|
16
|
+
"github-issues",
|
|
17
|
+
"element-picker"
|
|
18
|
+
],
|
|
13
19
|
"type": "module",
|
|
14
20
|
"exports": {
|
|
15
21
|
".": {
|
|
@@ -29,7 +35,9 @@
|
|
|
29
35
|
"bin": {
|
|
30
36
|
"tell-cecil": "dist/server.mjs"
|
|
31
37
|
},
|
|
32
|
-
"files": [
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
33
41
|
"scripts": {
|
|
34
42
|
"build": "vite build && vite build --config vite.relay.config.ts && tsc -p tsconfig.build.json",
|
|
35
43
|
"dev": "vite",
|
|
@@ -40,7 +48,7 @@
|
|
|
40
48
|
"prepublishOnly": "npm run typecheck && npm test && npm run build && node scripts/check-size.mjs"
|
|
41
49
|
},
|
|
42
50
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^20.
|
|
51
|
+
"@types/node": "^22.20.5",
|
|
44
52
|
"jsdom": "^26.0.0",
|
|
45
53
|
"tsx": "^4.23.15",
|
|
46
54
|
"typescript": "^5.6.0",
|