report-typo 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ # SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ MIT License
6
+
7
+ Copyright (c) 2026 Damián Búho
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
10
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
11
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
13
+ following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all copies or substantial
16
+ portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
19
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
20
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
22
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,58 @@
1
- # Temporary Holding Version
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>
3
+ SPDX-License-Identifier: MIT
4
+ pf-cli-managed: yes
5
+ -->
2
6
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
7
+ [Español](docs/es/README.md) · [Українська](docs/uk/README.md)
8
+
9
+ # Report Typo
10
+
11
+ A browser library that lets readers report a typo by selecting it and pressing Ctrl+Enter (Cmd+Enter on macOS). The report, with the text around the selection, goes to a report-relay endpoint, a webhook, or a function of your own.
12
+
13
+ [![Stand with Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://damian-buho.github.io/support-ukraine/) [![Projectfile inside](https://badges.kiota.ch/static/v1?label=projectfile&message=inside&labelColor=0d0d0d&color=8c6723&style=flat-square)](https://projectfile.org) [![License](https://badges.kiota.ch/static/v1?label=license&message=MIT&color=1e5913&style=flat-square)](LICENSE) [![PRs welcome](https://badges.kiota.ch/static/v1?label=PRs&message=welcome&color=1e5913&style=flat-square)](CONTRIBUTING.md) [![REUSE compliance](https://api.reuse.software/badge/codeberg.org/damian-buho/report-typo)](https://api.reuse.software/info/codeberg.org/damian-buho/report-typo)
14
+
15
+ ![Project status](https://badges.kiota.ch/static/v1?label=status&message=experimental&color=1d63ed&style=flat-square) [![Last commit on kiota.ch](https://badges.kiota.ch/gitea/last-commit/damian-buho/report-typo?gitea_url=https://kiota.ch&label=last%20commit%20on%20kiota.ch&style=flat-square)](https://kiota.ch/damian-buho/report-typo) [![Last commit on Codeberg](https://badges.kiota.ch/gitea/last-commit/damian-buho/report-typo?gitea_url=https://codeberg.org&label=last%20commit%20on%20Codeberg&style=flat-square)](https://codeberg.org/damian-buho/report-typo) [![Last commit on GitHub](https://badges.kiota.ch/github/last-commit/damian-buho/report-typo?label=last%20commit%20on%20GitHub&style=flat-square)](https://github.com/damian-buho/report-typo)
16
+
17
+ [![npm version](https://badges.kiota.ch/npm/v/report-typo?style=flat-square)](https://www.npmjs.com/package/report-typo) [![npm downloads per month](https://badges.kiota.ch/npm/dm/report-typo?style=flat-square)](https://www.npmjs.com/package/report-typo) [![Minified and gzipped bundle size](https://badges.kiota.ch/bundlephobia/minzip/report-typo?style=flat-square)](https://bundlephobia.com/package/report-typo) [![Dependency freshness](https://badges.kiota.ch/librariesio/release/npm/report-typo?style=flat-square)](https://libraries.io/npm/report-typo)
18
+
19
+ [![Publish pipeline on GitHub](https://github.com/damian-buho/report-typo/actions/workflows/published.yaml/badge.svg?style=flat-square)](https://github.com/damian-buho/report-typo/actions) [![Vulnerability audit on GitHub](https://github.com/damian-buho/report-typo/actions/workflows/audited.yaml/badge.svg?style=flat-square)](https://github.com/damian-buho/report-typo/actions) [![Dependency freshness on GitHub](https://github.com/damian-buho/report-typo/actions/workflows/check-outdated.yaml/badge.svg?style=flat-square)](https://github.com/damian-buho/report-typo/actions) [![Analysis sweep on GitHub](https://github.com/damian-buho/report-typo/actions/workflows/analyzed.yaml/badge.svg?style=flat-square)](https://github.com/damian-buho/report-typo/actions)
20
+
21
+ [![Publish pipeline on kiota.ch](https://kiota.ch/damian-buho/report-typo/badges/workflows/published.yaml/badge.svg?style=flat-square)](https://kiota.ch/damian-buho/report-typo/actions) [![Vulnerability audit on kiota.ch](https://kiota.ch/damian-buho/report-typo/badges/workflows/audited.yaml/badge.svg?style=flat-square)](https://kiota.ch/damian-buho/report-typo/actions) [![Dependency freshness on kiota.ch](https://kiota.ch/damian-buho/report-typo/badges/workflows/check-outdated.yaml/badge.svg?style=flat-square)](https://kiota.ch/damian-buho/report-typo/actions) [![Analysis sweep on kiota.ch](https://kiota.ch/damian-buho/report-typo/badges/workflows/analyzed.yaml/badge.svg?style=flat-square)](https://kiota.ch/damian-buho/report-typo/actions)
22
+
23
+ ## What this provides
24
+
25
+ - **npm package** `report-typo`
26
+
27
+ ## Installation
28
+
29
+ Add the package to your project:
30
+
31
+ ```sh
32
+ npm install report-typo
33
+ ```
34
+
35
+ ## Building
36
+
37
+ Clone the repository and run `make` — every build step, target and entry point is in [Building](docs/BUILD.md).
38
+
39
+ ## Documentation
40
+
41
+ - [Add typo reporting to a site](docs/how-to/usage.md)
42
+
43
+ ## Policies
44
+
45
+ - [How to contribute](CONTRIBUTING.md)
46
+ - [Security policy](SECURITY.md)
47
+ - [Getting support](SUPPORT.md)
48
+ - [Code of Conduct](CODE_OF_CONDUCT.md)
49
+ - [AI and LLM Policy](AI_POLICY.md)
50
+
51
+ ## Links
52
+
53
+ - [Issues on GitHub](https://github.com/damian-buho/report-typo/issues)
54
+ - [Projectfile Specification](https://projectfile.org)
55
+
56
+ ## License
57
+
58
+ This project is licensed under MIT — see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,15 @@
1
+ import type { Draft } from './types.js';
2
+ /** Characters of context kept on each side of the selection. */
3
+ export declare const CONTEXT_CHARS = 64;
4
+ /** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */
5
+ export declare function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean;
6
+ /** Reports whether the event target is a field the reader is typing in. */
7
+ export declare function isEditable(target: EventTarget | null): boolean;
8
+ /** Bytes report-relay keeps of one free-text field. */
9
+ export declare const MAX_TEXT_BYTES = 1024;
10
+ /** Cuts text to a UTF-8 byte budget on a code point boundary. */
11
+ export declare function clipBytes(text: string, max?: number): string;
12
+ /** Builds a draft from the text before, inside and after a selection, cut to whole words. */
13
+ export declare function toDraft(before: string, selected: string, after: string, max: number): Draft;
14
+ /** Reads the current selection and its surrounding text from the page. */
15
+ export declare function capture(selection: Selection | null, max: number): Draft | undefined;
package/dist/core.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { type Format } from './transport/webhook.js';
2
+ import type { Messages, Send, Ui } from './types.js';
3
+ /** Options of `reportTypo`; `endpoint` or `send` is required. */
4
+ export interface Options {
5
+ endpoint?: string;
6
+ transport?: 'relay' | 'webhook';
7
+ format?: Format;
8
+ send?: Send;
9
+ locale?: string;
10
+ maxSelection?: number;
11
+ cooldownMs?: number;
12
+ comment?: boolean;
13
+ ui?: Ui;
14
+ }
15
+ /** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */
16
+ export declare function pick(catalog: Record<string, Messages>, tag: string): [string, Messages];
17
+ /** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */
18
+ export declare function create(catalog: Record<string, Messages>): (options: Options) => () => void;
@@ -0,0 +1,3 @@
1
+ import type { Confirmation, Draft, Messages } from './types.js';
2
+ /** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */
3
+ export declare function confirm(draft: Draft, messages: Messages, lang: string, hasComment: boolean): Promise<Confirmation | undefined>;
package/dist/en.js ADDED
@@ -0,0 +1,7 @@
1
+ var O={dir:"ltr",title:"Report a typo",send:"Send",cancel:"Cancel",comment:"Comment (optional)",commentPlaceholder:"What should it say?"};var B='input,textarea,select,[contenteditable]:not([contenteditable="false"])',X="p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body";function R(e){return(e.ctrlKey||e.metaKey)&&e.key==="Enter"}function A(e){return!!e?.closest?.(B)}var E=1024;function N(e,o=E){let t=new TextEncoder,n=e;for(;t.encode(n).length>o;)n=[...n].slice(0,-1).join("");return n}var S=e=>e.replaceAll(/\s+/g," ");function H(e,o,t,n){let r=S(e),c=S(t),i=r.slice(-64),s=c.slice(0,64);return{selection:S(o).trim().slice(0,n),prefix:(r.length>64?i.replace(/^\S*\s/,""):i).trimStart(),suffix:(c.length>64?s.replace(/\s\S*$/,""):s).trimEnd()}}function _(e,o){if(!e||e.isCollapsed||e.rangeCount===0)return;let t=e.getRangeAt(0),n=t.commonAncestorContainer,r=(n instanceof Element?n:n.parentElement)?.closest(X)??document.body,c=document.createRange();c.selectNodeContents(r),c.setEnd(t.startContainer,t.startOffset);let i=document.createRange();i.selectNodeContents(r),i.setStart(t.endContainer,t.endOffset);let s=H(c.toString(),t.toString(),i.toString(),o);return s.selection?s:void 0}var F=`dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);
2
+ color:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);
3
+ border:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}
4
+ p,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}
5
+ textarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}
6
+ menu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`,l=(e,o,t="")=>{let n=document.createElement(e);return n.textContent=t,o.append(n),n};function j(e,o,t,n){let r=document.createElement("report-typo"),c=r.attachShadow({mode:"closed"}),i=new CSSStyleSheet;i.replaceSync(F),c.adoptedStyleSheets=[i];let s=l("dialog",c);s.dir=o.dir,s.lang=t;let m=l("form",s);m.method="dialog",l("h2",m,o.title);let y=l("p",m,e.prefix);l("mark",y,e.selection),y.append(e.suffix);let f=n?l("label",m,o.comment):void 0,a=f?l("textarea",f):void 0;a&&(a.placeholder=o.commentPlaceholder,a.maxLength=E,a.rows=3);let g=l("menu",m);l("button",g,o.cancel).value="cancel";let b=l("button",g,o.send);return b.value="send",b.autofocus=!a,document.body.append(r),new Promise(h=>{s.addEventListener("close",()=>{r.remove(),h(s.returnValue==="send"?{comment:a?.value}:void 0)}),s.showModal(),a?.focus()})}var K=(e,o)=>fetch(e,{method:"POST",headers:{"Content-Type":"application/reports+json"},body:JSON.stringify([o]),keepalive:!0,credentials:"omit",mode:"cors",signal:AbortSignal.timeout(5e3)});function U(e,o=Math.random){let t=Number(e);return(Number.isFinite(t)&&t>0?t:1)*1e3*(1+o()/2)}function C(e){return async o=>{let t=await K(e,o);if(t.status===429){let n=U(t.headers.get("Retry-After"));console.debug("report-typo: relay busy, retrying",{status:t.status,wait:n}),await new Promise(r=>setTimeout(r,n)),t=await K(e,o)}if(!t.ok)throw new Error(`report-relay answered ${t.status}`)}}function k(e,o=t=>t){return async t=>{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(t)),keepalive:!0,credentials:"omit",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`webhook answered ${n.status}`)}}var p=(e,o)=>console.debug(`report-typo: ${e}`,o);function I(e,o){let t=o.toLowerCase().split("-",1)[0]??"",n=e[t];if(n)return[t,n];let[r]=Object.entries(e);if(!r)throw new TypeError("report-typo: empty locale catalog");return r}function P(e){return function(t){let{endpoint:n,transport:r="relay",format:c,maxSelection:i=256,cooldownMs:s=1e4,comment:m=!1}=t;if(!n&&!t.send)throw new TypeError("report-typo: set `endpoint` or `send`");let y=t.send??(r==="webhook"?k(n,c):C(n)),f=document.documentElement.lang,[a,g]=I(e,t.locale??(f||navigator.language)),b=t.ui??(d=>j(d,g,a,m)),h=-1/0,w=!1,D=async d=>{if(!R(d))return;if(A(d.target))return p("ignored in an editable field",{target:d.target});let u=_(getSelection(),i);if(!u)return p("ignored without a selection",{maxSelection:i});let M=h+s-Date.now();if(M>0)return p("ignored while cooling down",{wait:M});if(w)return p("ignored while the dialog is open",{selection:u.selection});d.preventDefault(),w=!0;try{let T=await b(u);if(!T)return p("cancelled",{selection:u.selection});h=Date.now();let x=N(T.comment?.trim()??""),L={...u,lang:f||a,...x&&{comment:x}};await y({type:"typo",age:0,url:location.href,body:L}),p("sent",{selection:u.selection,transport:r,comment:!!x})}catch(T){p("not sent",{error:T})}finally{w=!1}},v=d=>{D(d)};return document.addEventListener("keydown",v),p("listening",{locale:a,transport:r,cooldownMs:s}),()=>{document.removeEventListener("keydown",v),p("disposed",{locale:a})}}}var re=P({en:O});export{C as relay,re as reportTypo,k as webhook};
7
+ //# sourceMappingURL=en.js.map
package/dist/en.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../locales/en.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/en.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Report a typo\",\"send\":\"Send\",\"cancel\":\"Cancel\",\"comment\":\"Comment (optional)\",\"commentPlaceholder\":\"What should it say?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 10_000,\n comment = false\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n const send =\n options.send ?? (transport === 'webhook' ? webhook(endpoint!, format) : relay(endpoint!))\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: location.href, body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport en from '../../locales/en.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ en })\n"],"mappings":"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","names":["en_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","capture","selection","range","common","scope","draft","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","en_default"]}
@@ -0,0 +1,6 @@
1
+ export type { Options } from '../core.js';
2
+ export type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js';
3
+ export { relay } from '../transport/relay.js';
4
+ export { webhook, type Format } from '../transport/webhook.js';
5
+ /** Starts listening for Ctrl+Enter and returns the function that stops it. */
6
+ export declare const reportTypo: (options: import("../core.js").Options) => () => void;
@@ -0,0 +1,6 @@
1
+ export type { Options } from '../core.js';
2
+ export type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js';
3
+ export { relay } from '../transport/relay.js';
4
+ export { webhook, type Format } from '../transport/webhook.js';
5
+ /** Starts listening for Ctrl+Enter and returns the function that stops it. */
6
+ export declare const reportTypo: (options: import("../core.js").Options) => () => void;
@@ -0,0 +1,6 @@
1
+ export type { Options } from '../core.js';
2
+ export type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js';
3
+ export { relay } from '../transport/relay.js';
4
+ export { webhook, type Format } from '../transport/webhook.js';
5
+ /** Starts listening for Ctrl+Enter and returns the function that stops it. */
6
+ export declare const reportTypo: (options: import("../core.js").Options) => () => void;
package/dist/es.js ADDED
@@ -0,0 +1,7 @@
1
+ var O={dir:"ltr",title:"Avisar de una errata",send:"Enviar",cancel:"Cancelar",comment:"Comentario (opcional)",commentPlaceholder:"\xBFQu\xE9 deber\xEDa decir?"};var B='input,textarea,select,[contenteditable]:not([contenteditable="false"])',X="p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body";function R(e){return(e.ctrlKey||e.metaKey)&&e.key==="Enter"}function A(e){return!!e?.closest?.(B)}var E=1024;function N(e,o=E){let t=new TextEncoder,n=e;for(;t.encode(n).length>o;)n=[...n].slice(0,-1).join("");return n}var S=e=>e.replaceAll(/\s+/g," ");function H(e,o,t,n){let r=S(e),c=S(t),s=r.slice(-64),i=c.slice(0,64);return{selection:S(o).trim().slice(0,n),prefix:(r.length>64?s.replace(/^\S*\s/,""):s).trimStart(),suffix:(c.length>64?i.replace(/\s\S*$/,""):i).trimEnd()}}function _(e,o){if(!e||e.isCollapsed||e.rangeCount===0)return;let t=e.getRangeAt(0),n=t.commonAncestorContainer,r=(n instanceof Element?n:n.parentElement)?.closest(X)??document.body,c=document.createRange();c.selectNodeContents(r),c.setEnd(t.startContainer,t.startOffset);let s=document.createRange();s.selectNodeContents(r),s.setStart(t.endContainer,t.endOffset);let i=H(c.toString(),t.toString(),s.toString(),o);return i.selection?i:void 0}var F=`dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);
2
+ color:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);
3
+ border:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}
4
+ p,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}
5
+ textarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}
6
+ menu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`,l=(e,o,t="")=>{let n=document.createElement(e);return n.textContent=t,o.append(n),n};function j(e,o,t,n){let r=document.createElement("report-typo"),c=r.attachShadow({mode:"closed"}),s=new CSSStyleSheet;s.replaceSync(F),c.adoptedStyleSheets=[s];let i=l("dialog",c);i.dir=o.dir,i.lang=t;let m=l("form",i);m.method="dialog",l("h2",m,o.title);let y=l("p",m,e.prefix);l("mark",y,e.selection),y.append(e.suffix);let f=n?l("label",m,o.comment):void 0,a=f?l("textarea",f):void 0;a&&(a.placeholder=o.commentPlaceholder,a.maxLength=E,a.rows=3);let g=l("menu",m);l("button",g,o.cancel).value="cancel";let b=l("button",g,o.send);return b.value="send",b.autofocus=!a,document.body.append(r),new Promise(h=>{i.addEventListener("close",()=>{r.remove(),h(i.returnValue==="send"?{comment:a?.value}:void 0)}),i.showModal(),a?.focus()})}var K=(e,o)=>fetch(e,{method:"POST",headers:{"Content-Type":"application/reports+json"},body:JSON.stringify([o]),keepalive:!0,credentials:"omit",mode:"cors",signal:AbortSignal.timeout(5e3)});function U(e,o=Math.random){let t=Number(e);return(Number.isFinite(t)&&t>0?t:1)*1e3*(1+o()/2)}function C(e){return async o=>{let t=await K(e,o);if(t.status===429){let n=U(t.headers.get("Retry-After"));console.debug("report-typo: relay busy, retrying",{status:t.status,wait:n}),await new Promise(r=>setTimeout(r,n)),t=await K(e,o)}if(!t.ok)throw new Error(`report-relay answered ${t.status}`)}}function k(e,o=t=>t){return async t=>{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(t)),keepalive:!0,credentials:"omit",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`webhook answered ${n.status}`)}}var d=(e,o)=>console.debug(`report-typo: ${e}`,o);function I(e,o){let t=o.toLowerCase().split("-",1)[0]??"",n=e[t];if(n)return[t,n];let[r]=Object.entries(e);if(!r)throw new TypeError("report-typo: empty locale catalog");return r}function P(e){return function(t){let{endpoint:n,transport:r="relay",format:c,maxSelection:s=256,cooldownMs:i=1e4,comment:m=!1}=t;if(!n&&!t.send)throw new TypeError("report-typo: set `endpoint` or `send`");let y=t.send??(r==="webhook"?k(n,c):C(n)),f=document.documentElement.lang,[a,g]=I(e,t.locale??(f||navigator.language)),b=t.ui??(p=>j(p,g,a,m)),h=-1/0,w=!1,D=async p=>{if(!R(p))return;if(A(p.target))return d("ignored in an editable field",{target:p.target});let u=_(getSelection(),s);if(!u)return d("ignored without a selection",{maxSelection:s});let M=h+i-Date.now();if(M>0)return d("ignored while cooling down",{wait:M});if(w)return d("ignored while the dialog is open",{selection:u.selection});p.preventDefault(),w=!0;try{let T=await b(u);if(!T)return d("cancelled",{selection:u.selection});h=Date.now();let x=N(T.comment?.trim()??""),L={...u,lang:f||a,...x&&{comment:x}};await y({type:"typo",age:0,url:location.href,body:L}),d("sent",{selection:u.selection,transport:r,comment:!!x})}catch(T){d("not sent",{error:T})}finally{w=!1}},v=p=>{D(p)};return document.addEventListener("keydown",v),d("listening",{locale:a,transport:r,cooldownMs:i}),()=>{document.removeEventListener("keydown",v),d("disposed",{locale:a})}}}var re=P({es:O});export{C as relay,re as reportTypo,k as webhook};
7
+ //# sourceMappingURL=es.js.map
package/dist/es.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../locales/es.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/es.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Avisar de una errata\",\"send\":\"Enviar\",\"cancel\":\"Cancelar\",\"comment\":\"Comentario (opcional)\",\"commentPlaceholder\":\"¿Qué debería decir?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 10_000,\n comment = false\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n const send =\n options.send ?? (transport === 'webhook' ? webhook(endpoint!, format) : relay(endpoint!))\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: location.href, body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport es from '../../locales/es.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ es })\n"],"mappings":"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","names":["es_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","capture","selection","range","common","scope","draft","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","es_default"]}
@@ -0,0 +1,6 @@
1
+ export type { Options } from './core.js';
2
+ export type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from './types.js';
3
+ export { relay } from './transport/relay.js';
4
+ export { webhook, type Format } from './transport/webhook.js';
5
+ /** Starts listening for Ctrl+Enter and returns the function that stops it. */
6
+ export declare const reportTypo: (options: import("./core.js").Options) => () => void;
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ var O={dir:"ltr",title:"Report a typo",send:"Send",cancel:"Cancel",comment:"Comment (optional)",commentPlaceholder:"What should it say?"};var R={dir:"ltr",title:"Avisar de una errata",send:"Enviar",cancel:"Cancelar",comment:"Comentario (opcional)",commentPlaceholder:"\xBFQu\xE9 deber\xEDa decir?"};var A={dir:"ltr",title:"\u041F\u043E\u0432\u0456\u0434\u043E\u043C\u0438\u0442\u0438 \u043F\u0440\u043E \u043E\u0434\u0440\u0443\u043A\u0456\u0432\u043A\u0443",send:"\u041D\u0430\u0434\u0456\u0441\u043B\u0430\u0442\u0438",cancel:"\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",comment:"\u041A\u043E\u043C\u0435\u043D\u0442\u0430\u0440 (\u043D\u0435\u043E\u0431\u043E\u0432\u2019\u044F\u0437\u043A\u043E\u0432\u043E)",commentPlaceholder:"\u042F\u043A \u043C\u0430\u0454 \u0431\u0443\u0442\u0438?"};var H='input,textarea,select,[contenteditable]:not([contenteditable="false"])',F="p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body";function N(e){return(e.ctrlKey||e.metaKey)&&e.key==="Enter"}function P(e){return!!e?.closest?.(H)}var E=1024;function _(e,o=E){let t=new TextEncoder,n=e;for(;t.encode(n).length>o;)n=[...n].slice(0,-1).join("");return n}var S=e=>e.replaceAll(/\s+/g," ");function U(e,o,t,n){let r=S(e),c=S(t),s=r.slice(-64),i=c.slice(0,64);return{selection:S(o).trim().slice(0,n),prefix:(r.length>64?s.replace(/^\S*\s/,""):s).trimStart(),suffix:(c.length>64?i.replace(/\s\S*$/,""):i).trimEnd()}}function j(e,o){if(!e||e.isCollapsed||e.rangeCount===0)return;let t=e.getRangeAt(0),n=t.commonAncestorContainer,r=(n instanceof Element?n:n.parentElement)?.closest(F)??document.body,c=document.createRange();c.selectNodeContents(r),c.setEnd(t.startContainer,t.startOffset);let s=document.createRange();s.selectNodeContents(r),s.setStart(t.endContainer,t.endOffset);let i=U(c.toString(),t.toString(),s.toString(),o);return i.selection?i:void 0}var I=`dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);
2
+ color:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);
3
+ border:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}
4
+ p,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}
5
+ textarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}
6
+ menu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`,l=(e,o,t="")=>{let n=document.createElement(e);return n.textContent=t,o.append(n),n};function K(e,o,t,n){let r=document.createElement("report-typo"),c=r.attachShadow({mode:"closed"}),s=new CSSStyleSheet;s.replaceSync(I),c.adoptedStyleSheets=[s];let i=l("dialog",c);i.dir=o.dir,i.lang=t;let m=l("form",i);m.method="dialog",l("h2",m,o.title);let y=l("p",m,e.prefix);l("mark",y,e.selection),y.append(e.suffix);let f=n?l("label",m,o.comment):void 0,a=f?l("textarea",f):void 0;a&&(a.placeholder=o.commentPlaceholder,a.maxLength=E,a.rows=3);let g=l("menu",m);l("button",g,o.cancel).value="cancel";let b=l("button",g,o.send);return b.value="send",b.autofocus=!a,document.body.append(r),new Promise(h=>{i.addEventListener("close",()=>{r.remove(),h(i.returnValue==="send"?{comment:a?.value}:void 0)}),i.showModal(),a?.focus()})}var D=(e,o)=>fetch(e,{method:"POST",headers:{"Content-Type":"application/reports+json"},body:JSON.stringify([o]),keepalive:!0,credentials:"omit",mode:"cors",signal:AbortSignal.timeout(5e3)});function $(e,o=Math.random){let t=Number(e);return(Number.isFinite(t)&&t>0?t:1)*1e3*(1+o()/2)}function C(e){return async o=>{let t=await D(e,o);if(t.status===429){let n=$(t.headers.get("Retry-After"));console.debug("report-typo: relay busy, retrying",{status:t.status,wait:n}),await new Promise(r=>setTimeout(r,n)),t=await D(e,o)}if(!t.ok)throw new Error(`report-relay answered ${t.status}`)}}function k(e,o=t=>t){return async t=>{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(t)),keepalive:!0,credentials:"omit",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`webhook answered ${n.status}`)}}var d=(e,o)=>console.debug(`report-typo: ${e}`,o);function z(e,o){let t=o.toLowerCase().split("-",1)[0]??"",n=e[t];if(n)return[t,n];let[r]=Object.entries(e);if(!r)throw new TypeError("report-typo: empty locale catalog");return r}function L(e){return function(t){let{endpoint:n,transport:r="relay",format:c,maxSelection:s=256,cooldownMs:i=1e4,comment:m=!1}=t;if(!n&&!t.send)throw new TypeError("report-typo: set `endpoint` or `send`");let y=t.send??(r==="webhook"?k(n,c):C(n)),f=document.documentElement.lang,[a,g]=z(e,t.locale??(f||navigator.language)),b=t.ui??(p=>K(p,g,a,m)),h=-1/0,x=!1,B=async p=>{if(!N(p))return;if(P(p.target))return d("ignored in an editable field",{target:p.target});let u=j(getSelection(),s);if(!u)return d("ignored without a selection",{maxSelection:s});let M=h+i-Date.now();if(M>0)return d("ignored while cooling down",{wait:M});if(x)return d("ignored while the dialog is open",{selection:u.selection});p.preventDefault(),x=!0;try{let T=await b(u);if(!T)return d("cancelled",{selection:u.selection});h=Date.now();let w=_(T.comment?.trim()??""),X={...u,lang:f||a,...w&&{comment:w}};await y({type:"typo",age:0,url:location.href,body:X}),d("sent",{selection:u.selection,transport:r,comment:!!w})}catch(T){d("not sent",{error:T})}finally{x=!1}},v=p=>{B(p)};return document.addEventListener("keydown",v),d("listening",{locale:a,transport:r,cooldownMs:i}),()=>{document.removeEventListener("keydown",v),d("disposed",{locale:a})}}}var de=L({en:O,es:R,uk:A});export{C as relay,de as reportTypo,k as webhook};
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../locales/en.yaml","../locales/es.yaml","../locales/uk.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/index.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Report a typo\",\"send\":\"Send\",\"cancel\":\"Cancel\",\"comment\":\"Comment (optional)\",\"commentPlaceholder\":\"What should it say?\"}","export default {\"dir\":\"ltr\",\"title\":\"Avisar de una errata\",\"send\":\"Enviar\",\"cancel\":\"Cancelar\",\"comment\":\"Comentario (opcional)\",\"commentPlaceholder\":\"¿Qué debería decir?\"}","export default {\"dir\":\"ltr\",\"title\":\"Повідомити про одруківку\",\"send\":\"Надіслати\",\"cancel\":\"Скасувати\",\"comment\":\"Коментар (необов’язково)\",\"commentPlaceholder\":\"Як має бути?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 10_000,\n comment = false\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n const send =\n options.send ?? (transport === 'webhook' ? webhook(endpoint!, format) : relay(endpoint!))\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: location.href, body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport en from '../locales/en.yaml'\nimport es from '../locales/es.yaml'\nimport uk from '../locales/uk.yaml'\nimport { create } from './core.js'\n\nexport type { Options } from './core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from './types.js'\nexport { relay } from './transport/relay.js'\nexport { webhook, type Format } from './transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ en, es, uk })\n"],"mappings":"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","names":["en_default","es_default","uk_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","capture","selection","range","common","scope","draft","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","en_default","es_default","uk_default"]}
@@ -0,0 +1,7 @@
1
+ import type { Send } from '../types.js';
2
+ /** Per-attempt deadline of one delivery. */
3
+ export declare const TIMEOUT_MS = 5000;
4
+ /** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */
5
+ export declare function retryAfterMs(header: string | null, random?: () => number): number;
6
+ /** Sends reports to a report-relay intake, retrying once on 429. */
7
+ export declare function relay(endpoint: string): Send;
@@ -0,0 +1,5 @@
1
+ import type { Send, TypoReport } from '../types.js';
2
+ /** Turns a report into the JSON a webhook expects. */
3
+ export type Format = (report: TypoReport) => unknown;
4
+ /** Sends reports as a plain JSON POST, shaped by `format` when given. */
5
+ export declare function webhook(url: string, format?: Format): Send;
@@ -0,0 +1,35 @@
1
+ /** The text a reader selected, with the words around it. */
2
+ export interface Draft {
3
+ selection: string;
4
+ prefix: string;
5
+ suffix: string;
6
+ }
7
+ /** The body of a `typo` report, as report-relay accepts it. */
8
+ export interface TypoBody extends Draft {
9
+ lang: string;
10
+ comment?: string;
11
+ }
12
+ /** One report in the Reporting API envelope. */
13
+ export interface TypoReport {
14
+ type: 'typo';
15
+ age: number;
16
+ url: string;
17
+ body: TypoBody;
18
+ }
19
+ /** What the reader confirmed. */
20
+ export interface Confirmation {
21
+ comment?: string;
22
+ }
23
+ /** Shows a draft to the reader; resolves null or undefined when they cancel. */
24
+ export type Ui = (draft: Draft) => Promise<Confirmation | null | undefined>;
25
+ /** Delivers one report; rejects when it was not delivered. */
26
+ export type Send = (report: TypoReport) => Promise<void>;
27
+ /** The strings of one locale. */
28
+ export interface Messages {
29
+ dir: 'ltr' | 'rtl';
30
+ title: string;
31
+ send: string;
32
+ cancel: string;
33
+ comment: string;
34
+ commentPlaceholder: string;
35
+ }
package/dist/uk.js ADDED
@@ -0,0 +1,7 @@
1
+ var O={dir:"ltr",title:"\u041F\u043E\u0432\u0456\u0434\u043E\u043C\u0438\u0442\u0438 \u043F\u0440\u043E \u043E\u0434\u0440\u0443\u043A\u0456\u0432\u043A\u0443",send:"\u041D\u0430\u0434\u0456\u0441\u043B\u0430\u0442\u0438",cancel:"\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",comment:"\u041A\u043E\u043C\u0435\u043D\u0442\u0430\u0440 (\u043D\u0435\u043E\u0431\u043E\u0432\u2019\u044F\u0437\u043A\u043E\u0432\u043E)",commentPlaceholder:"\u042F\u043A \u043C\u0430\u0454 \u0431\u0443\u0442\u0438?"};var B='input,textarea,select,[contenteditable]:not([contenteditable="false"])',X="p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body";function R(e){return(e.ctrlKey||e.metaKey)&&e.key==="Enter"}function A(e){return!!e?.closest?.(B)}var E=1024;function N(e,o=E){let t=new TextEncoder,n=e;for(;t.encode(n).length>o;)n=[...n].slice(0,-1).join("");return n}var S=e=>e.replaceAll(/\s+/g," ");function H(e,o,t,n){let r=S(e),c=S(t),i=r.slice(-64),s=c.slice(0,64);return{selection:S(o).trim().slice(0,n),prefix:(r.length>64?i.replace(/^\S*\s/,""):i).trimStart(),suffix:(c.length>64?s.replace(/\s\S*$/,""):s).trimEnd()}}function _(e,o){if(!e||e.isCollapsed||e.rangeCount===0)return;let t=e.getRangeAt(0),n=t.commonAncestorContainer,r=(n instanceof Element?n:n.parentElement)?.closest(X)??document.body,c=document.createRange();c.selectNodeContents(r),c.setEnd(t.startContainer,t.startOffset);let i=document.createRange();i.selectNodeContents(r),i.setStart(t.endContainer,t.endOffset);let s=H(c.toString(),t.toString(),i.toString(),o);return s.selection?s:void 0}var F=`dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);
2
+ color:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);
3
+ border:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}
4
+ p,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}
5
+ textarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}
6
+ menu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`,l=(e,o,t="")=>{let n=document.createElement(e);return n.textContent=t,o.append(n),n};function j(e,o,t,n){let r=document.createElement("report-typo"),c=r.attachShadow({mode:"closed"}),i=new CSSStyleSheet;i.replaceSync(F),c.adoptedStyleSheets=[i];let s=l("dialog",c);s.dir=o.dir,s.lang=t;let m=l("form",s);m.method="dialog",l("h2",m,o.title);let y=l("p",m,e.prefix);l("mark",y,e.selection),y.append(e.suffix);let f=n?l("label",m,o.comment):void 0,a=f?l("textarea",f):void 0;a&&(a.placeholder=o.commentPlaceholder,a.maxLength=E,a.rows=3);let g=l("menu",m);l("button",g,o.cancel).value="cancel";let b=l("button",g,o.send);return b.value="send",b.autofocus=!a,document.body.append(r),new Promise(h=>{s.addEventListener("close",()=>{r.remove(),h(s.returnValue==="send"?{comment:a?.value}:void 0)}),s.showModal(),a?.focus()})}var K=(e,o)=>fetch(e,{method:"POST",headers:{"Content-Type":"application/reports+json"},body:JSON.stringify([o]),keepalive:!0,credentials:"omit",mode:"cors",signal:AbortSignal.timeout(5e3)});function U(e,o=Math.random){let t=Number(e);return(Number.isFinite(t)&&t>0?t:1)*1e3*(1+o()/2)}function k(e){return async o=>{let t=await K(e,o);if(t.status===429){let n=U(t.headers.get("Retry-After"));console.debug("report-typo: relay busy, retrying",{status:t.status,wait:n}),await new Promise(r=>setTimeout(r,n)),t=await K(e,o)}if(!t.ok)throw new Error(`report-relay answered ${t.status}`)}}function C(e,o=t=>t){return async t=>{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(t)),keepalive:!0,credentials:"omit",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`webhook answered ${n.status}`)}}var p=(e,o)=>console.debug(`report-typo: ${e}`,o);function I(e,o){let t=o.toLowerCase().split("-",1)[0]??"",n=e[t];if(n)return[t,n];let[r]=Object.entries(e);if(!r)throw new TypeError("report-typo: empty locale catalog");return r}function P(e){return function(t){let{endpoint:n,transport:r="relay",format:c,maxSelection:i=256,cooldownMs:s=1e4,comment:m=!1}=t;if(!n&&!t.send)throw new TypeError("report-typo: set `endpoint` or `send`");let y=t.send??(r==="webhook"?C(n,c):k(n)),f=document.documentElement.lang,[a,g]=I(e,t.locale??(f||navigator.language)),b=t.ui??(d=>j(d,g,a,m)),h=-1/0,w=!1,D=async d=>{if(!R(d))return;if(A(d.target))return p("ignored in an editable field",{target:d.target});let u=_(getSelection(),i);if(!u)return p("ignored without a selection",{maxSelection:i});let M=h+s-Date.now();if(M>0)return p("ignored while cooling down",{wait:M});if(w)return p("ignored while the dialog is open",{selection:u.selection});d.preventDefault(),w=!0;try{let T=await b(u);if(!T)return p("cancelled",{selection:u.selection});h=Date.now();let x=N(T.comment?.trim()??""),L={...u,lang:f||a,...x&&{comment:x}};await y({type:"typo",age:0,url:location.href,body:L}),p("sent",{selection:u.selection,transport:r,comment:!!x})}catch(T){p("not sent",{error:T})}finally{w=!1}},v=d=>{D(d)};return document.addEventListener("keydown",v),p("listening",{locale:a,transport:r,cooldownMs:s}),()=>{document.removeEventListener("keydown",v),p("disposed",{locale:a})}}}var re=P({uk:O});export{k as relay,re as reportTypo,C as webhook};
7
+ //# sourceMappingURL=uk.js.map
package/dist/uk.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../locales/uk.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/uk.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Повідомити про одруківку\",\"send\":\"Надіслати\",\"cancel\":\"Скасувати\",\"comment\":\"Коментар (необов’язково)\",\"commentPlaceholder\":\"Як має бути?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 10_000,\n comment = false\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n const send =\n options.send ?? (transport === 'webhook' ? webhook(endpoint!, format) : relay(endpoint!))\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: location.href, body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport uk from '../../locales/uk.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ uk })\n"],"mappings":"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","names":["uk_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","capture","selection","range","common","scope","draft","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","uk_default"]}
package/package.json CHANGED
@@ -1,6 +1,121 @@
1
1
  {
2
2
  "name": "report-typo",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Ctrl+Enter typo reporting for any website",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./en": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/en.js"
16
+ },
17
+ "./es": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/es.js"
20
+ },
21
+ "./uk": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/uk.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist/"
28
+ ],
29
+ "sideEffects": false,
30
+ "engines": {
31
+ "node": ">=24"
32
+ },
33
+ "scripts": {
34
+ "prebuild": "rm -rf dist",
35
+ "build": "tsup && tsc --project tsconfig.build.json",
36
+ "build:test": "TEST_BUILD=1 tsup",
37
+ "pretest": "npm run build:test",
38
+ "test": "node --test 'dist/test/*.test.js'",
39
+ "posttest": "npm run build && size-limit",
40
+ "typecheck": "tsc --noEmit",
41
+ "check-size": "size-limit",
42
+ "prepublishOnly": "npm run build",
43
+ "lint": "eslint ./",
44
+ "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
45
+ "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'"
46
+ },
47
+ "keywords": [
48
+ "typo",
49
+ "orphus",
50
+ "proofreading",
51
+ "reporting-api",
52
+ "browser"
53
+ ],
54
+ "author": {
55
+ "email": "damian.buho@proton.me",
56
+ "name": "Damián Búho",
57
+ "url": "https://dbuho.me"
58
+ },
59
+ "license": "MIT",
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/damian-buho/report-typo.git"
66
+ },
67
+ "bugs": {
68
+ "url": "https://github.com/damian-buho/report-typo/issues"
69
+ },
70
+ "homepage": "https://github.com/damian-buho/report-typo#readme",
71
+ "devDependencies": {
72
+ "@eslint/js": "10.0.1",
73
+ "@size-limit/preset-small-lib": "14.2.0",
74
+ "@types/eslint": "9.6.1",
75
+ "@types/node": "26.6.5",
76
+ "esbuild": "0.28.2",
77
+ "eslint": "10.12.0",
78
+ "eslint-config-prettier": "10.1.8",
79
+ "eslint-plugin-import-x": "4.17.1",
80
+ "eslint-plugin-n": "18.4.1",
81
+ "eslint-plugin-promise": "7.3.0",
82
+ "eslint-plugin-tsdoc": "0.5.4",
83
+ "eslint-plugin-unicorn": "78.0.0",
84
+ "globals": "17.13.0",
85
+ "prettier": "3.9.10",
86
+ "size-limit": "14.2.0",
87
+ "tsup": "8.5.1",
88
+ "typescript": "6.0.3",
89
+ "typescript-eslint": "8.71.2",
90
+ "yaml": "2.9.1"
91
+ },
92
+ "prettier": {
93
+ "arrowParens": "avoid",
94
+ "bracketSpacing": true,
95
+ "printWidth": 100,
96
+ "quoteProps": "as-needed",
97
+ "semi": false,
98
+ "singleQuote": true,
99
+ "tabWidth": 2,
100
+ "trailingComma": "none",
101
+ "useTabs": false
102
+ },
103
+ "overrides": {
104
+ "esbuild": "0.28.2",
105
+ "globals": "$globals"
106
+ },
107
+ "size-limit": [
108
+ {
109
+ "path": "./dist/index.js",
110
+ "limit": "3 kB",
111
+ "gzip": true
112
+ }
113
+ ],
114
+ "maintainers": [
115
+ {
116
+ "email": "damian.buho@proton.me",
117
+ "name": "Damián Búho",
118
+ "url": "https://dbuho.me"
119
+ }
120
+ ]
121
+ }