@journeeze/sdk 0.1.0-rc.2 → 0.1.0-rc.3

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.
Files changed (72) hide show
  1. package/AGENTS.md +16 -3
  2. package/CHANGELOG.md +12 -1
  3. package/README.md +298 -119
  4. package/dist/anchor-cPvFFTOS.js +1 -0
  5. package/dist/attach-DpioALxW.js +21 -0
  6. package/dist/chat-BgmB0rnW.js +64 -0
  7. package/dist/consent-DckCUvYR.js +1 -0
  8. package/dist/contract.js +1 -1
  9. package/dist/feedback-CtH1a_kQ.js +1 -0
  10. package/dist/help-w0O5Qu-4.js +105 -0
  11. package/dist/hook-DGWypoEO.js +1 -0
  12. package/dist/index-B_nob8S0.js +1 -2091
  13. package/dist/{index-CfCbJJXa.js → index-CwG2LrhO.js} +3 -3
  14. package/dist/index.js +3 -3
  15. package/dist/jobs-CAXbM5-g.js +45 -0
  16. package/dist/loader-BZiIDD1D.js +96 -0
  17. package/dist/mask-audit-Du3NPoG_.js +5 -0
  18. package/dist/react.js +12 -11
  19. package/dist/resolve-BfWByuTp.js +1 -97
  20. package/dist/script.js +10 -9
  21. package/dist/signals-amjMQyOy.js +1 -0
  22. package/dist/telemetry-CyRJDaVs.js +1 -0
  23. package/dist/transport-B64CfIcF.js +1 -0
  24. package/dist/types/chat/capture/attach.d.ts +41 -0
  25. package/dist/types/chat/capture/draw.d.ts +10 -0
  26. package/dist/types/chat/capture/geometry.d.ts +30 -0
  27. package/dist/types/chat/capture/styles.d.ts +1 -0
  28. package/dist/types/chat/shell/chat.d.ts +27 -0
  29. package/dist/types/chat/shell/converse.d.ts +95 -0
  30. package/dist/types/chat/shell/copy.d.ts +8 -0
  31. package/dist/types/chat/shell/model.d.ts +36 -0
  32. package/dist/types/chat/shell/session.d.ts +148 -0
  33. package/dist/types/chat/shell/styles.d.ts +1 -0
  34. package/dist/types/chat/shell/view.d.ts +18 -0
  35. package/dist/types/chat/transport.d.ts +5 -0
  36. package/dist/types/config.d.ts +17 -0
  37. package/dist/types/help/core/controller.d.ts +12 -3
  38. package/dist/types/help/core/events.d.ts +24 -0
  39. package/dist/types/help/core/help.d.ts +1 -0
  40. package/dist/types/help/core/panel.d.ts +16 -0
  41. package/dist/types/help/core/routing.d.ts +21 -0
  42. package/dist/types/help/ui/jobs/jobs.d.ts +24 -0
  43. package/dist/types/help/ui/jobs/model.d.ts +33 -0
  44. package/dist/types/help/ui/jobs/styles.d.ts +1 -0
  45. package/dist/types/loader.d.ts +2 -0
  46. package/dist/types/mask-detector.d.ts +22 -0
  47. package/dist/types/preview/flag.d.ts +2 -0
  48. package/dist/types/preview/mask-audit.d.ts +29 -0
  49. package/dist/types/schemas/feedback.d.ts +76 -0
  50. package/dist/types/schemas/finding.d.ts +22 -0
  51. package/dist/types/schemas/index.d.ts +6 -0
  52. package/dist/types/schemas/turn.d.ts +26 -0
  53. package/dist/types/signals/signals.d.ts +7 -3
  54. package/dist/types/signals/steps.d.ts +33 -0
  55. package/dist/types/telemetry/telemetry.d.ts +10 -0
  56. package/dist/types/types.d.ts +72 -1
  57. package/dist/types/widget/capture.d.ts +12 -1
  58. package/dist/types/widget/upload.d.ts +14 -0
  59. package/dist/types/widget.d.ts +1 -21
  60. package/dist/upload-Fu2qvSJV.js +14 -0
  61. package/dist/widget-B6mR1J3P.js +118 -0
  62. package/package.json +4 -2
  63. package/dist/consent-CoKD33Rm.js +0 -170
  64. package/dist/feedback-C4CYtS2A.js +0 -196
  65. package/dist/help-C1x-y2f9.js +0 -878
  66. package/dist/hook-BdG4UL4k.js +0 -91
  67. package/dist/loader-Ba4X-WNl.js +0 -95
  68. package/dist/signals-E3VEdmWH.js +0 -477
  69. package/dist/telemetry-Cik-cYzw.js +0 -140
  70. package/dist/transport-B5YJFQEF.js +0 -60
  71. package/dist/types/signals/signals.conformance.d.ts +0 -2
  72. package/dist/widget-DQZm8sAC.js +0 -1599
package/AGENTS.md CHANGED
@@ -10,9 +10,13 @@ The public types in this package (`dist/types`) follow them.
10
10
 
11
11
  ## Configuration
12
12
  - `siteKey` (`pk_` + 32 hex, `SITE_KEY_PATTERN`) is **public** runtime config per site. It is not a secret.
13
- - Direct mode is the default (browser → `app.journeeze.dev`, CORS for the site's allowlisted
14
- origins). Set `apiHost`/`apiPath` **only** when the host runs a same-origin proxy
15
- (docs/collector-proxy.md); `http:` is accepted only for `localhost`/`127.0.0.1`.
13
+ - Direct mode is the default: the browser calls `https://app.journeeze.dev` with CORS for the
14
+ site's allowlisted origins. Set `apiHost` (script tag: `data-journeeze-api-host`) in every
15
+ environment except production: `https://app.staging.journeeze.dev` for a staging site key,
16
+ `http://localhost:8080` for the local stack, or the host's own origin when it runs a same-origin
17
+ proxy (then `apiPath` for a sub-path). Keep it in the same runtime config as the site key. The
18
+ SDK never infers it from where the script was loaded. `http:` is accepted only for
19
+ `localhost`/`127.0.0.1`.
16
20
  - CSP, if the host sends one: `script-src 'self' https://app.journeeze.dev` (+ `'wasm-unsafe-eval'`
17
21
  only with signals), `connect-src 'self' https://app.journeeze.dev`, `img-src 'self' blob:
18
22
  https://app.journeeze.dev`, `media-src 'self' https://app.journeeze.dev`, and pass your `nonce`.
@@ -60,6 +64,15 @@ The public types in this package (`dist/types`) follow them.
60
64
  one tflo runtime; otherwise the SDK lazy-loads its own copy after consent.
61
65
  - Signals start only after the same consent as feedback, and stop when the site's kill switch is off.
62
66
 
67
+ ## Script tag
68
+ - `window.Journeeze` exists only after the module script runs, which is after inline scripts. Use
69
+ it from a user event or after `DOMContentLoaded`, never during page load.
70
+
71
+ ## GitHub
72
+ - Issues and the `.jevitate/` catalog go through the Journeeze GitHub App
73
+ (`https://github.com/apps/journeeze/installations/new`), installed by the product owner on the
74
+ repository named at setup. Never use a personal access token.
75
+
63
76
  ## Never
64
77
  - Put a secret in the bundle (the HMAC secret, tokens, gateway keys).
65
78
  - Send raw user ids, emails, names, URLs or record ids anywhere (context, steps, rules, params).
package/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.1.0-rc.3 (not yet published)
4
+
5
+ - **Setup documentation:** the README is a self-contained setup guide: what the Journeeze team sends you, install with a pinned version, `apiHost` per environment, CSP, signed identity with a test vector, privacy and masking, help, feedback, signals, the Journeeze GitHub App and its permissions, a checklist and troubleshooting. Examples use `window.Journeeze` only after `DOMContentLoaded`. The license line matches `LICENSE.md`.
6
+ - **Help chat on the live `Converse` (HC8c):** each turn carries the reviewer token (when the host has one), and on consented turns the signed user id (`userId`, `userIdExp`, `userIdSig`) and the detector's over-masking counts (`overMasked`, counts per rule only). Closing the chat (✕ or Esc) ends a started conversation with a `cancel` turn; opening it again starts a new one.
7
+ - **Chat copy as data (ADR 0020 §5):** the help chat ships no words of its own. Its chrome comes from the site's `widget.ui.*` copy (new `GetWidgetConfig.copy`, by full key, `{name}` placeholders filled by the widget), and its buttons, chips and "+" menu use the labels the server sends on each `Converse` action (`label`, `menu[].label/hint`). The chat reads `GetWidgetConfig` once (copy and mask selectors). If that call fails, only the launcher keeps a last-resort accessible name.
8
+ - **Help chat shell (SDK 2.x, HC8a), behind `chat: true`:** `enableHelp()` (or `data-journeeze-chat` on the script tag) shows one "?" launcher that opens a conversation instead of the v1 help panel: job chips filtered as you type ("Other — none of these" last), inline guide cards, a composer with a "+" menu, a one-time consent line before anything is stored, a removable "Sent with this report" row and, for reviewers, "Team details". Turns go to the collector's `Converse` (direct-mode contract v2 §2). New `feedback()` on the handle opens the chat at "Report a problem" (without `chat: true` it switches feedback mode on). The chat is its own lazy chunk (≤ 11.5 KB gz).
9
+ - **Intent routing in help (P6d):** when a reader who has already consented sends "Something else", help first asks the collector (`RouteIntent`) which of the site's jobs their words mean. A confident match with a guide shows "Did you mean: <job>?" with "Show me how" (its guide) and "No, send my request"; otherwise the request is sent as before. The answer is only a job id and its guide, never generated text; the words are not stored by routing. Without consent nothing is routed.
10
+ - **Jobs in help (P4e2):** when the site's help index lists jobs, help opens on "What are you trying to do?": the jobs common for the host's `persona` under "Common for you", the rest under "More", a local filter, and "Something else". A job opens its guide (the persona's own when it has one) and ends with "Got it done?" (yes / partly / no, plus "Harder than it should be"); a job without a guide offers "Not possible here". `job_picked` / `job_not_possible` / `job_outcome` are aggregate engagement events (job id and entry version only), sent only after consent, with help on and never under Global Privacy Control. "Something else" is the reader's own words as a consented feedback request (with the optional "so you can…"), never telemetry. The jobs view is a separate lazy chunk (≤ 5 KB gz), loaded only for sites with jobs.
11
+
12
+ ## 0.1.0-rc.2 (2026-10-09)
13
+
14
+ First version published to the public npm registry.
4
15
 
5
16
  - **Public on npm:** published to the npm registry as `@journeeze/sdk` (no registry config or token), starting with 0.1.0-rc.2. GitHub Packages (`@matt-cochran-products/journeeze-sdk`) is no longer published. The package is licensed for use with the Journeeze service (`LICENSE.md`); it is not open source.
6
17
  - **Signed host identity (contract C0a §4, v1.3):** `userIdSig` / `userIdExp` config fields and provider props, and `setUser(userId, { sig, exp })`. The signature is sent with feedback and end-user screenshot uploads while it is unexpired; the same id with a new signature only refreshes it. Sites that require signed identity treat an unsigned id as anonymous.
package/README.md CHANGED
@@ -1,165 +1,344 @@
1
1
  # @journeeze/sdk
2
2
 
3
- Opt-in "suggest an improvement" mode for any website or app. A tiny loader (≤ 2 KB gz) holds
4
- your options and context; the widget is lazy-loaded only when the mode is switched on.
5
- Nothing observes the page while the mode is off. Inputs and editable regions are always masked,
6
- and the widget asks for consent before anything is captured.
3
+ Journeeze adds in-app help and an opt-in feedback mode to any website or app. Visitors read
4
+ step-by-step guides built from your documented user journeys, and invited reviewers (or signed-in
5
+ users) point at what is unclear. Journeeze groups that feedback and files it as GitHub issues in
6
+ your repository.
7
7
 
8
- ## Install and transport
8
+ The SDK is a small loader (about 2 KB gzipped). Nothing observes the page until help or the
9
+ feedback mode is switched on, and nothing is captured before the visitor consents.
9
10
 
10
- Install from npm, or use the script tag served by Journeeze at `/sdk/`. The Journeeze
11
- integration guide covers the full path (provisioning, CSP block, identity, help, signals,
12
- checklist).
11
+ **Contents:** [Before you start](#before-you-start) · [Install](#install) ·
12
+ [Environments](#environments-and-apihost) · [Content Security Policy](#content-security-policy) ·
13
+ [Signed-in users](#signed-in-users) · [Privacy and masking](#privacy-and-masking) ·
14
+ [Help](#in-app-help) · [Feedback mode](#feedback-mode) · [Signals](#signals-optional) ·
15
+ [GitHub](#connect-github) · [Checklist](#setup-checklist) ·
16
+ [Troubleshooting](#troubleshooting) · [Versions](#versions-and-license)
13
17
 
14
- ```sh
15
- npm install @journeeze/sdk@<version>
16
- ```
18
+ ## Before you start
17
19
 
18
- **Direct mode (default since v1):** the SDK calls the Journeeze collector that issued your site
19
- key (`https://app.journeeze.dev`) with CORS for your site's allowlisted origins
20
- (`docs/contract/direct-mode-v1.md`). Requests never carry cookies or other credentials and are
21
- never cached.
20
+ Journeeze is in early access. The Journeeze team sets up your account. Send them:
22
21
 
23
- **Proxy mode (optional):** set `apiHost` to your own origin (`data-journeeze-api-host` on the
24
- script tag; `apiPath` / `data-journeeze-api-path` for a sub-path) and proxy the collector paths
25
- per `docs/collector-proxy.md`. A v0 host that relied on the old same-origin default must set
26
- `apiHost: location.origin` before upgrading. `endpoint` / `data-journeeze-endpoint` still work
27
- and are deprecated.
22
+ 1. Your organisation and product name.
23
+ 2. Every origin the SDK will run on, with scheme, host and port. Example:
24
+ `https://app.example.com`, `https://*.preview.example.com`. Staging and production are separate.
25
+ 3. Whether visitors can be anonymous, or every user is signed in.
26
+ 4. The GitHub repository issues should go to (`owner/repo`).
27
+ 5. Who should be the project owner on the review board.
28
28
 
29
- ```html
30
- <script type="module" src="https://app.journeeze.dev/sdk/script.js" data-journeeze-site-key="pk_..."></script>
31
- <script>/* later, from your own control: */ window.Journeeze?.enable();</script>
29
+ You get back:
30
+
31
+ | You receive | What it is | Where it goes |
32
+ |---|---|---|
33
+ | A **site key** per site (`pk_` + 32 hex) | Public identifier. Not a secret. | Your page or app runtime config |
34
+ | An **identity secret** per site (64 hex) | Server secret for [signing user ids](#signed-in-users). Sent as a one-time 1Password link, never by email or chat. | Your server's secret store only |
35
+ | An **owner invite link** | Opens the review board as project owner. Single use. | Open it once, in your browser |
36
+
37
+ ## Install
38
+
39
+ Pick one way. Both load the same code.
40
+
41
+ ### npm (bundled apps)
42
+
43
+ ```sh
44
+ npm install @journeeze/sdk@0.1.0-rc.3 --save-exact
32
45
  ```
33
46
 
47
+ Pin an exact version. Release candidates (`-rc.N`) are on the `rc` tag; stable releases on
48
+ `latest`.
49
+
50
+ **React:**
51
+
34
52
  ```tsx
35
53
  import { JourneezeProvider, useJourneeze } from "@journeeze/sdk/react";
36
54
 
37
- <JourneezeProvider siteKey="pk_..." context={{ persona: "owner", journeyId: "r1-j09", journeyStep: "pay" }}>
38
- <App />
39
- </JourneezeProvider>;
55
+ export function Root() {
56
+ return (
57
+ <JourneezeProvider
58
+ siteKey={config.journeezeSiteKey} // public, from runtime config
59
+ apiHost={config.journeezeApiHost} // staging and local only; see "Environments"
60
+ context={{ persona: "owner", journeyId: "billing", journeyStep: "route.billing.pay" }}
61
+ pagePath="none" // you pass route ids as journeyStep
62
+ >
63
+ <App />
64
+ </JourneezeProvider>
65
+ );
66
+ }
40
67
 
41
- const jz = useJourneeze(); jz.enable(); jz.setContext({ journeyStep: "confirm" });
68
+ function HelpButton() {
69
+ const jz = useJourneeze();
70
+ return <button onClick={() => jz.enableHelp()}>Help</button>;
71
+ }
42
72
  ```
43
73
 
44
- - Keep `context` in sync with your router: the provider sets it on every change and clears values
45
- a route no longer provides. The widget asks "Did this step make sense?" about the step the
46
- reviewer just finished when `journeyStep` changes.
47
- - Invited reviewers arrive with `?jz_invite=<token>`: the script tag and the provider read it once,
48
- remove it from the URL and keep it for the tab's session (`takeReviewerToken`), unless you pass
49
- `reviewerToken` yourself.
50
- - `onModeChange(enabled)` tells you when the mode switches on or off, including from inside the
51
- widget (Esc, Exit, "Not now").
74
+ - While `siteKey` is `undefined` (config still loading) the provider renders with an inert handle:
75
+ every method does nothing and nothing is sent. The real handle starts with the first key.
76
+ - Keep `context` in sync with your router. The provider applies every change.
77
+ - With TypeScript's `exactOptionalPropertyTypes`, optional props do not accept `undefined`. Pass
78
+ them only when set: `{...(config.journeezeApiHost ? { apiHost: config.journeezeApiHost } : {})}`.
52
79
 
53
- ## Signed-in users, logout and privacy options (contract v1.1)
80
+ **Without React:**
54
81
 
55
- The full contract is `docs/contract/sdk-v1.md`. The parts a host touches most:
82
+ ```ts
83
+ import { createJourneeze } from "@journeeze/sdk";
56
84
 
57
- ```tsx
58
- <JourneezeProvider
59
- siteKey="pk_..."
60
- apiHost="https://www.example.com" // proxy mode only; omit for direct mode (the default)
61
- userId={user ? user.journeezeHash : undefined} // HMAC from YOUR server, never an email or raw id
62
- pagePath="none" // you pass route ids (e.g. route.projects.hub) as journeyStep
63
- maskSelectors={[".customer-name"]}
64
- context={{ persona, journeyId, journeyStep: routeTemplate }}
65
- >
85
+ const jz = createJourneeze({ siteKey: "pk_...", apiHost: "https://app.staging.journeeze.dev" });
86
+ document.querySelector("#help")?.addEventListener("click", () => jz.enableHelp());
66
87
  ```
67
88
 
68
- - **`userId`** is `HMAC-SHA256(server_secret, internal_user_id)` as 64 hex or 43 base64url
69
- characters, delivered in an authenticated `no-store` response. Anything else is ignored with a
70
- warning; the SDK never hashes. `USER_ID_PATTERN` and the other patterns are exported from
71
- `@journeeze/sdk/contract` for your server's tests.
72
- - **Logout:** the provider handles it. `userId` changing away from an id, or the provider
73
- unmounting, calls `reset()`. Script-tag hosts call `window.Journeeze.reset()` on every logout
74
- path. `setUser(otherId)` resets first. `reset()` sends the user's unsent feedback once and
75
- removes it from the device, clears context, consent and the reviewer token, and keeps help
76
- available (empty) if it was on.
77
- - **Masking:** inputs, editable regions, `[data-jz-mask]`, media and frames are always masked.
78
- `maskSelectors` and `maskAllText` widen it; an invalid selector switches screenshots off (fails
79
- closed). `[data-jz-block]` leaves an empty box and cannot be picked. Users can always send
80
- without the screenshot, after seeing it full size.
81
- - **Who sends a screenshot:** reviewers, and signed-in end users the host attributes with
82
- `userId` on a site that accepts non-reviewer feedback; the upload is bound to that user. A fully
83
- anonymous visitor never captures or sends one, and the consent copy says so (ADR 0013 §4).
84
- - **`pagePath`:** `"redacted"` (default) replaces id-like path segments with `:id`; `"none"` sends
85
- no path.
86
- - **`nonce`** is set on the SDK's `<style>` elements for strict CSPs; Global Privacy Control turns
87
- help telemetry off (`respectGlobalPrivacyControl`, default `true`).
89
+ ### Script tag (no bundler)
88
90
 
89
- ## In-app help
91
+ ```html
92
+ <script type="module"
93
+ src="https://app.journeeze.dev/sdk/script.js"
94
+ data-journeeze-site-key="pk_..."
95
+ data-journeeze-help></script>
96
+ ```
90
97
 
91
- Help is independent of the feedback mode. It shows a launcher that opens guides built from the
92
- site's published journeys, with search, step-by-step guidance that outlines each step's element,
93
- and an optional "Did this step make sense?" check-in. Reading guides needs no consent; nothing
94
- of the help chunk loads or listens on the page until you switch it on.
98
+ The tag creates `window.Journeeze`. It is a module script, so it runs after your inline scripts.
99
+ Use it from a click, or after `DOMContentLoaded`, never while the page is still loading:
95
100
 
96
101
  ```html
97
- <script type="module" src="https://<cdn>/journeeze/script.js" data-journeeze-site-key="pk_..." data-journeeze-help></script>
102
+ <button id="suggest">Suggest an improvement</button>
103
+ <script>
104
+ document.addEventListener("DOMContentLoaded", () => {
105
+ document.getElementById("suggest").addEventListener("click", () => window.Journeeze?.enable());
106
+ });
107
+ </script>
98
108
  ```
99
109
 
100
- ```tsx
101
- const jz = useJourneeze();
102
- await jz.enableHelp(); // lazy-loads the help chunk and shows the launcher
110
+ | Attribute | Effect |
111
+ |---|---|
112
+ | `data-journeeze-site-key` | Required. Your site key. |
113
+ | `data-journeeze-api-host` | The collector origin. Required outside production; see below. |
114
+ | `data-journeeze-api-path` | Path prefix, only for a same-origin proxy. |
115
+ | `data-journeeze-help` | Shows the help launcher at once. |
116
+ | `data-journeeze-chat` | Help opens as a conversation instead of the guide panel. |
117
+
118
+ `/sdk/script.js` is always the SDK version the Journeeze service currently runs. To control the
119
+ version yourself, install from npm and pin it.
120
+
121
+ ## Environments and `apiHost`
122
+
123
+ The SDK sends to production (`https://app.journeeze.dev`) unless you set `apiHost`. It does not
124
+ follow where the script was loaded from. A staging or local site key sent to production is
125
+ refused, so set `apiHost` next to the site key in each environment's config:
126
+
127
+ | Environment | `apiHost` / `data-journeeze-api-host` |
128
+ |---|---|
129
+ | Production | leave it out |
130
+ | Staging | `https://app.staging.journeeze.dev` |
131
+ | Local Journeeze stack | `http://localhost:8080` |
132
+ | Your own same-origin proxy | your origin, for example `https://www.example.com` |
133
+
134
+ The browser calls the collector directly with CORS, only from the origins you registered. Requests
135
+ carry no cookies and are not cached. A proxy is optional. It suits offices or CI where many users
136
+ share one IP address, or a policy that allows no third-party requests. The Journeeze team sends the
137
+ proxy guide (gateway key, the paths to forward, reference Worker and server code) on request.
138
+
139
+ ## Content Security Policy
140
+
141
+ If your site sends a CSP, add these. Use `https://app.staging.journeeze.dev` on staging.
142
+
143
+ ```
144
+ script-src 'self' https://app.journeeze.dev;
145
+ connect-src 'self' https://app.journeeze.dev;
146
+ img-src 'self' blob: https://app.journeeze.dev;
147
+ media-src 'self' https://app.journeeze.dev;
148
+ style-src 'self' 'nonce-<your nonce>';
149
+ ```
150
+
151
+ - Add `'wasm-unsafe-eval'` to `script-src` only if you turn on [signals](#signals-optional).
152
+ - Pass your nonce as the `nonce` option. The SDK puts it on the styles it injects.
153
+ - With npm and a proxy, `script-src` and `connect-src` need only `'self'`.
154
+
155
+ ## Signed-in users
156
+
157
+ Attributing feedback to a user is optional. When you do, your server makes the user id and signs
158
+ it. The browser never sees either secret.
159
+
160
+ 1. **User id.** `HMAC-SHA256(your own secret, your internal user id)`, as 64 lowercase hex or
161
+ 43 base64url characters. Never an email, name or raw id. The SDK ignores anything else with a
162
+ console warning. Keep this secret stable: changing it splits every user's history.
163
+ 2. **Signature.** With the identity secret you received for the site:
164
+
165
+ ```ts
166
+ import { createHmac } from "node:crypto";
167
+
168
+ export function signUserId(identitySecretHex: string, siteKey: string, userId: string) {
169
+ const userIdExp = Math.floor(Date.now() / 1000) + 3600; // at most 24 h ahead
170
+ const userIdSig = createHmac("sha256", Buffer.from(identitySecretHex, "hex"))
171
+ .update(`jz-id-v1\n${siteKey}\n${userId}\n${userIdExp}`)
172
+ .digest("base64url");
173
+ return { userId, userIdExp, userIdSig };
174
+ }
175
+ ```
176
+
177
+ Check your code against this vector. Secret: the bytes `0x00` to `0x1f` (hex
178
+ `000102…1e1f`). Site key `pk_0123456789abcdef0123456789abcdef`. User id: `a` × 64.
179
+ `userIdExp` `1800000000`. Expected `userIdSig`: `IluU_qUHY6OdSzqKu0Y_wS0sDB53qVOOnobfdHuCnjI`.
180
+ 3. **Deliver** `{ userId, userIdExp, userIdSig }` from an authenticated endpoint with
181
+ `Cache-Control: no-store`. Never put them in public config.
182
+ 4. **Pass them to the SDK:** provider props `userId`, `userIdSig`, `userIdExp`, or
183
+ `jz.setUser(userId, { sig, exp })`. Re-issue the signature before it expires. The SDK never
184
+ refreshes it.
185
+ 5. **Logout.** The React provider calls `reset()` when `userId` goes away or the provider
186
+ unmounts. Without React, call `jz.reset()` (or `window.Journeeze.reset()`) on every logout path.
187
+ `reset()` sends that user's unsent feedback once, then clears it, the context, consent and the
188
+ reviewer token from the device.
189
+
190
+ Sites require the signature by default. With a missing, expired or wrong signature, that feedback
191
+ is anonymous and has no screenshot.
192
+
193
+ ## Privacy and masking
194
+
195
+ - Nothing is captured until the visitor turns the feedback mode on and consents. Reading help
196
+ needs no consent.
197
+ - Screenshots are masked in the browser before encoding, and the visitor sees the masked image
198
+ before sending. They can always send without it.
199
+ - Always masked: inputs, textareas, selects, editable regions, video, audio, canvas, frames and
200
+ `[data-jz-mask]`. The SDK also masks elements that look sensitive (password and card fields, API
201
+ keys, IBANs, email-shaped text). Mark such elements yourself rather than relying on detection.
202
+ - `[data-jz-block]` leaves an empty box that cannot be picked.
203
+ - `maskSelectors: [".customer-name"]` and `maskAllText: true` widen masking. One invalid selector
204
+ turns screenshots off (fails closed).
205
+ - Anonymous visitors never capture or send a screenshot.
206
+ - `pagePath`: `"redacted"` (default) replaces id-like path segments with `:id`; `"none"` sends no
207
+ path.
208
+ - Global Privacy Control turns help telemetry and signals off (`respectGlobalPrivacyControl`,
209
+ default `true`).
210
+ - The collector accepts a closed list of fields. Raw ids, emails, URLs and page text are not on it.
211
+
212
+ Put `data-jz-mask` on every element that shows customer content.
213
+
214
+ ## In-app help
215
+
216
+ Help shows a launcher that opens guides built from your published journeys: search, step-by-step
217
+ guidance that outlines each step's element, and an optional "Did this step make sense?" check-in.
218
+
219
+ ```ts
220
+ await jz.enableHelp(); // loads the help code and shows the launcher
103
221
  jz.isHelpEnabled(); // true
104
- jz.disableHelp(); // removes the launcher and panel
222
+ jz.disableHelp(); // removes it
105
223
  ```
106
224
 
107
- To hide your own Help control when help isn't for this user, pass `onHelpStatus` (or the React
108
- provider's `onHelpStatus` prop). After `enableHelp()` checks the site it is called with
109
- `{ available: true }`, or `{ available: false, reason: "disabled" }` (the site has help off) or
110
- `{ available: false, reason: "audience" }` (reviewers-only help and no reviewer token), and again
111
- if that changes.
225
+ - `onHelpStatus(status)` tells you whether help is available here: `{ available: true }`, or
226
+ `{ available: false, reason: "disabled" }` (help is off for the site) or `"audience"` (help is
227
+ for reviewers only and this visitor is not one). Hide your own Help button on `false`.
228
+ - Steps point at elements by `data-tflow-id="area.element"` (lowercase letters, digits and `._:-`,
229
+ up to 96 characters). Give every element a guide points at a stable `data-tflow-id`. A step whose
230
+ element is not on the page shows its text without an outline.
231
+ - Set `journeyStep` in `context` to a stable id for where the user is (a route id such as
232
+ `route.projects.hub`), never a URL or record id. Guides and signals match on it.
233
+ - Help is switched on per site on the review board: **Settings → the site → In-app help**. Choose
234
+ **Reviewers** (default) or **Public visitors**.
112
235
 
113
- - `data-journeeze-help` on the script tag calls `enableHelp()` for you; a failed chunk load never
114
- surfaces as an unhandled rejection.
115
- - Help reads guides with the collector RPCs `GetHelpIndex` and `GetGuide`, and loads step
116
- screenshots, the guide video and its captions from `GET /journeeze.collector.v1/guide-assets/*`
117
- (short-lived signed links). Your proxy must forward that prefix and `GET /sdk/*` (the chunks)
118
- in addition to the feedback paths; see `docs/collector-proxy.md`.
119
- - Steps are anchored to `data-tflow-id` names. A step whose element isn't on the current page
120
- shows its text without an outline.
121
- - If help is switched off for the site (the site's help settings on the board's **Settings** screen), the launcher renders nothing.
236
+ Where guides come from: see [Connect GitHub](#connect-github).
122
237
 
123
- **Publishing guides.** Guides come from Jevitate journeys. An operator imports them into the
124
- project with `journeeze import-journeys --project <id> <.jevitate/journeys dir>`; each journey
125
- arrives as a draft. A triager reviews it on the board's **Journeys** screen (light edits to step
126
- text are allowed) and publishes it. Only published guides appear in help; a guide whose source
127
- journey changed goes stale until the new import is adopted.
238
+ ## Feedback mode
128
239
 
129
- Mark elements with `data-tflow-id="area.element"` to anchor feedback to stable names, and
130
- `data-jz-mask` to mask extra regions (form fields and `contenteditable` regions are always masked).
240
+ ```ts
241
+ await jz.enable(); // turns the feedback mode on (asks for consent first)
242
+ jz.disable();
243
+ jz.isEnabled();
244
+ ```
131
245
 
132
- License: undecided (`UNLICENSED`); packaged so it can be open-sourced later.
246
+ - Invited reviewers arrive with `?jz_invite=<token>`. The SDK reads it once, removes it from the
247
+ URL and keeps it for the tab.
248
+ - `onModeChange(enabled)` reports every switch, including Esc and Exit inside the widget.
249
+ - When `journeyStep` changes, the widget can ask about the step the reviewer just finished.
133
250
 
134
- ## Signals (signal contract C0d)
251
+ ## Signals (optional)
135
252
 
136
- Aggregate-only behaviour signals (`docs/contract/signals-v1.md`): rage clicks, dead clicks,
137
- hesitation, abandoned journey steps, and your own registered site signals. Raw events never
138
- leave the page; only derived signals are sent, through a closed allowlist, with no URL, text,
139
- user id or session id.
253
+ Aggregate behaviour signals: rage clicks, dead clicks, hesitation, abandoned journey steps and your
254
+ own registered signals. They are computed in the browser. Only the derived signal is sent, with no
255
+ URL, text, user id or session id.
140
256
 
141
257
  ```ts
142
258
  createJourneeze({
143
259
  siteKey,
144
260
  signals: {
145
261
  enabled: true,
146
- // Pass your own tflo module so the page runs one tflo runtime (recommended if you use tflo):
147
- tflo: () => import("tflo-browser-events"),
148
- // Optional site rules; each name must be registered for the site in Journeeze first.
149
- rules: [{ name: "export_clicked", when: 'event.kind == "click" && event.fields.target == "export.button"' }],
262
+ tflo: () => import("tflo-browser-events"), // pass your copy if your app already uses it
150
263
  },
151
264
  });
152
265
  ```
153
266
 
154
- - **When they run:** only after the visitor's consent, while help or feedback mode is on, with
155
- `signals.enabled: true` and the site's signals switch on in Journeeze. Global Privacy Control
156
- turns them off. Before that, no signals or tflo code is loaded.
157
- - **What is tracked:** elements with a `data-tflow-id` (`[a-z0-9._:-]`, up to 96 characters)
158
- outside `data-jz-mask` / `data-jz-block` regions and form fields. The SDK listens to `click`,
159
- `pointerover`/`pointerout`, `focusin`/`focusout` and `change` only: never keys, input or pointer
160
- moves, and it never patches `fetch` or XHR.
161
- - **tflo:** without `signals.tflo` the SDK loads its own copy as a separate lazy chunk with the
162
- WASM inlined. Under a Content-Security-Policy that needs `script-src 'wasm-unsafe-eval'` and
163
- `connect-src data:`; injecting your own module avoids the second.
164
- - **Journeys:** set `journeyId` and `journeyStep` with `setContext`; `journeyStep` must equal the
165
- published journey's step ids (its `stepRef`s) for `abandoned_step` to apply.
267
+ - They run only after consent, while help or feedback is on, and while the site's signals switch on
268
+ the board is on. Turning that switch off stops them without a deploy.
269
+ - Only elements with a `data-tflow-id` outside masked regions are tracked. The SDK listens to
270
+ clicks, hover, focus and change events. It never reads keystrokes, input values or pointer
271
+ movement.
272
+ - Without your own `tflo-browser-events` (`>=0.1.0 <1`, optional peer dependency) the SDK loads its
273
+ own copy. That needs `'wasm-unsafe-eval'` in `script-src` and `data:` in `connect-src`.
274
+
275
+ ## Connect GitHub
276
+
277
+ Journeeze files issues and reads your journey catalog through the **Journeeze GitHub App**.
278
+ Install it on the repository you named during setup:
279
+
280
+ **<https://github.com/apps/journeeze/installations/new>** → choose your account → **Only select
281
+ repositories** → your repository → **Install**.
282
+
283
+ | Permission | Why Journeeze needs it |
284
+ |---|---|
285
+ | Issues: read and write | File a cluster of feedback as one issue, and follow it until it is closed. |
286
+ | Contents: read and write | Read your `.jevitate/` journey catalog. Writes go only to a Journeeze branch, as a pull request you review. |
287
+ | Pull requests: read and write | Open those catalog pull requests and read their status. |
288
+ | Metadata: read | Basic repository information GitHub requires for every App. |
289
+
290
+ Each call uses a one-hour token limited to that repository. You can remove the App at any time
291
+ (GitHub → Settings → Applications → Installed GitHub Apps). Journeeze then stops reading and
292
+ filing.
293
+
294
+ **After installing, tell the Journeeze team.** They link the repository to your project. This is
295
+ one step on their side until the board does it for you.
296
+
297
+ **Your journey catalog.** Journeeze reads the Jevitate catalog in your repository's `.jevitate/`
298
+ folder (personas, jobs and journeys) on each push. Guides with screenshots and video are imported
299
+ from your approved journeys as drafts. A triager reviews each one on the board (**Journeys**), may
300
+ lightly edit the step text, and publishes it. Only published guides appear in help.
301
+
302
+ **Without the App: upload from CI.** Your CI can upload the catalog as a bundle instead:
303
+ `POST https://app.journeeze.dev/api/upload/v1/bundles` with `Authorization: Bearer jzu_…`. The
304
+ Journeeze team creates upload keys for now and sends them as a one-time 1Password link. Ask them
305
+ for the bundle format and a CI example. Never put an upload key in a URL or a log.
306
+
307
+ ## Setup checklist
308
+
309
+ - [ ] Site key (and `apiHost` outside production) in each environment's runtime config.
310
+ - [ ] SDK installed with a pinned version, or the script tag on every page.
311
+ - [ ] CSP updated, if you send one.
312
+ - [ ] `data-jz-mask` on every element with customer content; masked screenshot checked on each
313
+ screen that shows data.
314
+ - [ ] `data-tflow-id` on every element a guide points at; `journeyStep` set per route.
315
+ - [ ] Signed-in users: user id and signature made on your server and delivered `no-store`;
316
+ `reset()` on every logout path.
317
+ - [ ] Journeeze GitHub App installed on your repository, and the Journeeze team told.
318
+ - [ ] Help switched on for the site on the board, audience chosen, guides published.
319
+ - [ ] One test feedback item sent from each site and seen on the board.
320
+
321
+ ## Troubleshooting
322
+
323
+ | Symptom | Cause and fix |
324
+ |---|---|
325
+ | Requests go to `app.journeeze.dev` from staging or local, or the site key is refused | `apiHost` is not set. Set it for that environment. |
326
+ | CORS error | The page origin is not registered for the site (scheme, host and port must match exactly). Ask the Journeeze team to add it. |
327
+ | `window.Journeeze` is undefined | It was used during page load. Use it from a click or after `DOMContentLoaded`. |
328
+ | Help launcher never appears | Help is off for the site, or the audience is Reviewers and this visitor has no invite. Check `onHelpStatus`. |
329
+ | A guide step has no outline | The element's `data-tflow-id` is missing or changed. |
330
+ | No screenshot offered | An invalid `maskSelectors` entry (fails closed), or the visitor is anonymous. |
331
+ | Feedback is anonymous although `userId` is set | The signature is missing, wrong, expired, or made for another site key. |
332
+ | `userId` ignored with a console warning | It is not a 64-hex or 43-base64url hash (for example a raw UUID or an email). |
333
+ | `429` responses from an office or CI | Many users share one IP address. Use a proxy, or ask for a higher quota. |
334
+ | Issues are never filed | The Journeeze GitHub App is not installed on the repository, or the Journeeze team has not linked it yet. |
335
+
336
+ ## Versions and license
337
+
338
+ Changes are listed in `CHANGELOG.md` in this package. Public types ship in `dist/types`.
339
+ `@journeeze/sdk/contract` exports `SITE_KEY_PATTERN` and `USER_ID_PATTERN` for your server's
340
+ tests.
341
+
342
+ Proprietary: see `LICENSE.md`. You may install this package and include it, unmodified, in
343
+ websites and applications solely to connect them to the Journeeze service, under your agreement for
344
+ that service. It is not open source.
@@ -0,0 +1 @@
1
+ import{t as f}from"./context-UnG6_X61.js";const g=512,p=12;function b(t){const n=[];let o=t,e=false;for(let i=0;o&&i<p;i++){const r=o.hasAttribute("data-tflow-id")?f(o):void 0;if(r!==void 0&&o.getAttribute("data-tflow-id")===r){n.unshift(`[data-tflow-id="${r}"]`),e=true;break}const s=o.tagName.toLowerCase();if(s==="body"||s==="html"){n.unshift(s),e=true;break}const c=o,l=c.parentElement;let d=s;if(l){const u=Array.from(l.children).filter(m=>m.tagName===c.tagName);u.length>1&&(d+=`:nth-of-type(${u.indexOf(c)+1})`)}n.unshift(d),o=l}const a=n.join(" > ");return e&&a.length<=g?a:void 0}const h=t=>Number.isFinite(t)?Math.min(1,Math.max(0,t)):.5;function v(t,n,o){const e=t.getBoundingClientRect(),a={xRatio:h(e.width>0?(n-e.left)/e.width:.5),yRatio:h(e.height>0?(o-e.top)/e.height:.5)},i=f(t);i!==void 0&&(a.tflowId=i);const r=b(t);return r!==void 0&&(a.cssSelector=r),a}function A(t){return f(t)??t.tagName.toLowerCase()}function k(t){return t.closest("[data-jz-block]")!==null}function C(t){return t<768?"mobile":t<1024?"tablet":"desktop"}export{v as a,C as d,A as e,k as i};
@@ -0,0 +1,21 @@
1
+ import{t as K}from"./context-UnG6_X61.js";import{i as q}from"./anchor-cPvFFTOS.js";import{a as V,p as G}from"./upload-Fu2qvSJV.js";import{m as Se}from"./upload-Fu2qvSJV.js";import{M as I,u as J}from"./chat-BgmB0rnW.js";const R=10,j=.02,z=.01,h=(e,n,t)=>Number.isFinite(e)?Math.min(t,Math.max(n,e)):n,p=e=>Math.round(e*1e4)/1e4;function Q(e,n,t){return{x:h(t.width>0?(e-t.left)/t.width:0,0,1),y:h(t.height>0?(n-t.top)/t.height:0,0,1)}}function $(e,n,t){const i=h(n.x,0,1),a=h(n.y,0,1),r=h(t.x,0,1),s=h(t.y,0,1);return e==="arrow"?{type:e,x:p(i),y:p(a),w:p(r-i),h:p(s-a)}:{type:e,x:p(Math.min(i,r)),y:p(Math.min(a,s)),w:p(Math.abs(r-i)),h:p(Math.abs(s-a))}}function Z(e){return Math.abs(e.w)<z&&Math.abs(e.h)<z}function ee(e){return e==="arrow"?{type:e,x:.35,y:.35,w:.15,h:.15}:{type:e,x:.35,y:.4,w:.3,h:.2}}function te(e,n,t,i){const a=e.type==="arrow";if(i){const c=a?h(e.w+n,-e.x,1-e.x):h(e.w+n,z,1-e.x),u=a?h(e.h+t,-e.y,1-e.y):h(e.h+t,z,1-e.y);return{...e,w:p(c),h:p(u)}}const r=h(e.x+n,Math.max(0,-e.w),Math.min(1,1-e.w)),s=h(e.y+t,Math.max(0,-e.h),Math.min(1,1-e.h));return{...e,x:p(r),y:p(s)}}function U(e,n,t,i,a){const r=Math.atan2(i-n,t-e),s=c=>`${p(t-a*Math.cos(r+c))},${p(i-a*Math.sin(r+c))}`;return`${p(t)},${p(i)} ${s(Math.PI/7)} ${s(-Math.PI/7)}`}function ne(e,n,t,i){e.strokeStyle=e.fillStyle="#B42318",e.lineWidth=Math.max(2,Math.round(Math.max(t,i)/160));for(const a of n)if(e.beginPath(),a.type==="box"?e.rect(a.x*t,a.y*i,a.w*t,a.h*i):a.type==="circle"?e.ellipse((a.x+a.w/2)*t,(a.y+a.h/2)*i,a.w/2*t,a.h/2*i,0,0,Math.PI*2):(e.moveTo(a.x*t,a.y*i),e.lineTo((a.x+a.w)*t,(a.y+a.h)*i)),e.stroke(),a.type==="arrow"){const r=U(a.x*t,a.y*i,(a.x+a.w)*t,(a.y+a.h)*i,e.lineWidth*4).split(" ").map(s=>s.split(",").map(Number));e.beginPath(),r.forEach(([s,c],u)=>u?e.lineTo(s,c):e.moveTo(s,c)),e.fill()}}const ae="http://www.w3.org/2000/svg",B=["box","circle","arrow"];function m(e,n={},...t){const i=document.createElement(e);for(const[a,r]of Object.entries(n))i.setAttribute(a,r);return i.append(...t),i}function M(e,n){const t=document.createElementNS(ae,e);for(const[i,a]of Object.entries(n))t.setAttribute(i,String(a));return t}function ie(e,n,t){const i={fill:"none",stroke:"#B42318","stroke-width":3,"vector-effect":"non-scaling-stroke"};if(e.type==="box")return M("rect",{x:e.x*n,y:e.y*t,width:e.w*n,height:e.h*t,...i});if(e.type==="circle")return M("ellipse",{cx:(e.x+e.w/2)*n,cy:(e.y+e.h/2)*t,rx:e.w/2*n,ry:e.h/2*t,...i});const[a,r,s,c]=[e.x*n,e.y*t,(e.x+e.w)*n,(e.y+e.h)*t],u=M("g",{});return u.append(M("line",{x1:a,y1:r,x2:s,y2:c,...i}),M("polygon",{points:U(a,r,s,c,Math.max(n,t)/50),fill:"#B42318"})),u}function oe(e,n,t,i){return new Promise(a=>{let r="box",s=[],c,u;const v=B.map(o=>{const d=m("button",{type:"button",class:"tool","aria-pressed":String(o===r)},i(`draw.${o}`));return d.addEventListener("click",()=>{r=o,v.forEach((g,b)=>g.setAttribute("aria-pressed",String(B[b]===o)))}),d}),l=(o,d,g)=>{const b=m("button",{type:"button",class:d},o);return b.addEventListener("click",g),b},w=l(i("draw.undo"),"tool",()=>A(s.slice(0,-1),i("draw.removed_last"))),y=l(i("draw.clear"),"tool",()=>A([],i("draw.cleared"))),N=l(i("cancel"),"btn",()=>P(void 0)),X=l(i("draw.done"),"btn pri",()=>P(s)),T=M("svg",{viewBox:`0 0 ${t.w} ${t.h}`,preserveAspectRatio:"none","aria-hidden":"true"}),H=m("img",{src:n,alt:i("draw.image"),draggable:"false"}),W=m("span",{id:"jzd-k",class:"sr"},i("draw.keys")),f=m("div",{class:"stage",tabindex:"0",role:"group","aria-label":i("draw.stage"),"aria-describedby":"jzd-k"},H,T,W),L=m("p",{class:"msg",role:"status"}),E=m("div",{class:"jzd",role:"dialog","aria-modal":"true","aria-labelledby":"jzd-t","aria-describedby":"jzd-h"},m("header",{},m("h2",{id:"jzd-t"},i("draw.title")),m("p",{id:"jzd-h"},i("draw.hint"))),m("div",{class:"bar",role:"toolbar","aria-label":i("draw.tools")},...v,m("span",{class:"sep","aria-hidden":"true"}),w,y,m("span",{class:"end"},N,X)),m("div",{class:"wrap"},f),L);function k(){T.replaceChildren(...[...s,...u?[u]:[]].map(o=>ie(o,t.w,t.h))),w.disabled=y.disabled=s.length===0}function A(o,d){s=o,L.textContent=d,k()}function O(o){if(s.length>=R){L.textContent=i("draw.shape_limit",{max:R});return}A([...s,o],i(`draw.added.${o.type}`,{n:s.length+1,max:R}))}function S(){const o=f.parentElement,d=getComputedStyle(o),g=Math.max(1,o.clientWidth-parseFloat(d.paddingLeft||"0")-parseFloat(d.paddingRight||"0")),b=Math.max(1,o.clientHeight-parseFloat(d.paddingTop||"0")-parseFloat(d.paddingBottom||"0")),C=Math.min(g/t.w,b/t.h);f.style.width=`${Math.floor(t.w*C)}px`,f.style.height=`${Math.floor(t.h*C)}px`}const _=o=>Q(o.clientX,o.clientY,f.getBoundingClientRect());f.addEventListener("pointerdown",o=>{o.button===0&&(o.preventDefault(),f.setPointerCapture?.(o.pointerId),c=_(o))}),f.addEventListener("pointermove",o=>{c&&(u=$(r,c,_(o)),k())});const Y=o=>{if(!c)return;const d=$(r,c,_(o));c=u=void 0,Z(d)?k():O(d)};f.addEventListener("pointerup",Y),f.addEventListener("pointercancel",()=>{c=u=void 0,k()}),f.addEventListener("keydown",o=>{const d={ArrowLeft:[-j,0],ArrowRight:[j,0],ArrowUp:[0,-j],ArrowDown:[0,j]};if(o.key==="Enter"||o.key===" ")o.preventDefault(),O(ee(r));else if(d[o.key]&&s.length>0){o.preventDefault();const[g,b]=d[o.key];s=[...s.slice(0,-1),te(s[s.length-1],g,b,o.shiftKey)],k()}}),E.addEventListener("keydown",o=>{if(o.stopPropagation(),o.key==="Escape")o.preventDefault(),P(void 0);else if(o.key==="Tab"){const d=[...E.querySelectorAll("button:not([disabled]), [tabindex='0']")],g=d.indexOf(e.activeElement);(o.shiftKey?g<=0:g===d.length-1)&&(o.preventDefault(),d[o.shiftKey?d.length-1:0]?.focus())}}),window.addEventListener("resize",S);function P(o){window.removeEventListener("resize",S),E.remove(),a(o)}e.append(E),k(),S(),v[0].focus()})}const re=`
2
+ .jzd{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;background:var(--c);color:var(--i)}
3
+ .jzd header{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;padding:12px 16px;border-bottom:1px solid var(--l);background:var(--s)}
4
+ .jzd h2{margin:0;font-size:14px;font-weight:600}
5
+ .jzd header p{margin:0;font-size:12px;color:var(--m)}
6
+ .jzd .bar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:8px 16px;border-bottom:1px solid var(--l);background:var(--s)}
7
+ .jzd .bar .sep{width:1px;height:28px;background:var(--l);margin:0 4px}
8
+ .jzd .bar .end{margin-left:auto;display:flex;gap:6px}
9
+ .jzd .tool{min-width:44px;min-height:44px;padding:0 12px;border:1px solid var(--l);border-radius:6px;background:var(--s)}
10
+ .jzd .tool[aria-pressed=true]{background:var(--as);border-color:var(--a);color:var(--a);font-weight:600}
11
+ .jzd .wrap{flex:1;min-height:0;display:grid;place-items:center;padding:16px}
12
+ .jzd .stage{position:relative;border:1px solid var(--l);border-radius:6px;overflow:hidden;background:var(--r);touch-action:none;cursor:crosshair;user-select:none;-webkit-user-select:none}
13
+ .jzd .stage img,.jzd .stage svg{position:absolute;inset:0;width:100%;height:100%;display:block}
14
+ .jzd .stage svg{overflow:visible}
15
+ .jzd .stage *{pointer-events:none}
16
+ .jzd .msg{min-height:20px;margin:0;padding:0 16px 8px;font-size:12px;color:var(--m)}
17
+ .pick{position:fixed;inset:0;z-index:2147483647;cursor:crosshair;touch-action:none}
18
+ .pick .hl{position:fixed;border:2px solid var(--a);border-radius:4px;background:var(--as);pointer-events:none}
19
+ .pick .hint{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);display:flex;align-items:center;gap:8px;max-width:calc(100vw - 32px);padding:4px 4px 4px 16px;border:1px solid var(--l);border-radius:10px;background:var(--s);box-shadow:var(--sh);font-size:13px}
20
+ @media (max-width:640px){.jzd .bar .end{margin-left:0;width:100%}.jzd .bar .end .btn{flex:1}.jzd .wrap{padding:8px}}
21
+ `,D=["image/png","image/jpeg","image/webp","image/gif"],se=10*1024*1024,ce=2*1024*1024,de=1600,le="[data-journeeze], [data-journeeze-chat]",x=(e,n)=>e.session.note(e.session.t(n,{max:I}));async function ze(e,n){return pe(n),e==="point_at_it"?ge(n):n.options.reviewerToken||n.options.userId?n.session.attachments.length>=I?x(n,"note.attachment_limit"):e==="show_us_where"?he(n):xe(n):x(n,"note.signed_out")}function pe(e){if(e.root.querySelector("style[data-jz-capture]"))return;const n=document.createElement("style");n.setAttribute("data-jz-capture",""),n.textContent=re,e.options.nonce&&(n.nonce=e.options.nonce),e.root.append(n)}async function ue(e){const n=await e.config(),t=Array.isArray(n.maskSelectors)?n.maskSelectors:[],{maskSelectors:i,maskAllText:a}=e.options;return{...i?{maskSelectors:i}:{},...t.length?{configMaskSelectors:t}:{},...a?{maskAllText:a}:{}}}async function me(e){try{const n=await V(document.body,await ue(e));if(n.blob)for(const[t,i]of Object.entries(n.detectorCounts))i&&e.session.overMask.set(t,(e.session.overMask.get(t)??0)+i);return n.blob}catch{return}}async function he(e){const n=e.suspend(),t=await me(e);if(!t)return n(),x(e,"note.masking_failed");const i={w:Math.max(1,window.innerWidth),h:Math.max(1,window.innerHeight)},a=URL.createObjectURL(t),r=await oe(e.root,a,i,e.session.t);URL.revokeObjectURL(a),n(),r&&await F(e,t,i,r,"attachment.screenshot")}async function F(e,n,t,i,a){const r=await fe(n,t,i);e.session.addAttachment({id:J(),thumbnail:r,label:e.session.t(a,{n:e.session.attachments.length+1}),...i.length?{shapes:i}:{},upload:()=>G(e.endpoint,e.options,n),release:()=>URL.revokeObjectURL(r)})||(URL.revokeObjectURL(r),x(e,"note.attachment_limit"))}async function fe(e,n,t){try{const i=await createImageBitmap(e),a=document.createElement("canvas");a.width=256,a.height=Math.max(1,Math.round(256*n.h/n.w));const r=a.getContext("2d");r.drawImage(i,0,0,a.width,a.height),ne(r,t,a.width,a.height);const s=await new Promise(c=>a.toBlob(c,"image/webp",.8));if(s)return URL.createObjectURL(s)}catch{}return URL.createObjectURL(e)}async function ge(e){const n=e.suspend(),t=await be(e.root,e.session.t);if(n(),!t)return;const i=q(t)?void 0:K(t);i?e.session.setAnchor({tflowId:i}):x(e,"note.no_name")}function we(e,n){return document.elementsFromPoint(e,n).find(t=>!t.closest(le)&&t!==document.documentElement)}function be(e,n){return new Promise(t=>{const i=document.createElement("div");i.className="hl",i.hidden=true;const a=document.createElement("button");a.type="button",a.className="btn",a.textContent=n("cancel");const r=document.createElement("div");r.className="hint",r.setAttribute("role","dialog"),r.setAttribute("aria-label",n("point.label")),r.append(n("point.hint"),a);const s=document.createElement("div");s.className="pick",s.append(i,r);const c=l=>{const w=we(l.clientX,l.clientY);return w?.closest("[data-tflow-id]")??w};s.addEventListener("pointermove",l=>{const w=c(l);if(i.hidden=!w,!w)return;const y=w.getBoundingClientRect();Object.assign(i.style,{left:`${y.left}px`,top:`${y.top}px`,width:`${y.width}px`,height:`${y.height}px`})}),s.addEventListener("pointerup",l=>{r.contains(l.target)||v(c(l))}),a.addEventListener("click",()=>v(void 0));const u=l=>{l.key==="Escape"&&(l.preventDefault(),l.stopPropagation(),v(void 0))};document.addEventListener("keydown",u,true);function v(l){document.removeEventListener("keydown",u,true),s.remove(),t(l)}e.append(s),a.focus()})}function xe(e){return new Promise(n=>{const t=document.createElement("input");t.type="file",t.accept=D.join(","),t.hidden=true,e.root.append(t),t.addEventListener("change",()=>{const i=t.files?.[0];t.remove(),i?ve(e,i).then(n):n()}),t.addEventListener("cancel",()=>{t.remove(),n()}),t.click()})}async function ve(e,n){if(!D.includes(n.type)||n.size>se)return x(e,"note.file_type");if(e.session.attachments.length>=I)return x(e,"note.attachment_limit");const t=await ye(n).catch(()=>{});if(!t)return x(e,"note.file_read");await F(e,t.blob,t.size,[],"attachment.image")}async function ye(e){const n=await createImageBitmap(e,{imageOrientation:"from-image"});let t=Math.min(1,de/Math.max(n.width,n.height));for(const i of[.85,.7,.5]){const a=document.createElement("canvas");a.width=Math.max(1,Math.round(n.width*t)),a.height=Math.max(1,Math.round(n.height*t)),a.getContext("2d").drawImage(n,0,0,a.width,a.height);const r=await new Promise(s=>a.toBlob(s,"image/webp",i));if(!r||r.type!=="image/webp"&&r.type!=="image/png")return;if(r.size<=ce)return{blob:r,size:{w:a.width,h:a.height}};t*=.75}}export{D as IMAGE_TYPES,se as MAX_FILE_BYTES,ve as addFile,ue as maskOptions,me as maskedCapture,Se as maskedSnapshotSvg,we as pageElementAt,be as pick,ze as plusItem,ye as reencode};