@getpinax/widget 1.0.0-beta.1 → 1.0.0-beta.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/dist/preview.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { type SettingsInput } from './settings';
2
+ import type { KnownIssue } from './types';
2
3
  import { type WidgetView } from './ui/widget';
3
4
  export type { SettingsInput } from './settings';
4
- export type { CustomField, WidgetSettings, WidgetWording, } from './types';
5
+ export type { CustomField, KnownIssue, WidgetSettings, WidgetWording, } from './types';
5
6
  export type { WidgetView } from './ui/widget';
6
7
  export type PreviewOptions = {
7
8
  /** Screen to show. Default 'button' (closed). */
@@ -16,6 +17,18 @@ export type PreviewOptions = {
16
17
  primary?: string | null;
17
18
  accent?: string | null;
18
19
  };
20
+ /**
21
+ * Known issues to show on the chooser ('choose'), as if they matched this
22
+ * page (the first 3). Without them the card never shows; with
23
+ * `knownIssues.enabled` off in the settings it doesn't either. "Me too"
24
+ * only shows the thanks.
25
+ */
26
+ known?: KnownIssue[];
27
+ /**
28
+ * Who the form's notice says reports go to ("Sent to … with your note"),
29
+ * e.g. the site's name or domain. Default "this website".
30
+ */
31
+ site?: string;
19
32
  };
20
33
  export type PreviewHandle = {
21
34
  /** Re-render with new settings, staying on the screen on show. */
package/dist/preview.mjs CHANGED
@@ -1,54 +1,67 @@
1
- import { a as e, n as t, o as n, t as r } from "./chunks/widget-CkR1Ptzb.mjs";
1
+ import { a as e, n as t, o as n, s as r, t as i } from "./chunks/widget-BpTBdX2R.mjs";
2
2
  // src/preview.ts
3
- var i = `
3
+ var a = `
4
4
  :host { display: block; position: relative; overflow: hidden; }
5
5
  .preview .launcher, .preview .panel { position: absolute; }
6
6
  .preview .panel { max-height: calc(100% - 108px); }
7
7
  .preview.pos-right-edge .panel, .preview.no-button .panel { max-height: calc(100% - 48px); }
8
8
  ${t("", ".pnx.phone")}
9
9
  .pnx.preview.phone .panel { max-height: 88%; }
10
- `, a = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22340%22%20viewBox%3D%220%200%20600%20340%22%3E%3Crect%20width%3D%22600%22%20height%3D%22340%22%20fill%3D%22%23F4EDE2%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%22600%22%20height%3D%2244%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%2224%22%20y%3D%2216%22%20width%3D%22120%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23111827%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%2282%22%20width%3D%22300%22%20height%3D%2222%22%20rx%3D%228%22%20fill%3D%22%23111827%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22118%22%20width%3D%22380%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23C9C2B6%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22140%22%20width%3D%22330%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23C9C2B6%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22186%22%20width%3D%22250%22%20height%3D%22110%22%20rx%3D%2214%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%22316%22%20y%3D%22186%22%20width%3D%22250%22%20height%3D%22110%22%20rx%3D%2214%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%2260%22%20y%3D%22250%22%20width%3D%22110%22%20height%3D%2228%22%20rx%3D%2214%22%20fill%3D%22%23FF5C35%22%2F%3E%3C%2Fsvg%3E";
11
- function o(t, o, s = {}) {
12
- let c = t.shadowRoot ?? t.attachShadow({ mode: "open" }), l = { ...s }, u = null, d = (t, o) => {
13
- u?.destroy(), c.replaceChildren(), u = new r(c, {
10
+ `, o = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22340%22%20viewBox%3D%220%200%20600%20340%22%3E%3Crect%20width%3D%22600%22%20height%3D%22340%22%20fill%3D%22%23F4EDE2%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%22600%22%20height%3D%2244%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%2224%22%20y%3D%2216%22%20width%3D%22120%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23111827%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%2282%22%20width%3D%22300%22%20height%3D%2222%22%20rx%3D%228%22%20fill%3D%22%23111827%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22118%22%20width%3D%22380%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23C9C2B6%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22140%22%20width%3D%22330%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23C9C2B6%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22186%22%20width%3D%22250%22%20height%3D%22110%22%20rx%3D%2214%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%22316%22%20y%3D%22186%22%20width%3D%22250%22%20height%3D%22110%22%20rx%3D%2214%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%2260%22%20y%3D%22250%22%20width%3D%22110%22%20height%3D%2228%22%20rx%3D%2214%22%20fill%3D%22%23FF5C35%22%2F%3E%3C%2Fsvg%3E";
11
+ function s(t, s, c = {}) {
12
+ let l = t.shadowRoot ?? t.attachShadow({ mode: "open" }), u = { ...c }, d = null, f = (t, s) => {
13
+ d?.destroy(), l.replaceChildren();
14
+ let c = r(t), f = n(u.known, c.knownIssues);
15
+ d = new i(l, {
14
16
  enabled: !0,
15
- settings: n(t),
17
+ settings: c,
16
18
  branding: e({
17
- ...l.branding,
18
- poweredBy: l.poweredBy !== !1
19
+ ...u.branding,
20
+ poweredBy: u.poweredBy !== !1
19
21
  })
20
22
  }, {
21
23
  takeScreenshot: async () => ({
22
- dataUrl: a,
24
+ dataUrl: o,
23
25
  annotations: []
24
26
  }),
25
27
  markUp: async () => null,
26
28
  submit: async () => ({ status: "sent" }),
27
- user: () => void 0
29
+ user: () => void 0,
30
+ knownIssues: () => f,
31
+ meToo: async () => !0,
32
+ attachments: () => {
33
+ let e = c.consent?.required === !0, { console: t, network: n } = c.capture;
34
+ return {
35
+ logs: !e,
36
+ askLogs: e && (t || n),
37
+ replay: !1
38
+ };
39
+ },
40
+ site: () => u.site || "this website"
28
41
  }, {
29
- className: l.phone ? "preview phone" : "preview",
30
- css: i
31
- }), o !== "button" && u.showView(o);
42
+ className: u.phone ? "preview phone" : "preview",
43
+ css: a
44
+ }), s !== "button" && d.showView(s);
32
45
  };
33
- return d(o, l.view ?? "button"), {
46
+ return f(s, u.view ?? "button"), {
34
47
  update(e, t) {
35
- let n = u?.currentView() ?? "button";
36
- t && (l = {
37
- ...l,
48
+ let n = d?.currentView() ?? "button";
49
+ t && (u = {
50
+ ...u,
38
51
  ...t
39
- }), d(e, t?.view ?? n);
52
+ }), f(e, t?.view ?? n);
40
53
  },
41
54
  show(e) {
42
- u?.showView(e);
55
+ d?.showView(e);
43
56
  },
44
57
  view() {
45
- return u?.currentView() ?? "button";
58
+ return d?.currentView() ?? "button";
46
59
  },
47
60
  destroy() {
48
- u?.destroy(), u = null, c.replaceChildren();
61
+ d?.destroy(), d = null, l.replaceChildren();
49
62
  }
50
63
  };
51
64
  }
52
- export { o as preview };
65
+ export { s as preview };
53
66
 
54
67
  //# sourceMappingURL=preview.mjs.map
package/dist/privacy.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
- * AI-12: text patterns for personal details. Used only by the lazily loaded
3
- * chunks (screenshot auto-blur, replay masking), never by the initial script.
2
+ * AI-12: text patterns for personal details. Screenshot auto-blur and replay
3
+ * masking use all of it; the initial script uses only `redactPersonal` (and
4
+ * what it needs), for console text, breadcrumb targets and error signatures.
5
+ * Mirrored by `redactPersonal` in `backend/src/modules/widget/widget.sanitise.ts`.
4
6
  */
5
7
  export type Span = {
6
8
  start: number;
package/dist/queue.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type Fetch } from './api';
2
2
  import { type ReportBody } from './report';
3
- /** What may be kept in localStorage: the report without the private extras. */
3
+ export declare const MAX_AGE_MS: number;
4
+ /** What may be kept in localStorage: the report without the private parts. */
4
5
  export declare function queuedCopy(body: ReportBody): ReportBody;
5
6
  /** Saves a slimmed copy of a report for later (see above). */
6
7
  export declare function enqueue(key: string, body: ReportBody): boolean;
@@ -10,6 +10,16 @@ export type Recorder = {
10
10
  exportReplay(maxChars: number): Promise<string | null>;
11
11
  stop(): void;
12
12
  };
13
+ /**
14
+ * `'all'` (the default, CNIL's "mask everything"): every text is starred out
15
+ * and images, pictures, videos, canvases and frames are left out (the replay
16
+ * shows an empty box of the same size). `'inputs'`: form fields, editable
17
+ * text, emails and phone numbers only.
18
+ */
19
+ export type ReplayMask = 'all' | 'inputs';
20
+ /** Left out entirely with `mask: 'all'`; rrweb records only their size. */
21
+ export declare const MEDIA = "img,picture,video,canvas,iframe";
13
22
  export declare function startRecorder(options?: {
14
23
  exclude?: string;
24
+ mask?: ReplayMask;
15
25
  }): Recorder;
@@ -2,12 +2,18 @@ import { type ReplayEvent } from './buffer';
2
2
  /** A URL without its fragment or secret query values, and no email or phone. */
3
3
  export declare function cleanUrl(value: string, base?: string): string;
4
4
  type Attrs = Record<string, unknown>;
5
- /** A cleaned copy of a node's (or a mutation's) attributes. */
6
- export declare function cleanAttributes(attrs: Attrs, tagName?: string, base?: string): Attrs;
5
+ /**
6
+ * A cleaned copy of a node's (or a mutation's) attributes. `maskText` also
7
+ * stars out the text attributes above.
8
+ */
9
+ export declare function cleanAttributes(attrs: Attrs, tagName?: string, base?: string, maskText?: boolean): Attrs;
7
10
  /**
8
11
  * The events as JSON, with every attribute cleaned: full snapshot nodes
9
12
  * (`{ tagName, attributes }`), mutation adds (same shape) and attribute
10
13
  * changes (`{ id, attributes }`). Meta events get a cleaned `href`.
14
+ * `maskText` (replay mask 'all') stars out text attributes as well.
11
15
  */
12
- export declare function serializeReplay(events: readonly ReplayEvent[], base?: string | undefined): string;
16
+ export declare function serializeReplay(events: readonly ReplayEvent[], base?: string | undefined, options?: {
17
+ maskText?: boolean;
18
+ }): string;
13
19
  export {};
package/dist/report.d.ts CHANGED
@@ -2,7 +2,8 @@ import type { Breadcrumb } from './capture/breadcrumbs';
2
2
  import type { ConsoleEntry } from './capture/console';
3
3
  import type { Environment } from './capture/env';
4
4
  import type { NetworkEntry } from './capture/network';
5
- import type { Annotation, FieldAnswer, PinaxUser, ReportType } from './types';
5
+ import type { A11yScan, Annotation, Barrier, FieldAnswer, PinaxUser, ReportType } from './types';
6
+ export declare const BARRIERS: Barrier[];
6
7
  /**
7
8
  * Builds the POST /reports body. Mirrors the limits in
8
9
  * backend/src/modules/widget/widget.validator.ts so a report is never rejected
@@ -44,9 +45,21 @@ export type ReportInput = {
44
45
  };
45
46
  /** W-10 */
46
47
  fields?: FieldAnswer[];
48
+ /** W-19, W-20: accessibility reports only. */
49
+ barrier?: Barrier;
50
+ a11yScan?: A11yScan | null;
47
51
  /** P-04: honeypot value and time to fill in the form. */
48
52
  hp?: string;
49
53
  elapsedMs?: number;
54
+ /** Visitor consent, only on sites that wait for it. */
55
+ consent?: ReportConsent;
56
+ /** The visitor ticked "Include technical details" (consent not given). */
57
+ includeLogs?: boolean;
58
+ };
59
+ /** What the visitor agreed to through the site's banner. */
60
+ export type ReportConsent = {
61
+ diagnostics: boolean;
62
+ replay: boolean;
50
63
  };
51
64
  export type ReportContext = {
52
65
  page: {
@@ -79,9 +92,13 @@ export type ReportBody = {
79
92
  answer: string;
80
93
  };
81
94
  fields?: FieldAnswer[];
95
+ barrier?: Barrier;
96
+ a11yScan?: A11yScan;
82
97
  replay?: string;
83
98
  hp?: string;
84
99
  elapsedMs?: number;
100
+ consent?: ReportConsent;
101
+ includeLogs?: true;
85
102
  };
86
103
  /** Decoded byte length of a base64 data URL. */
87
104
  export declare function dataUrlBytes(dataUrl: string): number;
@@ -92,11 +109,13 @@ export declare function hasContent(input: {
92
109
  message: string;
93
110
  rating?: number;
94
111
  screenshot?: string;
112
+ barrier?: Barrier;
95
113
  }): boolean;
96
114
  /**
97
115
  * Page URL as the backend accepts it: absolute, at most 2000 chars, with no
98
116
  * fragment (OAuth implicit flow, Supabase and Firebase put tokens in
99
- * `#access_token=…`) and secret-looking query values masked (C-06).
117
+ * `#access_token=…`) and query values hidden (`…`) except the harmless
118
+ * `PAGE_PARAM` ones (`utm_*`, `page`, `lang`, `ref`, `variant`, `tab`, `sort`).
100
119
  */
101
120
  export declare function pageUrl(href: string): string;
102
121
  export declare function buildReport(input: ReportInput, ctx: ReportContext): ReportBody;
@@ -1,4 +1,4 @@
1
- import type { AiConfig, Branding, CustomField, WidgetSettings, WidgetWording } from './types';
1
+ import type { AiConfig, Branding, CustomField, KnownIssue, WidgetSettings, WidgetWording } from './types';
2
2
  /** Same defaults as `backend/src/modules/widget/widget-settings.ts`. */
3
3
  export declare const DEFAULT_SETTINGS: WidgetSettings;
4
4
  /** W-10: the backend's limits (`FIELD_LIMITS` in widget-settings.ts). */
@@ -10,10 +10,12 @@ export declare const FIELD_LIMITS: {
10
10
  readonly text: 200;
11
11
  };
12
12
  /** Settings with any field (or wording/type entry) left out. */
13
- export type SettingsInput = Partial<Omit<WidgetSettings, 'types' | 'wording' | 'capture'> & {
13
+ export type SettingsInput = Partial<Omit<WidgetSettings, 'types' | 'wording' | 'capture' | 'knownIssues' | 'consent'> & {
14
14
  types: Partial<WidgetSettings['types']>;
15
15
  wording: Partial<WidgetWording>;
16
16
  capture: Partial<WidgetSettings['capture']>;
17
+ knownIssues: Partial<NonNullable<WidgetSettings['knownIssues']>>;
18
+ consent: Partial<NonNullable<WidgetSettings['consent']>>;
17
19
  }>;
18
20
  /** W-10: well-formed fields only, so a half-made field in the dashboard can't break the form. */
19
21
  export declare function resolveFields(input: unknown): CustomField[];
@@ -22,6 +24,8 @@ export declare function resolveFields(input: unknown): CustomField[];
22
24
  * (or a half-edited dashboard form, in the preview) never breaks the widget.
23
25
  */
24
26
  export declare function resolveSettings(input?: SettingsInput | null): WidgetSettings;
27
+ /** The privacy policy link: an https address, or '' (never javascript: and the like). */
28
+ export declare function httpsUrl(v: unknown): string;
25
29
  /** W-17: only plain hex colours and an http(s) logo address get through. */
26
30
  export declare function resolveBranding(input: unknown): Branding;
27
31
  /**
@@ -29,3 +33,8 @@ export declare function resolveBranding(input: unknown): Branding;
29
33
  * a site without Pinax AI). `samplingRate` (B-17) caps both sample rates.
30
34
  */
31
35
  export declare function resolveAi(ai: unknown, samplingRate?: unknown): AiConfig;
36
+ /**
37
+ * Known issues from the config: none when missing (an older API or CDN
38
+ * copy) or switched off, and only well-formed entries otherwise.
39
+ */
40
+ export declare function resolveKnown(input: unknown, settings?: WidgetSettings['knownIssues']): KnownIssue[];
package/dist/types.d.ts CHANGED
@@ -11,13 +11,43 @@ export type PinaxOptions = {
11
11
  * come from the API.
12
12
  */
13
13
  cdnUrl?: string;
14
+ /**
15
+ * Visitor consent: `'wait'` holds replay, technical logs, error counts
16
+ * and the page-view count until `consent()` says yes; `'none'` never
17
+ * waits. Without it the site's setting (`settings.consent.required`)
18
+ * decides. `data-consent` on the script tag.
19
+ */
20
+ consent?: 'wait' | 'none';
21
+ };
22
+ /**
23
+ * Visitor consent, from the site's cookie banner: `true` or `false` for
24
+ * everything, or only the parts given. `diagnostics` covers technical logs
25
+ * on reports, fix checks, error counts (with the basket), the Segment user id
26
+ * and the page-view count; `replay` the session recorder.
27
+ */
28
+ export type PinaxConsent = boolean | {
29
+ diagnostics?: boolean;
30
+ replay?: boolean;
14
31
  };
15
- /** Signed-in visitor details attached to reports (C-09). */
32
+ /**
33
+ * Signed-in visitor details (C-09). `id` and `name` go with reports; `email`
34
+ * only fills in the form's email box (when the site allows it) and is sent
35
+ * only if the visitor leaves it there.
36
+ */
16
37
  export type PinaxUser = {
17
38
  id?: string;
18
39
  name?: string;
19
40
  email?: string;
20
41
  };
42
+ /**
43
+ * AI-21: what the visitor has in their basket right now, in major units
44
+ * (19.99, not 1999) and an ISO 4217 code. Sent only with a sampled
45
+ * sighting of an open report's error (P-17), never on its own.
46
+ */
47
+ export type PinaxBasket = {
48
+ value: number;
49
+ currency: string;
50
+ };
21
51
  export type WidgetPosition = 'bottom-right' | 'bottom-left' | 'right-edge';
22
52
  /** S-06: custom wording. An empty string means "use the default wording". */
23
53
  export type WidgetWording = {
@@ -25,6 +55,8 @@ export type WidgetWording = {
25
55
  problemTitle: string;
26
56
  ideaTitle: string;
27
57
  generalTitle: string;
58
+ /** W-19 */
59
+ accessibilityTitle: string;
28
60
  messagePlaceholder: string;
29
61
  confirmation: string;
30
62
  };
@@ -57,11 +89,12 @@ export type WidgetSettings = {
57
89
  color: string;
58
90
  text: string;
59
91
  position: WidgetPosition;
60
- /** S-02 */
92
+ /** S-02; W-19 `accessibility` is off unless the owner switches it on. */
61
93
  types: {
62
94
  problem: boolean;
63
95
  idea: boolean;
64
96
  general: boolean;
97
+ accessibility: boolean;
65
98
  };
66
99
  rating: boolean;
67
100
  /** S-04: path patterns where the button is hidden; `*` matches anything. */
@@ -81,6 +114,30 @@ export type WidgetSettings = {
81
114
  console: boolean;
82
115
  network: boolean;
83
116
  };
117
+ /**
118
+ * Known issues: show the "We know about this" card, and offer "Email me
119
+ * when it's fixed". Missing (older API) means both on, as the backend's
120
+ * defaults; with no `known` list in the config there's nothing to show.
121
+ */
122
+ knownIssues?: {
123
+ enabled: boolean;
124
+ notify: boolean;
125
+ };
126
+ /**
127
+ * Visitor consent: the site has a cookie banner, so replay, technical
128
+ * logs, error counts and the page-view count wait for `Pinax.consent()`.
129
+ * Missing (older API) means not required.
130
+ */
131
+ consent?: {
132
+ required: boolean;
133
+ };
134
+ /** The site's privacy policy (https), linked under the form; `''` = none. */
135
+ privacyUrl?: string;
136
+ /**
137
+ * Whether `identify()`'s email fills in the form's email box (the visitor
138
+ * can clear it). Missing (older API) means yes, as before.
139
+ */
140
+ identifyPrefill?: boolean;
84
141
  };
85
142
  /**
86
143
  * W-17: "Powered by Pinax" (Starter) and, with client branding (Agency),
@@ -98,6 +155,11 @@ export type FixCheck = {
98
155
  path: string;
99
156
  signature: string;
100
157
  };
158
+ /** AI-20: an open report whose error the widget counts on any page. */
159
+ export type ErrorWatch = {
160
+ id: string;
161
+ signature: string;
162
+ };
101
163
  /**
102
164
  * Pinax AI switches for the browser (`widgetAiConfig` in
103
165
  * `backend/src/modules/widget/widget.service.ts`).
@@ -110,10 +172,30 @@ export type AiConfig = {
110
172
  /** AI-15 / P-13: keep a rolling 30-second replay (Pro and Agency). */
111
173
  replay: boolean;
112
174
  replaySampleRate: number;
175
+ /**
176
+ * What the recorder hides: `'all'` (the default) every text and every
177
+ * image, video, canvas and frame; `'inputs'` form fields, editable text,
178
+ * emails and phone numbers.
179
+ */
180
+ replayMask: 'all' | 'inputs';
113
181
  /** AI-17 / P-14 */
114
182
  fixChecks: FixCheck[];
183
+ /** AI-20 / P-17: open reports' errors, counted when seen; same sample rate as fix checks. */
184
+ watches: ErrorWatch[];
115
185
  fixCheckSampleRate: number;
116
186
  };
187
+ /**
188
+ * An open report the team chose to show to visitors, with a public title
189
+ * they wrote (never the visitor's own words).
190
+ */
191
+ export type KnownIssue = {
192
+ id: string;
193
+ title: string;
194
+ /** Pages it was reported on; `/a` and `/a/` are the same page. */
195
+ paths: string[];
196
+ /** Its error signature (`errorSignature`), when it has one. */
197
+ signature: string | null;
198
+ };
117
199
  /** `data` of GET /v1/widget/:key/config. */
118
200
  export type WidgetConfig = {
119
201
  enabled: boolean;
@@ -124,8 +206,36 @@ export type WidgetConfig = {
124
206
  /** B-17: over the limit, replay and fix-check sampling is cut (to 0.01). */
125
207
  samplingRate?: number | null;
126
208
  ai?: AiConfig;
209
+ /** Known issues for this site (up to 20). Missing (older API) means none. */
210
+ known?: KnownIssue[];
211
+ };
212
+ export type ReportType = 'PROBLEM' | 'IDEA' | 'GENERAL' | 'ACCESSIBILITY';
213
+ /** W-19: what kind of barrier the visitor met (optional). */
214
+ export type Barrier = 'see' | 'keyboard' | 'screen-reader' | 'media' | 'other';
215
+ /** W-20: one failed axe-core rule. */
216
+ export type A11yIssue = {
217
+ rule: string;
218
+ impact: 'minor' | 'moderate' | 'serious' | 'critical' | null;
219
+ help: string;
220
+ helpUrl: string;
221
+ /** WCAG success criteria, e.g. "1.4.3". */
222
+ wcag: string[];
223
+ /** How many elements fail it; `nodes` holds the first few. */
224
+ count: number;
225
+ nodes: {
226
+ target: string;
227
+ html: string;
228
+ summary: string;
229
+ }[];
230
+ };
231
+ /** W-20: the automatic scan sent with an accessibility report. */
232
+ export type A11yScan = {
233
+ engine: string;
234
+ durationMs: number;
235
+ violations: A11yIssue[];
236
+ /** Rules that needed a person to check (not failures). */
237
+ incomplete: number;
127
238
  };
128
- export type ReportType = 'PROBLEM' | 'IDEA' | 'GENERAL';
129
239
  /** One drawn mark, coordinates as fractions (0 to 1) of the sent image. */
130
240
  export type Annotation = {
131
241
  type: 'box' | 'arrow' | 'text' | 'blur';
@@ -1,4 +1,4 @@
1
- import type { Annotation, FieldAnswer, PinaxUser, ReportType, WidgetConfig } from '../types';
1
+ import type { Annotation, Barrier, FieldAnswer, KnownIssue, PinaxUser, ReportType, WidgetConfig } from '../types';
2
2
  export type Shot = {
3
3
  /** The flattened image that will be sent. */
4
4
  dataUrl: string;
@@ -21,10 +21,30 @@ export type SubmitInput = {
21
21
  };
22
22
  /** W-10: answers to the site's custom fields. */
23
23
  fields?: FieldAnswer[];
24
+ /** W-19: the kind of accessibility barrier, when the visitor chose one. */
25
+ barrier?: Barrier;
24
26
  /** P-04: the hidden honeypot field (people leave it empty). */
25
27
  hp?: string;
26
28
  /** P-04: milliseconds from opening the panel to sending. */
27
29
  elapsedMs?: number;
30
+ /** Visitor consent: the visitor ticked "Include technical details". */
31
+ includeLogs?: boolean;
32
+ /** What the notice above Send said would go; nothing else is attached. */
33
+ listed?: {
34
+ logs: boolean;
35
+ replay: boolean;
36
+ };
37
+ };
38
+ /**
39
+ * Visitor consent: what goes with a report besides the note, for the
40
+ * notice above Send. `logs`: technical logs go as things stand; `askLogs`:
41
+ * they go only if the visitor ticks the box (shown then); `replay`: the
42
+ * last 30 seconds go.
43
+ */
44
+ export type Attachments = {
45
+ logs: boolean;
46
+ askLogs: boolean;
47
+ replay: boolean;
28
48
  };
29
49
  /** AI-10: only short notes on problems and ideas get a follow-up question. */
30
50
  export declare const FOLLOW_UP_MAX_NOTE = 140;
@@ -51,9 +71,20 @@ export type UIHandlers = {
51
71
  type: ReportType;
52
72
  message: string;
53
73
  }): Promise<string | null>;
74
+ /** Known issues that match this page view, asked each time the panel opens. */
75
+ knownIssues?(): KnownIssue[];
76
+ /** Known issues: "Me too", with an email when given. False on any error. */
77
+ meToo?(id: string, email?: string): Promise<boolean>;
78
+ /**
79
+ * Visitor consent: what a report would carry now (see `Attachments`).
80
+ * Without it: technical logs, no box, no recording.
81
+ */
82
+ attachments?(): Attachments;
83
+ /** Who the report goes to, in the notice. Default: this page's host name. */
84
+ site?(): string;
54
85
  };
55
86
  /** A screen of the panel, or 'button' for the closed widget. */
56
- export type WidgetView = 'button' | 'choose' | 'problem' | 'idea' | 'general' | 'done';
87
+ export type WidgetView = 'button' | 'choose' | 'problem' | 'idea' | 'general' | 'accessibility' | 'done';
57
88
  /**
58
89
  * The floating button and the feedback panel (W-01, W-02, W-07 to W-11,
59
90
  * W-15). Everything renders inside the shadow root.
@@ -71,7 +102,11 @@ export declare class WidgetUI {
71
102
  private shot;
72
103
  private message;
73
104
  private email;
105
+ /** The visitor typed in, changed or cleared an email box: never refill it. */
106
+ private emailEdited;
74
107
  private rating;
108
+ /** W-19 */
109
+ private barrier;
75
110
  /** W-10: answers by field id. */
76
111
  private answers;
77
112
  /** W-17: the client's colour on Send buttons (Agency). */
@@ -86,6 +121,16 @@ export declare class WidgetUI {
86
121
  private pathHidden;
87
122
  /** Where focus goes back to on close when there's no button to return to. */
88
123
  private returnFocus;
124
+ /** Known issues matched when the panel last opened, and those the visitor moved past. */
125
+ private known;
126
+ private knownHidden;
127
+ private meToo;
128
+ /** Visitor consent: the "Include technical details" box, unticked to start. */
129
+ private includeLogs;
130
+ /** The notice above Send on show, and what it said would go. */
131
+ private privacy;
132
+ private noticeText;
133
+ private shown;
89
134
  constructor(root: ShadowRoot, config: WidgetConfig, handlers: UIHandlers,
90
135
  /** Extra classes on the wrapper and extra CSS (the dashboard preview). */
91
136
  extra?: {
@@ -136,9 +181,44 @@ export declare class WidgetUI {
136
181
  private logo;
137
182
  private render;
138
183
  private renderChoose;
184
+ /** Known issues: the matches on show, without those the visitor moved past. */
185
+ private knownShown;
186
+ /** Asks which known issues match now; with any, the panel starts on the card. */
187
+ private offerKnown;
188
+ /**
189
+ * Known issues: "We know about this", each issue's public title and a
190
+ * Me too button; after it, thanks and (when the site allows it) "Email me
191
+ * when it's fixed". "Report something else" leaves the card.
192
+ */
193
+ private renderKnown;
194
+ /** Me too, or (with `emailEl`) the email to hear when it's fixed. */
195
+ private sendMeToo;
139
196
  private renderShot;
140
197
  private renderStars;
141
198
  private renderForm;
199
+ /** The optional email, the honeypot, the error line and Send. */
200
+ private renderEmailAndSend;
201
+ /** Visitor consent: what a report would carry now. */
202
+ private attachments;
203
+ /**
204
+ * Above Send: the "Include technical details" box (only while the page
205
+ * waits for consent and the visitor hasn't given it), then what goes
206
+ * with the note, and the site's privacy policy.
207
+ */
208
+ private renderPrivacy;
209
+ /** "Sent to … with your note: …", listing only what will go. */
210
+ private updateNotice;
211
+ /**
212
+ * Consent changed, or the recorder started: the notice (and the box)
213
+ * above Send follow, in place, so typing and focus aren't disturbed.
214
+ */
215
+ refresh(): void;
216
+ /**
217
+ * W-19: a short form that works well with a screen reader and the
218
+ * keyboard alone: native radio buttons, real labels, no screenshot unless
219
+ * the visitor adds one, and a note saying who reads it.
220
+ */
221
+ private renderAccessibility;
142
222
  /**
143
223
  * P-04: a field people never see or reach (off-screen, out of the tab
144
224
  * order and hidden from screen readers). Form-filling bots fill it in.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpinax/widget",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "type": "module",
5
5
  "description": "Pinax website feedback widget: a feedback button with screenshots, markup and page context",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "dev": "vite",
45
- "build": "tsc --noEmit && vite build && vite build --mode editor && vite build --mode recorder && vite build --mode embed && tsc -p tsconfig.build.json && node scripts/check-size.mjs",
45
+ "build": "tsc --noEmit && vite build && vite build --mode editor && vite build --mode recorder && vite build --mode axe && vite build --mode embed && tsc -p tsconfig.build.json && node scripts/check-size.mjs",
46
46
  "test": "vitest run",
47
47
  "typecheck": "tsc --noEmit",
48
48
  "check": "biome check ./src ./cdn",
@@ -50,12 +50,12 @@
50
50
  "size": "node scripts/check-size.mjs",
51
51
  "playground": "yarn build && node scripts/serve-playground.mjs",
52
52
  "prepublishOnly": "VITE_PINAX_API=https://api.getpinax.com VITE_PINAX_CDN=https://cdn.getpinax.com yarn build && yarn test",
53
- "cdn:deploy": "./cdn/deploy.sh"
53
+ "cdn:deploy": "./cdn/deploy.sh",
54
+ "release": "bash scripts/release.sh"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@biomejs/biome": "^2.6.5",
57
58
  "@types/node": "^22",
58
- "axe-core": "^4.10.0",
59
59
  "jsdom": "^30.1.3",
60
60
  "typescript": "^7.0.2",
61
61
  "vite": "^8.3.5",
@@ -63,6 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@rrweb/record": "2.1.7",
66
+ "axe-core": "^4.10.0",
66
67
  "modern-screenshot": "^4.7.0"
67
68
  },
68
69
  "resolutions": {
@@ -1,18 +0,0 @@
1
- // src/capture/text.ts
2
- function e(e, t) {
3
- return e.length > t ? `${e.slice(0, t - 1)}…` : e;
4
- }
5
- var t = /^(access_?token|token|id_token|refresh_token|auth|authorization|api_?key|key|secret|password|passwd|pwd|session|sessionid|sid|code|signature|sig)$/i;
6
- function n(e, n) {
7
- try {
8
- let r = new URL(e, n);
9
- r.hash = "", r.username = "", r.password = "";
10
- for (let e of [...r.searchParams.keys()]) t.test(e) && r.searchParams.set(e, "[hidden]");
11
- return r.toString();
12
- } catch {
13
- return String(e);
14
- }
15
- }
16
- export { n, e as t };
17
-
18
- //# sourceMappingURL=text-8QYEyRYs.mjs.map