@paikko/widget 0.1.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/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @paikko/widget
2
+
3
+ The consumer-installable paikko report widget. Mount it once in your app and
4
+ users get a floating report button that captures the DOM, storage, client state,
5
+ and provenance of what they were looking at, then POSTs a contract-valid
6
+ `ReportBundle` to your paikko backend.
7
+
8
+ The widget is backend-agnostic and store-agnostic: you configure where reports
9
+ go (`endpoint`), which tenant they belong to (`projectKey`), where the review
10
+ queue lives (`ticketsUrl`), and how to read your app's state (`getClientState`).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @paikko/widget @paikko/contract react
16
+ ```
17
+
18
+ `react` is a peer dependency (>= 18.3.1). `@paikko/contract` carries the shared
19
+ wire types/zod schemas and is required.
20
+
21
+ > The widget ships raw TS/TSX source (its `exports` point at `./src`). With
22
+ > Next.js, add it to `transpilePackages` so the host build transpiles it:
23
+ >
24
+ > ```js
25
+ > // next.config.js
26
+ > module.exports = { transpilePackages: ["@paikko/widget", "@paikko/contract"] };
27
+ > ```
28
+
29
+ ## Usage
30
+
31
+ Mount `<PaikkoProvider>` once, typically inside a `"use client"` island near the
32
+ root of your app (the function-valued `getClientState` prop cannot cross the
33
+ server/client boundary, so it must live in a client module):
34
+
35
+ ```tsx
36
+ "use client";
37
+ import { PaikkoProvider } from "@paikko/widget";
38
+ import { getState } from "@/lib/store";
39
+
40
+ export function PaikkoMount() {
41
+ return (
42
+ <PaikkoProvider
43
+ endpoint="http://localhost:8788/api/reports"
44
+ projectKey="my-app"
45
+ ticketsUrl="http://localhost:8788/tickets"
46
+ getClientState={getState}
47
+ reporter="anonymous"
48
+ />
49
+ );
50
+ }
51
+ ```
52
+
53
+ ### `<PaikkoProvider>` props
54
+
55
+ | Prop | Type | Required | Description |
56
+ |------------------|------------------------------------|----------|-------------|
57
+ | `endpoint` | `string` | yes | Backend reports intake URL the bundle is POSTed to. May be cross-origin. |
58
+ | `projectKey` | `string \| null` | no | Project/tenant key stamped onto every report (SaaS seam). Defaults to `null`. |
59
+ | `apiKey` | `string` | no | Project **publishable** key (`pk_...`), sent as `x-paikko-key`. Required whenever the backend enforces auth (the default; off only with `PAIKKO_AUTH=disabled`). Public - never pass a secret key (`sk_...`). See the repo's `AUTH.md`. |
60
+ | `ticketsUrl` | `string` | no | Absolute URL of the backend review queue. Renders a nav pill linking to it; omit to hide the pill. |
61
+ | `getClientState` | `() => Record<string, unknown>` | no | Reader for your app's client state, snapshotted at report time. Pass your store's `getState` (or equivalent). Omit and client state is captured as `{}`. |
62
+ | `reporter` | `string` | no | Who is filing the report. Defaults to `"anonymous"`. |
63
+ | `enabled` | `boolean` | no | Whether to mount at all. **Defaults to dev-only** (`NODE_ENV !== "production"`), so the pills never ship to real users. Pass `true`/`false` to force it. |
64
+
65
+ `PaikkoProvider` is the default export and also a named export. `ReportButton`,
66
+ `PaikkoNav`, and the capture primitives are exported individually for hosts that
67
+ want to compose the pieces themselves.
68
+
69
+ ## Environment & production (read this before deploying)
70
+
71
+ paikko is a development/internal tool. Both the widget and the provenance plugin
72
+ default to **dev-only** so nothing leaks into a production deploy, but there are
73
+ two footguns worth knowing:
74
+
75
+ - **Widget rendering.** `<PaikkoProvider>` renders only when `NODE_ENV !==
76
+ "production"` unless you pass `enabled`. To gate it from an env file, drive the
77
+ prop explicitly and put the flag in `.env.development` (loaded only by `next
78
+ dev`), **not** `.env.local` - Next loads `.env.local` in production builds too,
79
+ so a "dev" flag there silently enables the widget in prod:
80
+
81
+ ```tsx
82
+ <PaikkoProvider
83
+ enabled={process.env.NEXT_PUBLIC_PAIKKO_ENABLED === "true"}
84
+ endpoint="https://paikko.example.com/api/reports"
85
+ /* ... */
86
+ />
87
+ ```
88
+
89
+ - **Provenance attributes.** The babel plugin no-ops when `NODE_ENV ===
90
+ "production"` by default, so `data-src` source paths never ship in prod HTML.
91
+ A JSON `.babelrc` cannot read env vars; if you need to force it on/off, use a
92
+ `.babelrc.js` and pass `{ enabled: process.env.NEXT_PUBLIC_PAIKKO_ENABLED ===
93
+ "true" }`. Clear `.next` / `node_modules/.cache` after changing this - a stale
94
+ babel cache keeps the old behavior.
95
+
96
+ - **Cross-origin requests are safe.** The capture layer injects its
97
+ `x-paikko-trace` / `x-paikko-session` headers only on **same-origin** fetch/XHR
98
+ calls. Cross-origin requests (third-party CDNs, wasm, media) are still recorded
99
+ but their headers are left untouched, so paikko never trips a CORS preflight on
100
+ someone else's endpoint.
101
+
102
+ ## Provenance babel plugin (required)
103
+
104
+ The widget's capture relies on a babel plugin that stamps source provenance onto
105
+ your components at build time. Wire it into the consumer's babel config:
106
+
107
+ - Plugin: `@paikko/widget/build/provenancePlugin`
108
+ (`packages/widget/src/build/provenancePlugin.cjs`)
109
+ - Reference config: `examples/calculator/.babelrc`
110
+
111
+ ```json
112
+ // .babelrc
113
+ {
114
+ "presets": ["next/babel"],
115
+ "plugins": [
116
+ ["@paikko/widget/build/provenancePlugin", { "rootDir": "." }]
117
+ ]
118
+ }
119
+ ```
120
+
121
+ The `examples/calculator` app references the plugin by relative path
122
+ (`../../packages/widget/src/build/provenancePlugin.cjs`) because it runs from
123
+ inside this monorepo; a published consumer would use the package subpath shown
124
+ above. The subpath resolves via the package `exports` map to
125
+ `./src/build/provenancePlugin.cjs` (there is no literal `build/` dir); if your
126
+ toolchain doesn't honor the exports map, fall back to the explicit path
127
+ `./node_modules/@paikko/widget/src/build/provenancePlugin.cjs`.
128
+
129
+ The plugin no-ops in production by default (see **Environment & production**), so
130
+ the plain JSON `.babelrc` above is safe to commit - it emits provenance in dev
131
+ and strips it from prod builds automatically.
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@paikko/widget",
3
+ "version": "0.1.0",
4
+ "description": "paikko consumer report widget (PaikkoProvider, ReportButton, PaikkoNav, capture) + provenance plugin. Backend-agnostic: configurable endpoint, projectKey, ticketsUrl.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/willyt3hwhale/paikko.git",
9
+ "directory": "packages/widget"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./build/provenancePlugin": "./src/build/provenancePlugin.cjs"
15
+ },
16
+ "files": [
17
+ "src/**/*.ts",
18
+ "src/**/*.tsx",
19
+ "src/build/provenancePlugin.cjs",
20
+ "README.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit -p tsconfig.json"
27
+ },
28
+ "dependencies": {
29
+ "@paikko/contract": "^0.1.0",
30
+ "html2canvas": "^1.4.1",
31
+ "zod": "^3.23.8"
32
+ },
33
+ "peerDependencies": {
34
+ "react": "^18.3.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/react": "^18.3.11",
38
+ "react": "^18.3.1",
39
+ "typescript": "^5.6.3"
40
+ }
41
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ /**
4
+ * paikko `<PaikkoNav>` - a small global navigation pill that sits beside the
5
+ * `<ReportButton>` FAB and links to the review queue.
6
+ *
7
+ * It is mounted app-wide (alongside `<ReportButton>` in `<PaikkoProvider>`) so
8
+ * the user can reach the queue from anywhere. It is deliberately visually
9
+ * secondary to the Report FAB - an outlined dark pill rather than the solid
10
+ * primary fill - so Report stays the one-tap prominent action.
11
+ *
12
+ * Layout: the FAB lives at `right:20 bottom:20` (~78px wide). This pill sits
13
+ * just to its LEFT at `right:108 bottom:20`, clear of both the FAB and the
14
+ * report confirmation panel (which anchors at `right:20 bottom:76`).
15
+ *
16
+ * It is tagged `data-paikko-ui` so the capture controller ignores clicks on it
17
+ * (point-mode skips anything inside `[data-paikko-ui]`).
18
+ *
19
+ * The review UI now lives on the BACKEND origin (a separate deployment from the
20
+ * consumer app the widget is installed in), so the link target is a configurable
21
+ * `ticketsUrl` - typically an absolute URL like
22
+ * `https://api.example.com/tickets`. We render a plain `<a>` (not `next/link`)
23
+ * because the destination is cross-origin and because the widget must not depend
24
+ * on the host being a Next app. When `ticketsUrl` is omitted the pill is not
25
+ * rendered at all (an unconfigured nav is worse than no nav).
26
+ */
27
+ import React from "react";
28
+
29
+ export interface PaikkoNavProps {
30
+ /**
31
+ * Absolute URL of the backend review queue (e.g.
32
+ * `https://api.example.com/tickets`). Omit to hide the nav pill.
33
+ */
34
+ ticketsUrl?: string;
35
+ /** Pill label. Defaults to "Tickets". */
36
+ label?: string;
37
+ }
38
+
39
+ export function PaikkoNav({
40
+ ticketsUrl,
41
+ label = "Tickets",
42
+ }: PaikkoNavProps): React.JSX.Element | null {
43
+ if (!ticketsUrl) return null;
44
+
45
+ return (
46
+ <div data-paikko-ui="nav" style={styles.root}>
47
+ <a
48
+ href={ticketsUrl}
49
+ data-paikko-ui="nav-link"
50
+ style={styles.pill}
51
+ aria-label="Review tickets"
52
+ >
53
+ {label}
54
+ </a>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ const Z = 2147483000; // match the ReportButton stacking context
60
+
61
+ const styles: Record<string, React.CSSProperties> = {
62
+ root: {
63
+ position: "fixed",
64
+ inset: 0,
65
+ pointerEvents: "none",
66
+ zIndex: Z,
67
+ fontFamily:
68
+ "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
69
+ },
70
+ pill: {
71
+ position: "fixed",
72
+ right: 108, // just left of the FAB (right:20, ~78px wide) - never overlaps
73
+ bottom: 20,
74
+ pointerEvents: "auto",
75
+ display: "inline-flex",
76
+ alignItems: "center",
77
+ padding: "10px 16px",
78
+ borderRadius: 999,
79
+ // Outlined / lighter so it reads as secondary to the solid Report FAB.
80
+ border: "1px solid rgba(255,255,255,0.18)",
81
+ background: "rgba(17,24,39,0.85)",
82
+ color: "#e5e7eb",
83
+ fontSize: 14,
84
+ fontWeight: 600,
85
+ textDecoration: "none",
86
+ cursor: "pointer",
87
+ boxShadow: "0 6px 20px rgba(0,0,0,0.25)",
88
+ whiteSpace: "nowrap",
89
+ },
90
+ };
91
+
92
+ export default PaikkoNav;
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Client boundary that mounts the paikko `<ReportButton>` (and optional
5
+ * `<PaikkoNav>` pill) once for the whole host app.
6
+ *
7
+ * The host app's root layout is typically a server component, so the capture
8
+ * entry point has to be mounted inside a `"use client"` island. This is that
9
+ * island. The widget is backend-agnostic and store-agnostic:
10
+ *
11
+ * - `endpoint` / `projectKey` configure where reports go and which tenant they
12
+ * belong to (forwarded to `<ReportButton>`); the bundle is POSTed there
13
+ * cross-origin.
14
+ * - `ticketsUrl` configures the review-queue link (forwarded to `<PaikkoNav>`);
15
+ * omit it to hide the nav pill.
16
+ * - `getClientState` is supplied BY THE HOST APP - the provider does not import
17
+ * any store. The host passes its own store's `getState` (or equivalent) so
18
+ * the `clientState` artifact is captured from the host's app state at report
19
+ * time. Omit it and client state is captured as `{}`.
20
+ */
21
+ import React from "react";
22
+ import { ReportButton } from "./ReportButton";
23
+ import { PaikkoNav } from "./PaikkoNav";
24
+
25
+ export interface PaikkoProviderProps {
26
+ /** Backend reports intake URL the bundle is POSTed to (may be cross-origin). */
27
+ endpoint: string;
28
+ /** Project/tenant key stamped onto every report (SaaS seam). */
29
+ projectKey?: string | null;
30
+ /**
31
+ * The project's PUBLISHABLE api key (pk_...), forwarded to `<ReportButton>` and
32
+ * sent as `x-paikko-key`. Required only when the backend enforces auth
33
+ * (enforced by default; off via PAIKKO_AUTH=disabled). Public key - never pass a secret (sk_...).
34
+ */
35
+ apiKey?: string;
36
+ /** Absolute URL of the backend review queue; omit to hide the nav pill. */
37
+ ticketsUrl?: string;
38
+ /** Reader for the host app's client state, snapshotted at report time. */
39
+ getClientState?: () => Record<string, unknown>;
40
+ /** Who is filing. Defaults to "anonymous" inside `<ReportButton>`. */
41
+ reporter?: string;
42
+ /**
43
+ * Whether to mount the widget at all. paikko is a development/internal tool, so
44
+ * by DEFAULT it renders only when `NODE_ENV !== "production"` - the pills never
45
+ * ship to real end-users unless you opt in. Pass `true`/`false` to force it
46
+ * (e.g. `enabled={process.env.NEXT_PUBLIC_PAIKKO_ENABLED === "true"}` to drive
47
+ * it from a dev-only env file like `.env.development`).
48
+ */
49
+ enabled?: boolean;
50
+ }
51
+
52
+ export function PaikkoProvider({
53
+ endpoint,
54
+ projectKey = null,
55
+ apiKey,
56
+ ticketsUrl,
57
+ getClientState,
58
+ reporter,
59
+ enabled,
60
+ }: PaikkoProviderProps): React.JSX.Element | null {
61
+ const active =
62
+ enabled ?? process.env.NODE_ENV !== "production";
63
+ if (!active) return null;
64
+
65
+ return (
66
+ <>
67
+ <ReportButton
68
+ endpoint={endpoint}
69
+ projectKey={projectKey}
70
+ apiKey={apiKey}
71
+ getClientState={getClientState}
72
+ reporter={reporter}
73
+ />
74
+ <PaikkoNav ticketsUrl={ticketsUrl} />
75
+ </>
76
+ );
77
+ }
78
+
79
+ export default PaikkoProvider;