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 CHANGED
@@ -1,126 +1,91 @@
1
- # Tell Cecil
1
+ <p align="center"><img src="docs/logo.svg" width="96" height="96" alt="Tell Cecil logo"></p>
2
2
 
3
- Embeddable feedback widget. Visitors click a button, pick an element on the page, describe the problem, and Tell Cecil files it in your tracker with the selector, accessible name and page context. MIT licensed, no dependencies, about 8 KB gzipped.
3
+ <h1 align="center">Tell Cecil</h1>
4
4
 
5
- > **Alpha.** Works with GitHub. Expect changes.
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
- <!-- TODO: demo GIF -->
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
- > **If your GitHub repo is public, every report is public.** Use a private repo unless you want comments and page paths visible to everyone. See [Privacy](#privacy).
9
+ > **Alpha.** GitHub only; no screenshots or area selection yet.
10
+
11
+ ![A visitor picks the "Start free trail" button, notes the typo, and it lands as a GitHub issue](docs/demo.gif)
10
12
 
11
13
  ## How it works
12
14
 
13
- The widget collects the report in the browser and posts it to a small relay you run. The relay creates the GitHub issue with a server-side token, so reporters need no GitHub account and your token and labels never reach the page. The success reply includes the issue link, which shows the repo name to the reporter; set `exposeIssueUrl: false` on the project in `createRelay` to turn that off (the widget then shows no link).
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.** Needs Node 20 or later and a [fine-grained GitHub token](https://github.com/settings/personal-access-tokens/new) with access to one repository and Issues: read and write.
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_... # fine-grained, issues: read/write on one repo
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 # comma-separated exact origins
24
- npx tell-cecil # http://localhost:8787
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 same variables can live in a `.env` file in the folder you run it from (Node 20.12+). Optional: `CECIL_LABELS`, `PORT`, `CECIL_TRUST_PROXY=1`. Defaults are project key `dev` and origin `http://localhost:5173`.
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
- **Limits are weak by design.** The rate-limit store is in memory, per process. On Cloudflare Workers that means per isolate, which is weak: put Cloudflare rate-limiting rules or a WAF in front of the relay for real protection. The per-project cap also means a flood can lock legitimate reporters out for that window; that trade-off protects the GitHub token's quota.
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.1/dist/cecil.js"
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
- From a bundler, `import { init } from "tell-cecil"` and call `init({ endpoint, projectKey })`. The endpoint may be relative (`/cecil`) when the relay sits behind your own domain; it must be https unless it is localhost.
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
- **Who reported it.** Add `data-user="jane@corp.example"`, or in code `init({ ..., user: () => currentUser.email })`. The issue shows it as `Reporter: ... (self-reported)`. The browser supplies this value, so it is not verified. Anyone who can reach the relay can claim any name, so keep the relay behind your VPN or SSO proxy.
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
- The relay checks the project key and the exact `Origin` against an allowlist, rate limits, validates the report, then calls the GitHub API. The project key is public, not a secret. Origin checks stop other websites in browsers, not scripts, so your real protection is the rate limit and a scoped token (issues: write on one repo).
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
- ## Configuration reference
47
+ ## Security
54
48
 
55
- **Widget `init(config)`** (from `dist/cecil.mjs`) and the matching `<script>` data attributes (`dist/cecil.js`):
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
- | `init` option | Data attribute | Description |
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
- `init` returns a function that removes the widget. Calling it twice returns the existing one.
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
- **Node runner (`dist/server.mjs`) environment variables:**
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
- | Variable | Default | Description |
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
- **`createRelay(options)`:**
61
+ **Widget:** `init(options)`, or data attributes on the script tag. `init` returns a function that removes the widget.
81
62
 
82
- | Option | Default | Description |
63
+ | Option | Attribute | |
83
64
  | --- | --- | --- |
84
- | `projects` (required) | | Map of project key to `{ origins, github: { repo, token, labels? }, exposeIssueUrl? }`. `exposeIssueUrl` defaults to true. |
85
- | `rateLimit` | `{ max: 10, windowMs: 60000, projectMax: 60 }` | `max` per project key and client IP, `projectMax` per project key, per window. In memory, per instance. |
86
- | `clientIp` | `cf-connecting-ip`, else last `x-forwarded-for`, else `"unknown"` | Chooses the client identity for rate limiting. |
87
- | `fetch` | global `fetch` | Used for the GitHub call; injectable for tests. |
88
- | `now` | `Date.now` | Clock, injectable for tests. |
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
- ## What the reporter sees
72
+ **Relay:** `createRelay(options)`.
91
73
 
92
- Click **Feedback**, then choose **Element** (hover and click, or Tab and Enter) or **Page**, write a comment, and send. Esc exits. The button can be dragged to any corner. Area selection is planned.
93
-
94
- ## Privacy
95
-
96
- **If your repo is public, every report is public.** Use a private repo unless you want reporters' comments and page paths visible to everyone.
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 test
115
- npm run build # dist/cecil.js, dist/cecil.mjs, dist/relay.mjs, dist/server.mjs, dist/types
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 S(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 S(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&&!S(l,e.exclude)&&!S(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 S(t,e.mask)?J.includes(t.tagName.toLowerCase())&&!S(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(S(t,n.exclude))throw new Error("This area is private and cannot be captured.");const a=S(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 Ct({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,St(),u.width<8||u.height<8){e.style.display="none";return}C(),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(),C(),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"){C(),s(null);return}const y=document.activeElement;n!=="element"||o.key!=="Enter"&&o.key!==" "||!y||y===document.body||!p(y)||o.key!==" "&&(C(),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 C(){M(!1),e.style.display="none"}return M(!0),C}function St(){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=`
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 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6.2C3 4.4 4.4 3 6.2 3h5.6C13.6 3 15 4.4 15 6.2v3.1c0 1.8-1.4 3.2-3.2 3.2H8.4L5.2 15v-2.7A3.2 3.2 0 0 1 3 9.3z"/></svg>
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",C,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(C),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=Ct({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(C),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(),C=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})({});
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 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6.2C3 4.4 4.4 3 6.2 3h5.6C13.6 3 15 4.4 15 6.2v3.1c0 1.8-1.4 3.2-3.2 3.2H8.4L5.2 15v-2.7A3.2 3.2 0 0 1 3 9.3z"/></svg>
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.1",
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": ["feedback", "widget", "bug-report", "github-issues", "element-picker"],
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": ["dist"],
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.19.43",
51
+ "@types/node": "^22.20.5",
44
52
  "jsdom": "^26.0.0",
45
53
  "tsx": "^4.23.15",
46
54
  "typescript": "^5.6.0",