@getpinax/widget 0.0.0-stage → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +267 -2
- package/dist/api.d.ts +62 -0
- package/dist/capture/breadcrumbs.d.ts +49 -0
- package/dist/capture/console.d.ts +17 -0
- package/dist/capture/env.d.ts +25 -0
- package/dist/capture/network.d.ts +23 -0
- package/dist/capture/ring.d.ts +14 -0
- package/dist/capture/signature.d.ts +2 -0
- package/dist/capture/text.d.ts +7 -0
- package/dist/capture/ua.d.ts +13 -0
- package/dist/chunks/editor-DbHLGwOA.mjs +1368 -0
- package/dist/chunks/privacy-DmU6v5Fr.mjs +39 -0
- package/dist/chunks/recorder-BUtEB_em.mjs +2859 -0
- package/dist/chunks/text-8QYEyRYs.mjs +18 -0
- package/dist/chunks/widget-CkR1Ptzb.mjs +961 -0
- package/dist/editor/annotator.d.ts +20 -0
- package/dist/editor/capture.d.ts +28 -0
- package/dist/editor/encode.d.ts +7 -0
- package/dist/editor/index.d.ts +5 -0
- package/dist/editor/mask.d.ts +29 -0
- package/dist/editor/pii.d.ts +22 -0
- package/dist/editor/shapes.d.ts +31 -0
- package/dist/editor-loader.d.ts +6 -0
- package/dist/editor-loader.embed.d.ts +5 -0
- package/dist/fixcheck.d.ts +31 -0
- package/dist/index.d.ts +46 -0
- package/dist/mount.d.ts +8 -0
- package/dist/navigation.d.ts +1 -0
- package/dist/paths.d.ts +4 -0
- package/dist/pinax-editor.js +1417 -0
- package/dist/pinax-recorder.js +2902 -0
- package/dist/pinax.js +113 -0
- package/dist/pinax.mjs +1041 -0
- package/dist/preview.d.ts +35 -0
- package/dist/preview.mjs +54 -0
- package/dist/privacy.d.ts +22 -0
- package/dist/queue.d.ts +9 -0
- package/dist/replay/buffer.d.ts +40 -0
- package/dist/replay/recorder.d.ts +15 -0
- package/dist/replay/sanitize.d.ts +13 -0
- package/dist/report.d.ts +109 -0
- package/dist/segment.d.ts +31 -0
- package/dist/settings.d.ts +31 -0
- package/dist/types.d.ts +137 -0
- package/dist/ui/color.d.ts +7 -0
- package/dist/ui/dom.d.ts +14 -0
- package/dist/ui/styles.d.ts +3 -0
- package/dist/ui/widget.d.ts +161 -0
- package/package.json +73 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type SettingsInput } from './settings';
|
|
2
|
+
import { type WidgetView } from './ui/widget';
|
|
3
|
+
export type { SettingsInput } from './settings';
|
|
4
|
+
export type { CustomField, WidgetSettings, WidgetWording, } from './types';
|
|
5
|
+
export type { WidgetView } from './ui/widget';
|
|
6
|
+
export type PreviewOptions = {
|
|
7
|
+
/** Screen to show. Default 'button' (closed). */
|
|
8
|
+
view?: WidgetView;
|
|
9
|
+
/** Phone layout (bottom sheet, `smallOnMobile`) whatever the page width. */
|
|
10
|
+
phone?: boolean;
|
|
11
|
+
/** Show "Powered by Pinax". Default true. */
|
|
12
|
+
poweredBy?: boolean;
|
|
13
|
+
/** W-17: the client's logo and colours (Agency). */
|
|
14
|
+
branding?: {
|
|
15
|
+
logoUrl?: string | null;
|
|
16
|
+
primary?: string | null;
|
|
17
|
+
accent?: string | null;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type PreviewHandle = {
|
|
21
|
+
/** Re-render with new settings, staying on the screen on show. */
|
|
22
|
+
update(settings: SettingsInput, options?: PreviewOptions): void;
|
|
23
|
+
/** Jump to a screen ('button' closes the panel). */
|
|
24
|
+
show(view: WidgetView): void;
|
|
25
|
+
/** The screen on show (changes when the viewer clicks around). */
|
|
26
|
+
view(): WidgetView;
|
|
27
|
+
/** Removes the preview's UI from the container. */
|
|
28
|
+
destroy(): void;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Renders the widget into `container`'s shadow root with `settings`.
|
|
32
|
+
* Give the container a size (e.g. 100% x 520px); the button and panel are
|
|
33
|
+
* positioned inside it exactly as they would be in the visitor's window.
|
|
34
|
+
*/
|
|
35
|
+
export declare function preview(container: HTMLElement, settings: SettingsInput, options?: PreviewOptions): PreviewHandle;
|
package/dist/preview.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { a as e, n as t, o as n, t as r } from "./chunks/widget-CkR1Ptzb.mjs";
|
|
2
|
+
// src/preview.ts
|
|
3
|
+
var i = `
|
|
4
|
+
:host { display: block; position: relative; overflow: hidden; }
|
|
5
|
+
.preview .launcher, .preview .panel { position: absolute; }
|
|
6
|
+
.preview .panel { max-height: calc(100% - 108px); }
|
|
7
|
+
.preview.pos-right-edge .panel, .preview.no-button .panel { max-height: calc(100% - 48px); }
|
|
8
|
+
${t("", ".pnx.phone")}
|
|
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, {
|
|
14
|
+
enabled: !0,
|
|
15
|
+
settings: n(t),
|
|
16
|
+
branding: e({
|
|
17
|
+
...l.branding,
|
|
18
|
+
poweredBy: l.poweredBy !== !1
|
|
19
|
+
})
|
|
20
|
+
}, {
|
|
21
|
+
takeScreenshot: async () => ({
|
|
22
|
+
dataUrl: a,
|
|
23
|
+
annotations: []
|
|
24
|
+
}),
|
|
25
|
+
markUp: async () => null,
|
|
26
|
+
submit: async () => ({ status: "sent" }),
|
|
27
|
+
user: () => void 0
|
|
28
|
+
}, {
|
|
29
|
+
className: l.phone ? "preview phone" : "preview",
|
|
30
|
+
css: i
|
|
31
|
+
}), o !== "button" && u.showView(o);
|
|
32
|
+
};
|
|
33
|
+
return d(o, l.view ?? "button"), {
|
|
34
|
+
update(e, t) {
|
|
35
|
+
let n = u?.currentView() ?? "button";
|
|
36
|
+
t && (l = {
|
|
37
|
+
...l,
|
|
38
|
+
...t
|
|
39
|
+
}), d(e, t?.view ?? n);
|
|
40
|
+
},
|
|
41
|
+
show(e) {
|
|
42
|
+
u?.showView(e);
|
|
43
|
+
},
|
|
44
|
+
view() {
|
|
45
|
+
return u?.currentView() ?? "button";
|
|
46
|
+
},
|
|
47
|
+
destroy() {
|
|
48
|
+
u?.destroy(), u = null, c.replaceChildren();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export { o as preview };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=preview.mjs.map
|
|
@@ -0,0 +1,22 @@
|
|
|
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.
|
|
4
|
+
*/
|
|
5
|
+
export type Span = {
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
kind: 'email' | 'phone';
|
|
9
|
+
};
|
|
10
|
+
export declare function isPhone(raw: string): boolean;
|
|
11
|
+
/** Emails and phone numbers in `text`, in order, never overlapping. */
|
|
12
|
+
export declare function findPersonalSpans(text: string): Span[];
|
|
13
|
+
/** Replaces emails and phone numbers with asterisks (same length). */
|
|
14
|
+
export declare function redactPersonal(text: string): string;
|
|
15
|
+
/** A text that is only a label ("Name", "Customer:"); the name follows it. */
|
|
16
|
+
export declare const NAME_LABEL_ONLY: RegExp;
|
|
17
|
+
/** "Name: Ada Lovelace" in one text. */
|
|
18
|
+
export declare const NAME_LABEL_INLINE: RegExp;
|
|
19
|
+
/** "Signed in as Ada", "Welcome back, Ada" anywhere in a text. */
|
|
20
|
+
export declare const SIGNED_IN: RegExp;
|
|
21
|
+
/** Text short enough, and wordy enough, to be a name or username after a label. */
|
|
22
|
+
export declare function looksLikeNameValue(text: string): boolean;
|
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Fetch } from './api';
|
|
2
|
+
import { type ReportBody } from './report';
|
|
3
|
+
/** What may be kept in localStorage: the report without the private extras. */
|
|
4
|
+
export declare function queuedCopy(body: ReportBody): ReportBody;
|
|
5
|
+
/** Saves a slimmed copy of a report for later (see above). */
|
|
6
|
+
export declare function enqueue(key: string, body: ReportBody): boolean;
|
|
7
|
+
export declare function queuedCount(key: string): number;
|
|
8
|
+
/** Sends what's waiting. Items the server refuses for good are dropped. */
|
|
9
|
+
export declare function flush(doFetch: Fetch, api: string, key: string): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-15: a rolling window of rrweb events. Events are kept in segments that
|
|
3
|
+
* each start with a full snapshot (rrweb's Meta event, then FullSnapshot,
|
|
4
|
+
* from `checkoutEveryNms`), so whatever is kept can be replayed from its
|
|
5
|
+
* first event. A segment is dropped once the next one already starts at or
|
|
6
|
+
* before the window's edge, so the buffer always covers the last
|
|
7
|
+
* `windowMs` (or the whole visit, if shorter) and at most one checkout
|
|
8
|
+
* interval more.
|
|
9
|
+
*/
|
|
10
|
+
export type ReplayEvent = {
|
|
11
|
+
type: number;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
};
|
|
14
|
+
/** rrweb's EventType.Meta: the first event of every full snapshot. */
|
|
15
|
+
export declare const META = 4;
|
|
16
|
+
export declare class ReplayBuffer<E extends ReplayEvent = ReplayEvent> {
|
|
17
|
+
readonly windowMs: number;
|
|
18
|
+
private now;
|
|
19
|
+
/** Hard cap on events held, whatever the time window (a very busy page). */
|
|
20
|
+
readonly maxEvents: number;
|
|
21
|
+
private segments;
|
|
22
|
+
constructor(windowMs?: number, now?: () => number,
|
|
23
|
+
/** Hard cap on events held, whatever the time window (a very busy page). */
|
|
24
|
+
maxEvents?: number);
|
|
25
|
+
push(event: E): void;
|
|
26
|
+
/** Drops whole segments that are no longer needed to cover the window. */
|
|
27
|
+
trim(): void;
|
|
28
|
+
private count;
|
|
29
|
+
/**
|
|
30
|
+
* The events to send, oldest first. `lastSegments` keeps only the newest
|
|
31
|
+
* segments, for when the full window is too big to upload.
|
|
32
|
+
*/
|
|
33
|
+
events(lastSegments?: number): E[];
|
|
34
|
+
get segmentCount(): number;
|
|
35
|
+
clear(): void;
|
|
36
|
+
}
|
|
37
|
+
/** base64 of bytes, in chunks so big replays don't overflow the call stack. */
|
|
38
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
39
|
+
/** gzip with the browser's CompressionStream; null where it isn't supported. */
|
|
40
|
+
export declare function gzip(text: string): Promise<Uint8Array | null>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const WINDOW_MS = 30000;
|
|
2
|
+
/** A full snapshot this often, so the kept window can start mid-visit. */
|
|
3
|
+
export declare const CHECKOUT_MS = 10000;
|
|
4
|
+
export type Recorder = {
|
|
5
|
+
/**
|
|
6
|
+
* The buffer as base64 of gzipped JSON, at most `maxChars` long (fewer
|
|
7
|
+
* seconds if the whole window doesn't fit), or null when it can't be made
|
|
8
|
+
* (no CompressionStream, nothing recorded, too big).
|
|
9
|
+
*/
|
|
10
|
+
exportReplay(maxChars: number): Promise<string | null>;
|
|
11
|
+
stop(): void;
|
|
12
|
+
};
|
|
13
|
+
export declare function startRecorder(options?: {
|
|
14
|
+
exclude?: string;
|
|
15
|
+
}): Recorder;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReplayEvent } from './buffer';
|
|
2
|
+
/** A URL without its fragment or secret query values, and no email or phone. */
|
|
3
|
+
export declare function cleanUrl(value: string, base?: string): string;
|
|
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;
|
|
7
|
+
/**
|
|
8
|
+
* The events as JSON, with every attribute cleaned: full snapshot nodes
|
|
9
|
+
* (`{ tagName, attributes }`), mutation adds (same shape) and attribute
|
|
10
|
+
* changes (`{ id, attributes }`). Meta events get a cleaned `href`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function serializeReplay(events: readonly ReplayEvent[], base?: string | undefined): string;
|
|
13
|
+
export {};
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Breadcrumb } from './capture/breadcrumbs';
|
|
2
|
+
import type { ConsoleEntry } from './capture/console';
|
|
3
|
+
import type { Environment } from './capture/env';
|
|
4
|
+
import type { NetworkEntry } from './capture/network';
|
|
5
|
+
import type { Annotation, FieldAnswer, PinaxUser, ReportType } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Builds the POST /reports body. Mirrors the limits in
|
|
8
|
+
* backend/src/modules/widget/widget.validator.ts so a report is never rejected
|
|
9
|
+
* for something the widget could have trimmed.
|
|
10
|
+
*/
|
|
11
|
+
export declare const LIMITS: {
|
|
12
|
+
readonly message: 5000;
|
|
13
|
+
readonly email: 320;
|
|
14
|
+
readonly entries: 50;
|
|
15
|
+
readonly annotations: 200;
|
|
16
|
+
/** The backend caps the data URL at 6,000,000 chars and the decoded image at 4.5 MB. */
|
|
17
|
+
readonly screenshotChars: 6000000;
|
|
18
|
+
readonly screenshotBytes: number;
|
|
19
|
+
readonly breadcrumbs: 20;
|
|
20
|
+
readonly followUpQuestion: 300;
|
|
21
|
+
readonly followUpAnswer: 2000;
|
|
22
|
+
/** AI-15: base64 of the gzipped replay. */
|
|
23
|
+
readonly replayChars: 4500000;
|
|
24
|
+
/** W-10: answers per report, and the longest text answer. */
|
|
25
|
+
readonly fields: 10;
|
|
26
|
+
readonly fieldId: 40;
|
|
27
|
+
readonly fieldText: 200;
|
|
28
|
+
/** The reports route parses at most 11 MB (`express.json({ limit: '11mb' })`). */
|
|
29
|
+
readonly bodyBytes: number;
|
|
30
|
+
};
|
|
31
|
+
export declare function isEmail(value: string): boolean;
|
|
32
|
+
export type ReportInput = {
|
|
33
|
+
type: ReportType;
|
|
34
|
+
message: string;
|
|
35
|
+
rating?: number;
|
|
36
|
+
email?: string;
|
|
37
|
+
user?: PinaxUser;
|
|
38
|
+
screenshot?: string;
|
|
39
|
+
annotations?: Annotation[];
|
|
40
|
+
/** AI-10 */
|
|
41
|
+
followUp?: {
|
|
42
|
+
question: string;
|
|
43
|
+
answer: string;
|
|
44
|
+
};
|
|
45
|
+
/** W-10 */
|
|
46
|
+
fields?: FieldAnswer[];
|
|
47
|
+
/** P-04: honeypot value and time to fill in the form. */
|
|
48
|
+
hp?: string;
|
|
49
|
+
elapsedMs?: number;
|
|
50
|
+
};
|
|
51
|
+
export type ReportContext = {
|
|
52
|
+
page: {
|
|
53
|
+
url: string;
|
|
54
|
+
title?: string;
|
|
55
|
+
};
|
|
56
|
+
environment: Environment;
|
|
57
|
+
console: ConsoleEntry[];
|
|
58
|
+
network: NetworkEntry[];
|
|
59
|
+
breadcrumbs?: Breadcrumb[];
|
|
60
|
+
};
|
|
61
|
+
export type ReportBody = {
|
|
62
|
+
type: ReportType;
|
|
63
|
+
message: string;
|
|
64
|
+
rating?: number;
|
|
65
|
+
email?: string;
|
|
66
|
+
user?: PinaxUser;
|
|
67
|
+
page: {
|
|
68
|
+
url: string;
|
|
69
|
+
title?: string;
|
|
70
|
+
};
|
|
71
|
+
environment: Environment;
|
|
72
|
+
console?: ConsoleEntry[];
|
|
73
|
+
network?: NetworkEntry[];
|
|
74
|
+
screenshot?: string;
|
|
75
|
+
annotations?: Array<Record<string, unknown>>;
|
|
76
|
+
breadcrumbs?: Breadcrumb[];
|
|
77
|
+
followUp?: {
|
|
78
|
+
question: string;
|
|
79
|
+
answer: string;
|
|
80
|
+
};
|
|
81
|
+
fields?: FieldAnswer[];
|
|
82
|
+
replay?: string;
|
|
83
|
+
hp?: string;
|
|
84
|
+
elapsedMs?: number;
|
|
85
|
+
};
|
|
86
|
+
/** Decoded byte length of a base64 data URL. */
|
|
87
|
+
export declare function dataUrlBytes(dataUrl: string): number;
|
|
88
|
+
export declare function isSendableScreenshot(dataUrl: string): boolean;
|
|
89
|
+
/** The backend refuses a report with no note, no rating and no screenshot. */
|
|
90
|
+
export declare function hasContent(input: {
|
|
91
|
+
type: ReportType;
|
|
92
|
+
message: string;
|
|
93
|
+
rating?: number;
|
|
94
|
+
screenshot?: string;
|
|
95
|
+
}): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Page URL as the backend accepts it: absolute, at most 2000 chars, with no
|
|
98
|
+
* fragment (OAuth implicit flow, Supabase and Firebase put tokens in
|
|
99
|
+
* `#access_token=…`) and secret-looking query values masked (C-06).
|
|
100
|
+
*/
|
|
101
|
+
export declare function pageUrl(href: string): string;
|
|
102
|
+
export declare function buildReport(input: ReportInput, ctx: ReportContext): ReportBody;
|
|
103
|
+
/** UTF-8 size of the JSON body. */
|
|
104
|
+
export declare function bodyBytes(body: ReportBody): number;
|
|
105
|
+
/**
|
|
106
|
+
* AI-15: adds the replay only when it fits the backend's limits (4.5 M
|
|
107
|
+
* base64 characters, the whole request under 11 MB). Returns whether it did.
|
|
108
|
+
*/
|
|
109
|
+
export declare function attachReplay(body: ReportBody, replay: string | null): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReportType } from './types';
|
|
2
|
+
type SegmentUser = {
|
|
3
|
+
id?: () => unknown;
|
|
4
|
+
};
|
|
5
|
+
/** The parts of `window.analytics` we use. The snippet stub has only `track`. */
|
|
6
|
+
export type SegmentAnalytics = {
|
|
7
|
+
track?: (event: string, properties?: Record<string, unknown>) => unknown;
|
|
8
|
+
user?: () => SegmentUser | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare const SEGMENT_EVENTS: {
|
|
11
|
+
readonly opened: 'Pinax Widget Opened';
|
|
12
|
+
readonly submitted: 'Feedback Submitted';
|
|
13
|
+
};
|
|
14
|
+
/** `window.analytics`, if it looks like Segment's. */
|
|
15
|
+
export declare function segmentAnalytics(win?: Window): SegmentAnalytics | null;
|
|
16
|
+
/** The init option wins over the site's setting. */
|
|
17
|
+
export declare function segmentEnabled(option: boolean | undefined, setting: boolean | undefined): boolean;
|
|
18
|
+
export declare function trackOpened(): void;
|
|
19
|
+
export declare function trackSubmitted(input: {
|
|
20
|
+
type: ReportType;
|
|
21
|
+
rating?: number | null;
|
|
22
|
+
reportId?: string;
|
|
23
|
+
queued?: boolean;
|
|
24
|
+
}): void;
|
|
25
|
+
/**
|
|
26
|
+
* Segment's signed-in user id (`analytics.user().id()`), used when the site
|
|
27
|
+
* didn't call `Pinax.identify()`. Null before analytics.js has loaded or for
|
|
28
|
+
* anonymous visitors.
|
|
29
|
+
*/
|
|
30
|
+
export declare function segmentUserId(): string | null;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AiConfig, Branding, CustomField, WidgetSettings, WidgetWording } from './types';
|
|
2
|
+
/** Same defaults as `backend/src/modules/widget/widget-settings.ts`. */
|
|
3
|
+
export declare const DEFAULT_SETTINGS: WidgetSettings;
|
|
4
|
+
/** W-10: the backend's limits (`FIELD_LIMITS` in widget-settings.ts). */
|
|
5
|
+
export declare const FIELD_LIMITS: {
|
|
6
|
+
readonly fields: 10;
|
|
7
|
+
readonly label: 80;
|
|
8
|
+
readonly options: 20;
|
|
9
|
+
readonly option: 80;
|
|
10
|
+
readonly text: 200;
|
|
11
|
+
};
|
|
12
|
+
/** Settings with any field (or wording/type entry) left out. */
|
|
13
|
+
export type SettingsInput = Partial<Omit<WidgetSettings, 'types' | 'wording' | 'capture'> & {
|
|
14
|
+
types: Partial<WidgetSettings['types']>;
|
|
15
|
+
wording: Partial<WidgetWording>;
|
|
16
|
+
capture: Partial<WidgetSettings['capture']>;
|
|
17
|
+
}>;
|
|
18
|
+
/** W-10: well-formed fields only, so a half-made field in the dashboard can't break the form. */
|
|
19
|
+
export declare function resolveFields(input: unknown): CustomField[];
|
|
20
|
+
/**
|
|
21
|
+
* Fills in anything missing or malformed with the defaults, so an older API
|
|
22
|
+
* (or a half-edited dashboard form, in the preview) never breaks the widget.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveSettings(input?: SettingsInput | null): WidgetSettings;
|
|
25
|
+
/** W-17: only plain hex colours and an http(s) logo address get through. */
|
|
26
|
+
export declare function resolveBranding(input: unknown): Branding;
|
|
27
|
+
/**
|
|
28
|
+
* The AI part of the config, everything off when missing (an older API, or
|
|
29
|
+
* a site without Pinax AI). `samplingRate` (B-17) caps both sample rates.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveAi(ai: unknown, samplingRate?: unknown): AiConfig;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export type PinaxOptions = {
|
|
2
|
+
/** The website's public key (S-08). */
|
|
3
|
+
key: string;
|
|
4
|
+
/** Override the API origin, e.g. for local development. */
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
/** Segment: force sending events to `window.analytics` on or off, whatever the site's setting. */
|
|
7
|
+
segment?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Override the CDN origin that serves the site's settings (P-15), default
|
|
10
|
+
* https://cdn.getpinax.com. When `apiUrl` is set and this isn't, settings
|
|
11
|
+
* come from the API.
|
|
12
|
+
*/
|
|
13
|
+
cdnUrl?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Signed-in visitor details attached to reports (C-09). */
|
|
16
|
+
export type PinaxUser = {
|
|
17
|
+
id?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
};
|
|
21
|
+
export type WidgetPosition = 'bottom-right' | 'bottom-left' | 'right-edge';
|
|
22
|
+
/** S-06: custom wording. An empty string means "use the default wording". */
|
|
23
|
+
export type WidgetWording = {
|
|
24
|
+
chooserTitle: string;
|
|
25
|
+
problemTitle: string;
|
|
26
|
+
ideaTitle: string;
|
|
27
|
+
generalTitle: string;
|
|
28
|
+
messagePlaceholder: string;
|
|
29
|
+
confirmation: string;
|
|
30
|
+
};
|
|
31
|
+
/** W-10: an owner-defined field in the form. */
|
|
32
|
+
export type CustomField = {
|
|
33
|
+
id: string;
|
|
34
|
+
type: 'select' | 'text' | 'checkbox';
|
|
35
|
+
label: string;
|
|
36
|
+
required: boolean;
|
|
37
|
+
/** Dropdown choices. */
|
|
38
|
+
options: string[];
|
|
39
|
+
/** Which kinds of feedback show it. */
|
|
40
|
+
types: {
|
|
41
|
+
problem: boolean;
|
|
42
|
+
idea: boolean;
|
|
43
|
+
general: boolean;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** W-10: a visitor's answer, as sent in the report body. */
|
|
47
|
+
export type FieldAnswer = {
|
|
48
|
+
id: string;
|
|
49
|
+
value: string | boolean;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The website's widget settings, as defined by
|
|
53
|
+
* `backend/src/modules/widget/widget-settings.ts`.
|
|
54
|
+
*/
|
|
55
|
+
export type WidgetSettings = {
|
|
56
|
+
/** S-01 */
|
|
57
|
+
color: string;
|
|
58
|
+
text: string;
|
|
59
|
+
position: WidgetPosition;
|
|
60
|
+
/** S-02 */
|
|
61
|
+
types: {
|
|
62
|
+
problem: boolean;
|
|
63
|
+
idea: boolean;
|
|
64
|
+
general: boolean;
|
|
65
|
+
};
|
|
66
|
+
rating: boolean;
|
|
67
|
+
/** S-04: path patterns where the button is hidden; `*` matches anything. */
|
|
68
|
+
hidePaths: string[];
|
|
69
|
+
/** S-04: a smaller button on phones. */
|
|
70
|
+
smallOnMobile: boolean;
|
|
71
|
+
/** S-05: no floating button; the site opens the widget with `Pinax.open()`. */
|
|
72
|
+
hideButton: boolean;
|
|
73
|
+
/** Segment: send widget events to the page's own `window.analytics`. */
|
|
74
|
+
segment: boolean;
|
|
75
|
+
/** S-06 */
|
|
76
|
+
wording: WidgetWording;
|
|
77
|
+
/** W-10 */
|
|
78
|
+
fields: CustomField[];
|
|
79
|
+
/** C-10: what is recorded with each report. */
|
|
80
|
+
capture: {
|
|
81
|
+
console: boolean;
|
|
82
|
+
network: boolean;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* W-17: "Powered by Pinax" (Starter) and, with client branding (Agency),
|
|
87
|
+
* the client's logo and colours. Missing or null = Pinax's own look.
|
|
88
|
+
*/
|
|
89
|
+
export type Branding = {
|
|
90
|
+
poweredBy: boolean;
|
|
91
|
+
logoUrl?: string | null;
|
|
92
|
+
primary?: string | null;
|
|
93
|
+
accent?: string | null;
|
|
94
|
+
};
|
|
95
|
+
/** AI-17: a report marked Fixed whose error the widget watches for. */
|
|
96
|
+
export type FixCheck = {
|
|
97
|
+
id: string;
|
|
98
|
+
path: string;
|
|
99
|
+
signature: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Pinax AI switches for the browser (`widgetAiConfig` in
|
|
103
|
+
* `backend/src/modules/widget/widget.service.ts`).
|
|
104
|
+
*/
|
|
105
|
+
export type AiConfig = {
|
|
106
|
+
/** AI-10: ask one follow-up question about a short note. */
|
|
107
|
+
followUp: boolean;
|
|
108
|
+
/** AI-12: blur emails, phone numbers and names in screenshots. */
|
|
109
|
+
autoBlur: boolean;
|
|
110
|
+
/** AI-15 / P-13: keep a rolling 30-second replay (Pro and Agency). */
|
|
111
|
+
replay: boolean;
|
|
112
|
+
replaySampleRate: number;
|
|
113
|
+
/** AI-17 / P-14 */
|
|
114
|
+
fixChecks: FixCheck[];
|
|
115
|
+
fixCheckSampleRate: number;
|
|
116
|
+
};
|
|
117
|
+
/** `data` of GET /v1/widget/:key/config. */
|
|
118
|
+
export type WidgetConfig = {
|
|
119
|
+
enabled: boolean;
|
|
120
|
+
/** S-15: hostnames that may send reports. Missing (older API) means don't check. */
|
|
121
|
+
allowedDomains?: string[];
|
|
122
|
+
settings: WidgetSettings;
|
|
123
|
+
branding: Branding;
|
|
124
|
+
/** B-17: over the limit, replay and fix-check sampling is cut (to 0.01). */
|
|
125
|
+
samplingRate?: number | null;
|
|
126
|
+
ai?: AiConfig;
|
|
127
|
+
};
|
|
128
|
+
export type ReportType = 'PROBLEM' | 'IDEA' | 'GENERAL';
|
|
129
|
+
/** One drawn mark, coordinates as fractions (0 to 1) of the sent image. */
|
|
130
|
+
export type Annotation = {
|
|
131
|
+
type: 'box' | 'arrow' | 'text' | 'blur';
|
|
132
|
+
x: number;
|
|
133
|
+
y: number;
|
|
134
|
+
w: number;
|
|
135
|
+
h: number;
|
|
136
|
+
text?: string;
|
|
137
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Relative luminance of a #rgb or #rrggbb colour (WCAG 2). */
|
|
2
|
+
export declare function luminance(hex: string): number | null;
|
|
3
|
+
export declare function contrast(a: number, b: number): number;
|
|
4
|
+
/** The board's dark ink (#0B1300) or white, whichever reads better on `bg`. */
|
|
5
|
+
export declare function inkFor(bg: string): string;
|
|
6
|
+
/** Only accept plain hex colours from config, so nothing odd reaches CSS. */
|
|
7
|
+
export declare function safeColor(value: unknown, fallback?: string): string;
|
package/dist/ui/dom.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Attrs = Record<string, string | number | boolean | undefined | null>;
|
|
2
|
+
type Child = Node | string | null | undefined | false;
|
|
3
|
+
/** Tiny element builder. Text is always set as text, never parsed as HTML. */
|
|
4
|
+
export declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs?: Attrs, ...children: Child[]): HTMLElementTagNameMap[K];
|
|
5
|
+
/** A 16px stroked icon from a path. */
|
|
6
|
+
export declare function icon(d: string, size?: number): SVGSVGElement;
|
|
7
|
+
export declare const ICONS: {
|
|
8
|
+
close: string;
|
|
9
|
+
back: string;
|
|
10
|
+
pen: string;
|
|
11
|
+
camera: string;
|
|
12
|
+
trash: string;
|
|
13
|
+
};
|
|
14
|
+
export {};
|