@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.
- package/AGENTS.md +16 -3
- package/CHANGELOG.md +12 -1
- package/README.md +298 -119
- package/dist/anchor-cPvFFTOS.js +1 -0
- package/dist/attach-DpioALxW.js +21 -0
- package/dist/chat-BgmB0rnW.js +64 -0
- package/dist/consent-DckCUvYR.js +1 -0
- package/dist/contract.js +1 -1
- package/dist/feedback-CtH1a_kQ.js +1 -0
- package/dist/help-w0O5Qu-4.js +105 -0
- package/dist/hook-DGWypoEO.js +1 -0
- package/dist/index-B_nob8S0.js +1 -2091
- package/dist/{index-CfCbJJXa.js → index-CwG2LrhO.js} +3 -3
- package/dist/index.js +3 -3
- package/dist/jobs-CAXbM5-g.js +45 -0
- package/dist/loader-BZiIDD1D.js +96 -0
- package/dist/mask-audit-Du3NPoG_.js +5 -0
- package/dist/react.js +12 -11
- package/dist/resolve-BfWByuTp.js +1 -97
- package/dist/script.js +10 -9
- package/dist/signals-amjMQyOy.js +1 -0
- package/dist/telemetry-CyRJDaVs.js +1 -0
- package/dist/transport-B64CfIcF.js +1 -0
- package/dist/types/chat/capture/attach.d.ts +41 -0
- package/dist/types/chat/capture/draw.d.ts +10 -0
- package/dist/types/chat/capture/geometry.d.ts +30 -0
- package/dist/types/chat/capture/styles.d.ts +1 -0
- package/dist/types/chat/shell/chat.d.ts +27 -0
- package/dist/types/chat/shell/converse.d.ts +95 -0
- package/dist/types/chat/shell/copy.d.ts +8 -0
- package/dist/types/chat/shell/model.d.ts +36 -0
- package/dist/types/chat/shell/session.d.ts +148 -0
- package/dist/types/chat/shell/styles.d.ts +1 -0
- package/dist/types/chat/shell/view.d.ts +18 -0
- package/dist/types/chat/transport.d.ts +5 -0
- package/dist/types/config.d.ts +17 -0
- package/dist/types/help/core/controller.d.ts +12 -3
- package/dist/types/help/core/events.d.ts +24 -0
- package/dist/types/help/core/help.d.ts +1 -0
- package/dist/types/help/core/panel.d.ts +16 -0
- package/dist/types/help/core/routing.d.ts +21 -0
- package/dist/types/help/ui/jobs/jobs.d.ts +24 -0
- package/dist/types/help/ui/jobs/model.d.ts +33 -0
- package/dist/types/help/ui/jobs/styles.d.ts +1 -0
- package/dist/types/loader.d.ts +2 -0
- package/dist/types/mask-detector.d.ts +22 -0
- package/dist/types/preview/flag.d.ts +2 -0
- package/dist/types/preview/mask-audit.d.ts +29 -0
- package/dist/types/schemas/feedback.d.ts +76 -0
- package/dist/types/schemas/finding.d.ts +22 -0
- package/dist/types/schemas/index.d.ts +6 -0
- package/dist/types/schemas/turn.d.ts +26 -0
- package/dist/types/signals/signals.d.ts +7 -3
- package/dist/types/signals/steps.d.ts +33 -0
- package/dist/types/telemetry/telemetry.d.ts +10 -0
- package/dist/types/types.d.ts +72 -1
- package/dist/types/widget/capture.d.ts +12 -1
- package/dist/types/widget/upload.d.ts +14 -0
- package/dist/types/widget.d.ts +1 -21
- package/dist/upload-Fu2qvSJV.js +14 -0
- package/dist/widget-B6mR1J3P.js +118 -0
- package/package.json +4 -2
- package/dist/consent-CoKD33Rm.js +0 -170
- package/dist/feedback-C4CYtS2A.js +0 -196
- package/dist/help-C1x-y2f9.js +0 -878
- package/dist/hook-BdG4UL4k.js +0 -91
- package/dist/loader-Ba4X-WNl.js +0 -95
- package/dist/signals-E3VEdmWH.js +0 -477
- package/dist/telemetry-Cik-cYzw.js +0 -140
- package/dist/transport-B5YJFQEF.js +0 -60
- package/dist/types/signals/signals.conformance.d.ts +0 -2
- 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
|
|
14
|
-
origins
|
|
15
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
npm install @journeeze/sdk@<version>
|
|
16
|
-
```
|
|
18
|
+
## Before you start
|
|
17
19
|
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
68
|
+
function HelpButton() {
|
|
69
|
+
const jz = useJourneeze();
|
|
70
|
+
return <button onClick={() => jz.enableHelp()}>Help</button>;
|
|
71
|
+
}
|
|
42
72
|
```
|
|
43
73
|
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
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
|
-
|
|
80
|
+
**Without React:**
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
```ts
|
|
83
|
+
import { createJourneeze } from "@journeeze/sdk";
|
|
56
84
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
222
|
+
jz.disableHelp(); // removes it
|
|
105
223
|
```
|
|
106
224
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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 (
|
|
251
|
+
## Signals (optional)
|
|
135
252
|
|
|
136
|
-
Aggregate
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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};
|