@qoretechnologies/reqraft 0.10.57-pr.127.ge03fd3a → 0.10.57
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/.claude/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/design/TRACKING.md +82 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/tracking/ui/ExperimentCard.d.ts +55 -0
- package/dist/tracking/ui/ExperimentCard.d.ts.map +1 -0
- package/dist/tracking/ui/ExperimentCard.js +396 -0
- package/dist/tracking/ui/ExperimentCard.js.map +1 -0
- package/dist/tracking/ui/ExperimentsOverlay.d.ts +59 -0
- package/dist/tracking/ui/ExperimentsOverlay.d.ts.map +1 -0
- package/dist/tracking/ui/ExperimentsOverlay.js +359 -0
- package/dist/tracking/ui/ExperimentsOverlay.js.map +1 -0
- package/dist/tracking/ui/anchor.d.ts +107 -0
- package/dist/tracking/ui/anchor.d.ts.map +1 -0
- package/dist/tracking/ui/anchor.js +108 -0
- package/dist/tracking/ui/anchor.js.map +1 -0
- package/dist/tracking/ui/card.d.ts +121 -0
- package/dist/tracking/ui/card.d.ts.map +1 -0
- package/dist/tracking/ui/card.js +417 -0
- package/dist/tracking/ui/card.js.map +1 -0
- package/dist/tracking/ui/client.d.ts +42 -0
- package/dist/tracking/ui/client.d.ts.map +1 -0
- package/dist/tracking/ui/client.js +123 -0
- package/dist/tracking/ui/client.js.map +1 -0
- package/dist/tracking/ui/index.d.ts +20 -0
- package/dist/tracking/ui/index.d.ts.map +1 -0
- package/dist/tracking/ui/index.js +60 -0
- package/dist/tracking/ui/index.js.map +1 -0
- package/dist/tracking/ui/types.d.ts +120 -0
- package/dist/tracking/ui/types.d.ts.map +1 -0
- package/dist/tracking/ui/types.js +8 -0
- package/dist/tracking/ui/types.js.map +1 -0
- package/dist/tracking/ui/useExperimentsAdmin.d.ts +31 -0
- package/dist/tracking/ui/useExperimentsAdmin.d.ts.map +1 -0
- package/dist/tracking/ui/useExperimentsAdmin.js +136 -0
- package/dist/tracking/ui/useExperimentsAdmin.js.map +1 -0
- package/package.json +3 -3
- package/src/index.tsx +3 -1
- package/src/stories/tracking.tsx +206 -0
- package/src/tracking/ui/ExperimentCard.stories.tsx +296 -0
- package/src/tracking/ui/ExperimentCard.tsx +667 -0
- package/src/tracking/ui/ExperimentsOverlay.stories.tsx +155 -0
- package/src/tracking/ui/ExperimentsOverlay.tsx +437 -0
- package/src/tracking/ui/anchor.ts +180 -0
- package/src/tracking/ui/card.ts +464 -0
- package/src/tracking/ui/client.ts +117 -0
- package/src/tracking/ui/index.ts +77 -0
- package/src/tracking/ui/types.ts +110 -0
- package/src/tracking/ui/useExperimentsAdmin.ts +103 -0
- package/src/tracking/react/Experiment.stories.tsx +0 -174
package/.claude/CLAUDE.md
CHANGED
|
@@ -307,7 +307,7 @@ yarn build:test # Type-check without emit
|
|
|
307
307
|
| `src/components/form/` | Form field components |
|
|
308
308
|
| `src/components/log/` | Log display component |
|
|
309
309
|
| `src/components/menu/` | Navigation menu component |
|
|
310
|
-
| `src/tracking/` | First-party tracking, consent and A/B testing (`core/` no DOM, `browser/`, `react
|
|
310
|
+
| `src/tracking/` | First-party tracking, consent and A/B testing (`core/` no DOM, `browser/`, `react/`, and `ui/`: the A/B test card and overlay on Reqore); how-to in `design/TRACKING.md` |
|
|
311
311
|
| `src/types/` | Shared TypeScript types |
|
|
312
312
|
| `__tests__/` | Vitest unit tests (jsdom) |
|
|
313
313
|
|
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ const tracking = createBrowserTracker({
|
|
|
166
166
|
</TrackingProvider>;
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
`@qoretechnologies/reqraft/dist/tracking` loads nothing else from Reqraft; the same names are exported from the package root. Config, consent, `useExperiment`, `trackClick` / `trackSection` markup, `createMemoryTracker` for tests and the server contract: [design/TRACKING.md](design/TRACKING.md).
|
|
169
|
+
`@qoretechnologies/reqraft/dist/tracking` loads nothing else from Reqraft; the same names are exported from the package root. The A/B test UI every product shows (`<ExperimentsOverlay>`, `<ExperimentCard>`, `useExperimentsAdmin`) is in `@qoretechnologies/reqraft/dist/tracking/ui`. Config, consent, `useExperiment`, `trackClick` / `trackSection` markup, `createMemoryTracker` for tests and the server contract: [design/TRACKING.md](design/TRACKING.md).
|
|
170
170
|
|
|
171
171
|
Clients on the same LSP endpoint share ONE underlying WebSocket — each `ReqraftLspClient` is a per-document facade over the shared connection, multiplexed by document URI (the server keys language sessions per document), so N editors cost one socket. Auto-reconnect re-opens every document on the shared socket, 15s request timeout, request/response correlation by `id`, pending requests rejected on close. A `connect()` never outlives its socket: once the socket exhausts `maxReconnectTries`, a handshake still waiting rejects, and the next `connect()` — from that client or any other — dials again, so an editor opened after an outage connects once the server is back, without a page reload. Document URIs should be opaque and client-generated — per the Qonsole LSP contract they must not contain session tokens, usernames, sandbox identifiers, or other secrets that end up in server logs.
|
|
172
172
|
|
package/design/TRACKING.md
CHANGED
|
@@ -49,7 +49,7 @@ The module has three layers, and each only imports the ones above it:
|
|
|
49
49
|
| browser | `src/tracking/browser/` | the only code that touches `window` / `document`: transport (`sendBeacon` → `fetch` keepalive), storage, listeners, `IntersectionObserver` on sections, delegated clicks |
|
|
50
50
|
| react | `src/tracking/react/` | `<TrackingProvider>`, `useTracker`, `useConsent`, `useExperiment`, `<Experiment>` |
|
|
51
51
|
|
|
52
|
-
Plus `markup.ts` (`trackClick`, `trackSection`) and `testing.ts` (`createMemoryTracker`).
|
|
52
|
+
Plus `markup.ts` (`trackClick`, `trackSection`) and `testing.ts` (`createMemoryTracker`). The A/B test UI (the card and the overlay, on Reqore) is a fourth layer, `src/tracking/ui/`, with its own entry (§8); the engine never imports it.
|
|
53
53
|
|
|
54
54
|
## 3. Configure one tracker per app
|
|
55
55
|
|
|
@@ -203,8 +203,88 @@ Options: `config` (any config field; `property` defaults to `test`), `consent`,
|
|
|
203
203
|
`start` (the clock), `seed` (localStorage before the tracker starts). In a story, pass
|
|
204
204
|
`m.tracker` to `<TrackingProvider>` without `attach`; seed the experiments cache
|
|
205
205
|
(`KEYS.experimentsCache`) so the first render already has the definitions — see
|
|
206
|
-
`src/tracking/
|
|
206
|
+
`src/tracking/ui/ExperimentsOverlay.stories.tsx`.
|
|
207
207
|
|
|
208
208
|
Unit tests: `__tests__/tracking/` (hashing vectors, assignment, consent gating, batching,
|
|
209
209
|
sections, page views, sign-up links, concluded winners, the in-app switch, product config,
|
|
210
210
|
the React bindings, and the layer rules).
|
|
211
|
+
|
|
212
|
+
## 8. The A/B test UI: embed the overlay
|
|
213
|
+
|
|
214
|
+
The same A/B test controls every product shows, built on Reqore, in their own entry so the engine
|
|
215
|
+
stays light:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { ExperimentsOverlay, ExperimentCard, useExperimentsAdmin } from '@qoretechnologies/reqraft/dist/tracking/ui';
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
| Export | What |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `<ExperimentsOverlay>` | Finds every `<Experiment>` on the page (its `data-experiment` marker) and draws a dashed outline and an "A/B test · <name>" handle at its corner. Hover, keyboard focus or a click opens the test's card, anchored to the element: it scrolls and resizes with it and flips above or below the handle to stay in view. ✕, Esc and a click outside close it. An ended test shows "Winner live · code clean-up pending" instead. |
|
|
224
|
+
| `<ExperimentCard>` | One test: name, status tag, a "?" help and ✕; the verdict; one row per version, best first (conversion on the primary metric, then visitors; re-sorted only when new data arrives), the best one tagged "Leading", or "Winner" for a likely winner or an ended test's winner (a success-coloured glow and wash on its row, stronger for a winner; static, nothing animates), the original tagged "Original". Tags use Reqore's `appearance="soft"` (reqore 0.77.2+); a long title stays on one line with an ellipsis. A row shows visitors, conversion, chance to beat the original and a bar; the shown version has an info-coloured ring and wash (`aria-current`; the row itself takes no intent, so its actions stay neutral); the whole row shows that version (Enter / Space too). Row actions: a neutral, minimal "Show analytics" for that version (`versionDetailsUrl`), minimal Accept, and a bin icon (Remove) at the far end. Footer: the optional "Open in …" link (hidden while the test is not set up; just its icon in a card narrower than 400 px) and Start / Resume (solid), Pause, Stop. Past three and a half rows the list scrolls inside the card. Every action asks first (Reqore's confirm dialog) and notifies what happened. |
|
|
225
|
+
| `useExperimentsAdmin(options)` | Optional: the list and the actions from the qorus-api routes. |
|
|
226
|
+
| `createExperimentsAdminClient(options)` | The same without React. |
|
|
227
|
+
| `placeCard`, `cardReducer`, `versionRows`, `verdictLine`, `versionsMaxHeight`, … | The plain logic behind them, for tests and custom UIs. |
|
|
228
|
+
|
|
229
|
+
Both components are driven by props only: the data is the qorus-api editor / admin list (an array of
|
|
230
|
+
`{ experiment, results }`: the admin experiment object with `status`, `variants`, `phase`,
|
|
231
|
+
`phases`, `history`, `winner`, and the results object with `verdict_text` and the per-metric
|
|
232
|
+
numbers), and every action is a callback that may return a promise (the button shows busy until it
|
|
233
|
+
settles, and a rejection's message is shown). An action whose callback is not given does not
|
|
234
|
+
appear. Neither makes a request of its own.
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
import { ExperimentsOverlay, useExperimentsAdmin } from '@qoretechnologies/reqraft/dist/tracking/ui';
|
|
238
|
+
|
|
239
|
+
const AbTestsLayer = ({ visible }: { visible: boolean }) => {
|
|
240
|
+
const admin = useExperimentsAdmin({
|
|
241
|
+
listUrl: '/qorus-cloud/editor/experiments?property=ide', // actions go to …/experiments/<key>/<action>
|
|
242
|
+
headers: { Authorization: `Bearer ${editorToken}` }, // or leave it to a proxy
|
|
243
|
+
enabled: visible,
|
|
244
|
+
});
|
|
245
|
+
return (
|
|
246
|
+
<ExperimentsOverlay
|
|
247
|
+
visible={visible} // the product's "hide editor elements"
|
|
248
|
+
tests={admin.tests}
|
|
249
|
+
loadError={admin.error}
|
|
250
|
+
onAccept={admin.accept}
|
|
251
|
+
onRemove={admin.remove}
|
|
252
|
+
onStart={admin.start}
|
|
253
|
+
onPause={admin.pause}
|
|
254
|
+
onStop={admin.stop}
|
|
255
|
+
detailsUrl={(key) => `${ADMIN_PORTAL}/analytics?dashboard=tracking&experiment=${key}`}
|
|
256
|
+
actorLabels={{ 'ide-editor': 'in the IDE' }}
|
|
257
|
+
topInset={() => document.querySelector('header')?.getBoundingClientRect().bottom ?? 0}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- Mount it inside `<TrackingProvider>`: then clicking a version's row uses the tracker's in-app
|
|
264
|
+
switch (`tracker.experiments.setEditorOverride`, this browser only, never logged), and after
|
|
265
|
+
an action the page shows what visitors see now (the switch is cleared and the tracker reloads
|
|
266
|
+
the definitions). Pass `onShow` / `onSettled` to do something else.
|
|
267
|
+
- `versionDetailsUrl={(key, variant) => \`${ADMIN_PORTAL}/analytics?dashboard=tracking&experiment=${key}&variant=${variant}\`}`
|
|
268
|
+
adds the per-version "Show analytics" action (the admin portal opens the test with that version
|
|
269
|
+
expanded); without it the action is hidden. `detailsUrl` stays for the whole test.
|
|
270
|
+
- The "?" opens a dialog explaining the test for a marketer: every button, how visitors are split
|
|
271
|
+
and counted, reading the verdict and the chance to beat, the too-early rule (the test's
|
|
272
|
+
`min_sample`, 200 visitors per version and 7 days by default), a test that is not set up, and
|
|
273
|
+
where the full analytics are (`analyticsLabel`).
|
|
274
|
+
- A test that is in the page's code (`<Experiment>`) but not registered in the analytics service
|
|
275
|
+
yet shows "Not set up" and asks to have it set up (`notSetUpText` changes the words); when the list
|
|
276
|
+
cannot be loaded (`loadError`) it shows "No connection" instead.
|
|
277
|
+
- `exclude` (a selector) skips markers inside the product's own UI (e.g. its preview frames);
|
|
278
|
+
markers inside the overlay itself are always skipped.
|
|
279
|
+
- The same overlay works inside a phone-preview iframe: mount it in the frame's page too. The
|
|
280
|
+
switch is shared through `localStorage`, so both follow it.
|
|
281
|
+
- `useExperimentsAdmin` reloads the list every minute (`refreshMs`) and after every action; its
|
|
282
|
+
errors explain a 401 / 403 as a missing or wrong key. `actionUrl` and `createUrl` change the
|
|
283
|
+
routes; `fetch` replaces the transport.
|
|
284
|
+
- The card on its own: `<ExperimentCard experimentKey="signup-cta" test={entry} shown="a" codeVariants={['a', 'b']} onShow={…} onAccept={…} … />`.
|
|
285
|
+
|
|
286
|
+
Stories: `Tracking/Experiment Card` (draft, running with two and five versions, paused, concluded,
|
|
287
|
+
no data yet, not set up, no connection, the confirm step on Remove, the help) and
|
|
288
|
+
`Tracking/Experiments Overlay` (anchored, flipped above near the bottom, hidden), each in dark,
|
|
289
|
+
light and phone. The visually empty ones (hidden, the phone flip) run as tests without a Qlip
|
|
290
|
+
snapshot (`parameters.qlip.skip`).
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,4BAA4B,EAC5B,eAAe,EACf,gCAAgC,GACjC,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,YAAY,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACxE,cAAc,mCAAmC,CAAC;AAClD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,kBAAkB,GACnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,gBAAgB,IAAI,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC/F,YAAY,EACV,YAAY,EACZ,iBAAiB,GAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,WAAW,EACX,KAAK,EACL,QAAQ,EACR,cAAc,EACd,eAAe,EACf,UAAU,GACX,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,wBAAwB,EACxB,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,wBAAwB,EACxB,YAAY,EACZ,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,4BAA4B,EAC5B,eAAe,EACf,gCAAgC,GACjC,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,YAAY,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACxE,cAAc,mCAAmC,CAAC;AAClD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,kBAAkB,GACnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,gBAAgB,IAAI,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC/F,YAAY,EACV,YAAY,EACZ,iBAAiB,GAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,WAAW,EACX,KAAK,EACL,QAAQ,EACR,cAAc,EACd,eAAe,EACf,UAAU,GACX,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,wBAAwB,EACxB,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,wBAAwB,EACxB,YAAY,EACZ,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AAItC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,8 @@ __exportStar(require("./components/ticketReferences"), exports);
|
|
|
62
62
|
__exportStar(require("./components/searchFilterBar"), exports);
|
|
63
63
|
__exportStar(require("./components/codeSize"), exports);
|
|
64
64
|
// First-party tracking, consent and A/B testing (design/TRACKING.md). Also importable
|
|
65
|
-
// on its own, without the rest of Reqraft: `@qoretechnologies/reqraft/dist/tracking
|
|
65
|
+
// on its own, without the rest of Reqraft: `@qoretechnologies/reqraft/dist/tracking`
|
|
66
|
+
// (the engine, React only) and `@qoretechnologies/reqraft/dist/tracking/ui` (the A/B test UI).
|
|
66
67
|
__exportStar(require("./tracking"), exports);
|
|
68
|
+
__exportStar(require("./tracking/ui"), exports);
|
|
67
69
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,oDAAkC;AAClC,uDAAqC;AACrC,qDAS+B;AAR7B,4GAAA,aAAa,OAAA;AACb,sHAAA,uBAAuB,OAAA;AACvB,yGAAA,UAAU,OAAA;AACV,+GAAA,gBAAgB,OAAA;AAChB,6GAAA,cAAc,OAAA;AACd,2HAAA,4BAA4B,OAAA;AAC5B,8GAAA,eAAe,OAAA;AACf,+HAAA,gCAAgC,OAAA;AAGlC,iDAAoD;AAA3C,0GAAA,aAAa,OAAA;AACtB,6DAA2C;AAC3C,+CAAqD;AAA5C,mGAAA,WAAW,OAAA;AAQpB,sDAAqD;AAA5C,oGAAA,QAAQ,OAAA;AAEjB,4DAAkE;AAAzD,+GAAA,iBAAiB,OAAA;AAE1B,oEAAkD;AAClD,+DAIqC;AAHnC,oHAAA,iBAAiB,OAAA;AACjB,kHAAA,eAAe,OAAA;AACf,qHAAA,kBAAkB,OAAA;AAEpB,gEAA+F;AAAtF,sHAAA,gBAAgB,OAA2B;AAKpD,uCAOuB;AANrB,oGAAA,WAAW,OAAA;AACX,8FAAA,KAAK,OAAA;AACL,iGAAA,QAAQ,OAAA;AACR,uGAAA,cAAc,OAAA;AACd,wGAAA,eAAe,OAAA;AACf,mGAAA,UAAU,OAAA;AAQZ,oDAAkC;AAClC,+CAAqD;AAA5C,6GAAA,gBAAgB,OAAA;AAiBzB,wDAAsC;AACtC,2DAAyC;AACzC,0DAAwC;AACxC,iEAA+C;AAC/C,6DAA2C;AAC3C,4DAA0C;AAC1C,gEAA8C;AAC9C,+DAA6C;AAC7C,wDAAsC;AACtC,sFAAsF;AACtF,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,oDAAkC;AAClC,uDAAqC;AACrC,qDAS+B;AAR7B,4GAAA,aAAa,OAAA;AACb,sHAAA,uBAAuB,OAAA;AACvB,yGAAA,UAAU,OAAA;AACV,+GAAA,gBAAgB,OAAA;AAChB,6GAAA,cAAc,OAAA;AACd,2HAAA,4BAA4B,OAAA;AAC5B,8GAAA,eAAe,OAAA;AACf,+HAAA,gCAAgC,OAAA;AAGlC,iDAAoD;AAA3C,0GAAA,aAAa,OAAA;AACtB,6DAA2C;AAC3C,+CAAqD;AAA5C,mGAAA,WAAW,OAAA;AAQpB,sDAAqD;AAA5C,oGAAA,QAAQ,OAAA;AAEjB,4DAAkE;AAAzD,+GAAA,iBAAiB,OAAA;AAE1B,oEAAkD;AAClD,+DAIqC;AAHnC,oHAAA,iBAAiB,OAAA;AACjB,kHAAA,eAAe,OAAA;AACf,qHAAA,kBAAkB,OAAA;AAEpB,gEAA+F;AAAtF,sHAAA,gBAAgB,OAA2B;AAKpD,uCAOuB;AANrB,oGAAA,WAAW,OAAA;AACX,8FAAA,KAAK,OAAA;AACL,iGAAA,QAAQ,OAAA;AACR,uGAAA,cAAc,OAAA;AACd,wGAAA,eAAe,OAAA;AACf,mGAAA,UAAU,OAAA;AAQZ,oDAAkC;AAClC,+CAAqD;AAA5C,6GAAA,gBAAgB,OAAA;AAiBzB,wDAAsC;AACtC,2DAAyC;AACzC,0DAAwC;AACxC,iEAA+C;AAC/C,6DAA2C;AAC3C,4DAA0C;AAC1C,gEAA8C;AAC9C,+DAA6C;AAC7C,wDAAsC;AACtC,sFAAsF;AACtF,qFAAqF;AACrF,+FAA+F;AAC/F,6CAA2B;AAC3B,gDAA8B"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { IExperimentEntry, TExperimentActionResult } from './types';
|
|
2
|
+
type TMaybeAsync<T = unknown> = T | Promise<T>;
|
|
3
|
+
export interface IExperimentCardProps {
|
|
4
|
+
/** The experiment key (`data-experiment` of `<Experiment>`). */
|
|
5
|
+
experimentKey: string;
|
|
6
|
+
/** The server's experiment and its results; absent while the test is not on the server. */
|
|
7
|
+
test?: IExperimentEntry | null;
|
|
8
|
+
/** The card's title. Default: the experiment's name, else its key. */
|
|
9
|
+
name?: string;
|
|
10
|
+
/** The version this page shows now: its row takes the info colour (and `aria-current`). */
|
|
11
|
+
shown?: string;
|
|
12
|
+
/** The versions the page's code renders (`data-variants`); a server version outside it cannot be shown. */
|
|
13
|
+
codeVariants?: string[];
|
|
14
|
+
/** The room the card has (px): the versions scroll inside when it is short. */
|
|
15
|
+
maxHeight?: number;
|
|
16
|
+
/** Why the list could not be loaded (the analytics service cannot be reached). */
|
|
17
|
+
loadError?: string | null;
|
|
18
|
+
/** An "Open in …" link at the start of the footer (a new tab). */
|
|
19
|
+
detailsUrl?: string | null;
|
|
20
|
+
/** The link's label. Default `Open in admin portal`. */
|
|
21
|
+
detailsLabel?: string;
|
|
22
|
+
/** One version's analytics (a new tab): a "Show analytics" action on each row; none without it. */
|
|
23
|
+
versionDetailsUrl?: (variant: string) => string | null | undefined;
|
|
24
|
+
/** Where the full analytics live, in the help's words. Default `the admin portal`. */
|
|
25
|
+
analyticsLabel?: string;
|
|
26
|
+
/** Best first (conversion on the primary metric, then visitors). Default true. */
|
|
27
|
+
sort?: boolean;
|
|
28
|
+
/** The words for a test that is in the page's code but not set up in the analytics service yet. */
|
|
29
|
+
notSetUpText?: string;
|
|
30
|
+
/** Who removed a version, in words, by the history's `actor` (`{ 'landing-editor': 'in the editor' }`). */
|
|
31
|
+
actorLabels?: Record<string, string>;
|
|
32
|
+
/** Asks before Accept, Remove, Start, Pause and Stop (Reqore's confirm dialog). Default true. */
|
|
33
|
+
confirm?: boolean;
|
|
34
|
+
/** A notification after each action (what happened, or the server's error). Default true. */
|
|
35
|
+
notify?: boolean;
|
|
36
|
+
/** A click (or Enter / Space) on a version's row: show that version on this page. Rows are not clickable without it. */
|
|
37
|
+
onShow?: (variant: string) => void;
|
|
38
|
+
/** Each action appears only when its callback is given; a returned promise shows the button busy until it settles. */
|
|
39
|
+
onAccept?: (variant: string) => TMaybeAsync<TExperimentActionResult | unknown>;
|
|
40
|
+
onRemove?: (variant: string) => TMaybeAsync<TExperimentActionResult | unknown>;
|
|
41
|
+
onStart?: () => TMaybeAsync;
|
|
42
|
+
onPause?: () => TMaybeAsync;
|
|
43
|
+
onStop?: () => TMaybeAsync;
|
|
44
|
+
/** The ✕ in the header. */
|
|
45
|
+
onClose?: () => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One A/B test: its name and status, the verdict in plain words, one row per version (visitors,
|
|
49
|
+
* conversion, chance to beat the original, a bar; click a row to show that version; Accept, and
|
|
50
|
+
* Remove at the far end), and the test's own controls (Start / Resume, Pause, Stop) with an
|
|
51
|
+
* optional link to its details. Driven only by props: no requests of its own.
|
|
52
|
+
*/
|
|
53
|
+
export declare const ExperimentCard: ({ experimentKey, test, name, shown, codeVariants, maxHeight, loadError, detailsUrl, detailsLabel, versionDetailsUrl, analyticsLabel, sort, notSetUpText, actorLabels, confirm, notify, onShow, onAccept, onRemove, onStart, onPause, onStop, onClose, }: IExperimentCardProps) => import("react/jsx-runtime").JSX.Element;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=ExperimentCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExperimentCard.d.ts","sourceRoot":"","sources":["../../../src/tracking/ui/ExperimentCard.tsx"],"names":[],"mappings":"AAkDA,OAAO,KAAK,EAAE,gBAAgB,EAAqB,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAE5F,KAAK,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAE/C,MAAM,WAAW,oBAAoB;IACnC,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/B,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACnE,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mGAAmG;IACnG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2GAA2G;IAC3G,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,iGAAiG;IACjG,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wHAAwH;IACxH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,sHAAsH;IACtH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,WAAW,CAAC,uBAAuB,GAAG,OAAO,CAAC,CAAC;IAC/E,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,WAAW,CAAC,uBAAuB,GAAG,OAAO,CAAC,CAAC;IAC/E,OAAO,CAAC,EAAE,MAAM,WAAW,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,WAAW,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,WAAW,CAAC;IAC3B,2BAA2B;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAoGD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,4PAwBxB,oBAAoB,4CAubtB,CAAC"}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
|
3
|
+
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
|
4
|
+
return cooked;
|
|
5
|
+
};
|
|
6
|
+
var __assign = (this && this.__assign) || function () {
|
|
7
|
+
__assign = Object.assign || function(t) {
|
|
8
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
9
|
+
s = arguments[i];
|
|
10
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
11
|
+
t[p] = s[p];
|
|
12
|
+
}
|
|
13
|
+
return t;
|
|
14
|
+
};
|
|
15
|
+
return __assign.apply(this, arguments);
|
|
16
|
+
};
|
|
17
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
18
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
19
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
20
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
21
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
22
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
23
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
27
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
28
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
29
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
30
|
+
function step(op) {
|
|
31
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
32
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
33
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
34
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
35
|
+
switch (op[0]) {
|
|
36
|
+
case 0: case 1: t = op; break;
|
|
37
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
38
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
39
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
40
|
+
default:
|
|
41
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
42
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
43
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
44
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
45
|
+
if (t[2]) _.ops.pop();
|
|
46
|
+
_.trys.pop(); continue;
|
|
47
|
+
}
|
|
48
|
+
op = body.call(thisArg, _);
|
|
49
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
50
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
54
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
55
|
+
};
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.ExperimentCard = void 0;
|
|
58
|
+
var jsx_runtime_1 = require("react/jsx-runtime");
|
|
59
|
+
var reqore_1 = require("@qoretechnologies/reqore");
|
|
60
|
+
var sizes_1 = require("@qoretechnologies/reqore/dist/constants/sizes");
|
|
61
|
+
var polished_1 = require("polished");
|
|
62
|
+
var react_1 = require("react");
|
|
63
|
+
var styled_components_1 = __importDefault(require("styled-components"));
|
|
64
|
+
var anchor_1 = require("./anchor");
|
|
65
|
+
var card_1 = require("./card");
|
|
66
|
+
// Soft tags: an outlined pill over a wash of its colour, readable on dark and light themes.
|
|
67
|
+
var soft = function (intent) { return ({ intent: intent, appearance: 'soft' }); };
|
|
68
|
+
var ROW_GAP = 6;
|
|
69
|
+
/** Below this width (the versions list, px) the footer's link drops its words for its icon. */
|
|
70
|
+
var NARROW_CARD = 400;
|
|
71
|
+
/** Room around the rows for the leader's glow, taken from the card's own padding. */
|
|
72
|
+
var GLOW_ROOM = 6;
|
|
73
|
+
/**
|
|
74
|
+
* How a version's row stands out, drawn around and over the row without taking the pointer:
|
|
75
|
+
*
|
|
76
|
+
* - the version this page shows: an inset ring and a flat wash in the info colour (the row itself
|
|
77
|
+
* keeps no intent, so its actions stay neutral);
|
|
78
|
+
* - the leading version: a ring and a soft glow in the success colour, and a wash from the left;
|
|
79
|
+
* - the winner (a likely-winner verdict or an ended test's winner): the same, clearly stronger.
|
|
80
|
+
*
|
|
81
|
+
* Static (nothing animates, so there is nothing for reduced motion to stop), and in the theme's
|
|
82
|
+
* own intent colours, so it reads on dark and light.
|
|
83
|
+
*/
|
|
84
|
+
var StyledVersion = styled_components_1.default.div(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n position: relative;\n border-radius: ", "px;\n box-shadow: ", ";\n\n &::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n /* The shown version's ring sits over the row (an inset shadow on the wrapper would be\n painted under the row's own background). */\n box-shadow: ", ";\n background: ", ";\n }\n"], ["\n position: relative;\n border-radius: ", "px;\n box-shadow: ", ";\n\n &::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n /* The shown version's ring sits over the row (an inset shadow on the wrapper would be\n painted under the row's own background). */\n box-shadow: ", ";\n background: ", ";\n }\n"])), function (_a) {
|
|
85
|
+
var $radius = _a.$radius;
|
|
86
|
+
return $radius;
|
|
87
|
+
}, function (_a) {
|
|
88
|
+
var $highlight = _a.$highlight, $success = _a.$success;
|
|
89
|
+
return [
|
|
90
|
+
$highlight === 'winner' ?
|
|
91
|
+
"0 0 0 1px ".concat((0, polished_1.rgba)($success, 0.95), ", 0 0 18px ").concat((0, polished_1.rgba)($success, 0.4))
|
|
92
|
+
: null,
|
|
93
|
+
$highlight === 'leading' ?
|
|
94
|
+
"0 0 0 1px ".concat((0, polished_1.rgba)($success, 0.5), ", 0 0 10px ").concat((0, polished_1.rgba)($success, 0.18))
|
|
95
|
+
: null,
|
|
96
|
+
]
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(', ') || 'none';
|
|
99
|
+
}, function (_a) {
|
|
100
|
+
var $shown = _a.$shown, $info = _a.$info;
|
|
101
|
+
return ($shown ? "inset 0 0 0 1px ".concat((0, polished_1.rgba)($info, 0.85)) : 'none');
|
|
102
|
+
}, function (_a) {
|
|
103
|
+
var $highlight = _a.$highlight, $shown = _a.$shown, $success = _a.$success, $info = _a.$info;
|
|
104
|
+
return [
|
|
105
|
+
$highlight ?
|
|
106
|
+
"linear-gradient(100deg, ".concat((0, polished_1.rgba)($success, $highlight === 'winner' ? 0.26 : 0.13), " 0%, ").concat((0, polished_1.rgba)($success, $highlight === 'winner' ? 0.08 : 0.04), " 45%, ").concat((0, polished_1.rgba)($success, 0), " 75%)")
|
|
107
|
+
: null,
|
|
108
|
+
$shown ? "linear-gradient(".concat((0, polished_1.rgba)($info, 0.1), ", ").concat((0, polished_1.rgba)($info, 0.1), ")") : null,
|
|
109
|
+
]
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join(', ') || 'none';
|
|
112
|
+
});
|
|
113
|
+
// Layout only: the versions scroll inside the card past three and a half rows.
|
|
114
|
+
var StyledVersions = styled_components_1.default.div(templateObject_2 || (templateObject_2 = __makeTemplateObject(["\n display: flex;\n flex-direction: column;\n gap: ", "px;\n max-height: ", ";\n overflow-y: ", ";\n overflow-x: hidden;\n overscroll-behavior: contain;\n min-width: 0;\n /* Room for the leader's glow, taken back from the card's padding so the rows keep their width. */\n margin: 0 -", "px;\n padding: ", "px;\n\n /* The rows take the card's width, never their content's, padding included: a host page\n without a global border-box reset would otherwise push each row past the card's edge. */\n > * {\n min-width: 0;\n }\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n"], ["\n display: flex;\n flex-direction: column;\n gap: ", "px;\n max-height: ", ";\n overflow-y: ", ";\n overflow-x: hidden;\n overscroll-behavior: contain;\n min-width: 0;\n /* Room for the leader's glow, taken back from the card's padding so the rows keep their width. */\n margin: 0 -", "px;\n padding: ", "px;\n\n /* The rows take the card's width, never their content's, padding included: a host page\n without a global border-box reset would otherwise push each row past the card's edge. */\n > * {\n min-width: 0;\n }\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n"])), ROW_GAP, function (_a) {
|
|
115
|
+
var $max = _a.$max;
|
|
116
|
+
return ($max ? "".concat($max, "px") : 'none');
|
|
117
|
+
}, function (_a) {
|
|
118
|
+
var $max = _a.$max;
|
|
119
|
+
return ($max ? 'auto' : 'visible');
|
|
120
|
+
}, GLOW_ROOM, GLOW_ROOM);
|
|
121
|
+
/** Header, verdict and footer take about this much of `maxHeight`; the versions get the rest. */
|
|
122
|
+
var CHROME_HEIGHT = 230;
|
|
123
|
+
/**
|
|
124
|
+
* One A/B test: its name and status, the verdict in plain words, one row per version (visitors,
|
|
125
|
+
* conversion, chance to beat the original, a bar; click a row to show that version; Accept, and
|
|
126
|
+
* Remove at the far end), and the test's own controls (Start / Resume, Pause, Stop) with an
|
|
127
|
+
* optional link to its details. Driven only by props: no requests of its own.
|
|
128
|
+
*/
|
|
129
|
+
var ExperimentCard = function (_a) {
|
|
130
|
+
var experimentKey = _a.experimentKey, test = _a.test, name = _a.name, shown = _a.shown, _b = _a.codeVariants, codeVariants = _b === void 0 ? [] : _b, maxHeight = _a.maxHeight, loadError = _a.loadError, detailsUrl = _a.detailsUrl, _c = _a.detailsLabel, detailsLabel = _c === void 0 ? 'Open in admin portal' : _c, versionDetailsUrl = _a.versionDetailsUrl, _d = _a.analyticsLabel, analyticsLabel = _d === void 0 ? 'the admin portal' : _d, _e = _a.sort, sort = _e === void 0 ? true : _e, notSetUpText = _a.notSetUpText, actorLabels = _a.actorLabels, _f = _a.confirm, confirm = _f === void 0 ? true : _f, _g = _a.notify, notify = _g === void 0 ? true : _g, onShow = _a.onShow, onAccept = _a.onAccept, onRemove = _a.onRemove, onStart = _a.onStart, onPause = _a.onPause, onStop = _a.onStop, onClose = _a.onClose;
|
|
131
|
+
var entry = test !== null && test !== void 0 ? test : undefined;
|
|
132
|
+
var confirmAction = (0, reqore_1.useReqoreProperty)('confirmAction');
|
|
133
|
+
var addNotification = (0, reqore_1.useReqoreProperty)('addNotification');
|
|
134
|
+
var theme = (0, reqore_1.useReqoreTheme)();
|
|
135
|
+
// The card's own width, not the window's: it lives in a narrow overlay on a wide page.
|
|
136
|
+
var _h = (0, react_1.useState)(false), narrow = _h[0], setNarrow = _h[1];
|
|
137
|
+
var versionsList = (0, react_1.useRef)(null);
|
|
138
|
+
(0, react_1.useEffect)(function () {
|
|
139
|
+
var el = versionsList.current;
|
|
140
|
+
if (!el || typeof ResizeObserver === 'undefined')
|
|
141
|
+
return undefined;
|
|
142
|
+
var ro = new ResizeObserver(function () { return setNarrow(el.offsetWidth < NARROW_CARD); });
|
|
143
|
+
ro.observe(el);
|
|
144
|
+
return function () { return ro.disconnect(); };
|
|
145
|
+
}, []);
|
|
146
|
+
var light = (function () {
|
|
147
|
+
try {
|
|
148
|
+
return (0, polished_1.getLuminance)(theme.main) > 0.5;
|
|
149
|
+
}
|
|
150
|
+
catch (_a) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
var _j = (0, react_1.useState)(null), busy = _j[0], setBusy = _j[1];
|
|
155
|
+
var _k = (0, react_1.useState)(false), helpOpen = _k[0], setHelpOpen = _k[1];
|
|
156
|
+
var _l = (0, react_1.useState)(0), rowHeight = _l[0], setRowHeight = _l[1];
|
|
157
|
+
var firstRow = (0, react_1.useRef)(null);
|
|
158
|
+
var mounted = (0, react_1.useRef)(true);
|
|
159
|
+
(0, react_1.useEffect)(function () { return function () {
|
|
160
|
+
mounted.current = false;
|
|
161
|
+
}; }, []);
|
|
162
|
+
var unreachable = !entry && !!loadError;
|
|
163
|
+
var codeKey = codeVariants.join(',');
|
|
164
|
+
// The order is decided when the data arrives (a refresh), never while the user works in the card.
|
|
165
|
+
var order = (0, react_1.useMemo)(function () {
|
|
166
|
+
var all = (0, card_1.versionRows)(entry, codeVariants);
|
|
167
|
+
return (sort ? (0, card_1.sortVersionRows)(all) : all).map(function (r) { return r.key; });
|
|
168
|
+
// `codeVariants` is followed by its keys, not its identity.
|
|
169
|
+
}, [entry, codeKey, sort]);
|
|
170
|
+
var unsorted = (0, card_1.versionRows)(entry, codeVariants);
|
|
171
|
+
var rows = order
|
|
172
|
+
.map(function (key) { return unsorted.find(function (r) { return r.key === key; }); })
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.concat(unsorted.filter(function (r) { return !order.includes(r.key); }));
|
|
175
|
+
var leader = (0, card_1.leadingVersion)(entry, rows);
|
|
176
|
+
var bars = (0, card_1.barValues)(rows);
|
|
177
|
+
var metric = (0, card_1.primaryMetric)(entry === null || entry === void 0 ? void 0 : entry.results);
|
|
178
|
+
var status = entry === null || entry === void 0 ? void 0 : entry.experiment.status;
|
|
179
|
+
var title = name || (entry === null || entry === void 0 ? void 0 : entry.experiment.name) || experimentKey;
|
|
180
|
+
var tag = (0, card_1.statusTag)(entry, unreachable);
|
|
181
|
+
var note = (0, card_1.phaseNote)(entry, actorLabels);
|
|
182
|
+
var busyOn = function (action, variant) {
|
|
183
|
+
if (variant === void 0) { variant = ''; }
|
|
184
|
+
return busy === "".concat(action, ":").concat(variant);
|
|
185
|
+
};
|
|
186
|
+
(0, react_1.useLayoutEffect)(function () {
|
|
187
|
+
var _a, _b;
|
|
188
|
+
var h = (_b = (_a = firstRow.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) !== null && _b !== void 0 ? _b : 0;
|
|
189
|
+
if (h && h !== rowHeight)
|
|
190
|
+
setRowHeight(h);
|
|
191
|
+
});
|
|
192
|
+
// A row's height changes with the card's width (a phone, a resized window): measure it again.
|
|
193
|
+
(0, react_1.useEffect)(function () {
|
|
194
|
+
var el = firstRow.current;
|
|
195
|
+
if (!el || typeof ResizeObserver === 'undefined')
|
|
196
|
+
return undefined;
|
|
197
|
+
var ro = new ResizeObserver(function () { return setRowHeight(el.offsetHeight); });
|
|
198
|
+
ro.observe(el);
|
|
199
|
+
return function () { return ro.disconnect(); };
|
|
200
|
+
}, []);
|
|
201
|
+
// Minimal buttons tint their text from the intent, too pale on a light theme: a deeper shade there.
|
|
202
|
+
var quiet = function (intent) { return (__assign({ minimal: true }, (light && intent ?
|
|
203
|
+
{
|
|
204
|
+
effect: { color: "".concat(intent, ":darken:2"), weight: 'thick' },
|
|
205
|
+
iconColor: "".concat(intent, ":darken:2"),
|
|
206
|
+
}
|
|
207
|
+
: {}))); };
|
|
208
|
+
var act = function (action, variant) {
|
|
209
|
+
if (!entry)
|
|
210
|
+
return;
|
|
211
|
+
var callbacks = {
|
|
212
|
+
accept: onAccept,
|
|
213
|
+
remove: onRemove,
|
|
214
|
+
start: onStart,
|
|
215
|
+
pause: onPause,
|
|
216
|
+
stop: onStop,
|
|
217
|
+
};
|
|
218
|
+
var callback = callbacks[action];
|
|
219
|
+
if (!callback)
|
|
220
|
+
return;
|
|
221
|
+
var isVersion = action === 'accept' || action === 'remove';
|
|
222
|
+
var copy = isVersion ? (0, card_1.confirmCopy)(action, entry, variant) : (0, card_1.lifecycleCopy)(action, entry);
|
|
223
|
+
var titles = {
|
|
224
|
+
accept: ['Winner accepted', 'Could not accept'],
|
|
225
|
+
remove: ['Version removed', 'Could not remove'],
|
|
226
|
+
start: [status === 'paused' ? 'Test resumed' : 'Test started', 'Could not start'],
|
|
227
|
+
pause: ['Test paused', 'Could not pause'],
|
|
228
|
+
stop: ['Test stopped', 'Could not stop'],
|
|
229
|
+
};
|
|
230
|
+
var run = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
231
|
+
var result, content, err_1;
|
|
232
|
+
var _a;
|
|
233
|
+
return __generator(this, function (_b) {
|
|
234
|
+
switch (_b.label) {
|
|
235
|
+
case 0:
|
|
236
|
+
setBusy("".concat(action, ":").concat(variant !== null && variant !== void 0 ? variant : ''));
|
|
237
|
+
_b.label = 1;
|
|
238
|
+
case 1:
|
|
239
|
+
_b.trys.push([1, 3, 4, 5]);
|
|
240
|
+
return [4 /*yield*/, callback(variant)];
|
|
241
|
+
case 2:
|
|
242
|
+
result = _b.sent();
|
|
243
|
+
if (notify) {
|
|
244
|
+
content = isVersion ?
|
|
245
|
+
(0, card_1.actionOutcome)(action, result, variant)
|
|
246
|
+
: "\u201C".concat(title, "\u201D is ").concat(action === 'start' ? 'running'
|
|
247
|
+
: action === 'pause' ? 'paused'
|
|
248
|
+
: 'stopped', ".");
|
|
249
|
+
addNotification === null || addNotification === void 0 ? void 0 : addNotification({
|
|
250
|
+
intent: 'success',
|
|
251
|
+
title: titles[action][0],
|
|
252
|
+
content: content,
|
|
253
|
+
duration: 8000,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return [3 /*break*/, 5];
|
|
257
|
+
case 3:
|
|
258
|
+
err_1 = _b.sent();
|
|
259
|
+
if (notify) {
|
|
260
|
+
addNotification === null || addNotification === void 0 ? void 0 : addNotification({
|
|
261
|
+
intent: 'danger',
|
|
262
|
+
title: titles[action][1],
|
|
263
|
+
content: (_a = err_1 === null || err_1 === void 0 ? void 0 : err_1.message) !== null && _a !== void 0 ? _a : String(err_1),
|
|
264
|
+
duration: 10000,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return [3 /*break*/, 5];
|
|
268
|
+
case 4:
|
|
269
|
+
if (mounted.current)
|
|
270
|
+
setBusy(null);
|
|
271
|
+
return [7 /*endfinally*/];
|
|
272
|
+
case 5: return [2 /*return*/];
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}); };
|
|
276
|
+
if (confirm && confirmAction) {
|
|
277
|
+
confirmAction({
|
|
278
|
+
title: copy.title,
|
|
279
|
+
description: copy.description,
|
|
280
|
+
confirmLabel: copy.confirmLabel,
|
|
281
|
+
confirmButtonIntent: action === 'accept' || action === 'start' ? 'success'
|
|
282
|
+
: action === 'pause' ? 'warning'
|
|
283
|
+
: 'danger',
|
|
284
|
+
onConfirm: function () { return void run(); },
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
void run();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
var footer = [];
|
|
292
|
+
// A test the analytics service does not know has no analytics to open.
|
|
293
|
+
if (detailsUrl && entry) {
|
|
294
|
+
footer.push(__assign(__assign({ position: 'left', icon: 'ExternalLinkLine',
|
|
295
|
+
// In a narrow card it is the icon alone (with its tooltip and name), so the footer fits one row.
|
|
296
|
+
label: narrow ? undefined : detailsLabel, 'aria-label': detailsLabel, responsive: false }, quiet()), { tooltip: 'This test’s details (a new tab)', onClick: function () { return window.open(detailsUrl, '_blank', 'noopener'); } }));
|
|
297
|
+
}
|
|
298
|
+
if (entry && status) {
|
|
299
|
+
// Every button in the card is minimal, except Start test / Resume: the one primary action.
|
|
300
|
+
if (onStart && (0, card_1.canStart)(status)) {
|
|
301
|
+
footer.push({
|
|
302
|
+
position: 'right',
|
|
303
|
+
icon: 'PlayLine',
|
|
304
|
+
intent: 'success',
|
|
305
|
+
label: status === 'paused' ? 'Resume' : 'Start test',
|
|
306
|
+
responsive: false,
|
|
307
|
+
disabled: !!busy,
|
|
308
|
+
loading: busyOn('start'),
|
|
309
|
+
tooltip: status === 'paused' ?
|
|
310
|
+
'Split visitors between the versions again'
|
|
311
|
+
: 'Start splitting visitors between the versions and counting',
|
|
312
|
+
onClick: function () { return act('start'); },
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if (onPause && (0, card_1.canPause)(status)) {
|
|
316
|
+
footer.push(__assign(__assign({ position: 'right', icon: 'PauseLine', intent: 'warning', label: 'Pause', responsive: false }, quiet('warning')), { disabled: !!busy, loading: busyOn('pause'), tooltip: 'Everybody sees the original until you resume', onClick: function () { return act('pause'); } }));
|
|
317
|
+
}
|
|
318
|
+
if (onStop && (0, card_1.canStop)(status)) {
|
|
319
|
+
footer.push(__assign(__assign({ position: 'right', icon: 'StopLine', intent: 'danger', label: 'Stop', responsive: false }, quiet('danger')), { disabled: !!busy, loading: busyOn('stop'), tooltip: 'End the test without a winner', onClick: function () { return act('stop'); } }));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
var accept = status ? (0, card_1.canAccept)(status) : false;
|
|
323
|
+
var remove = status ? (0, card_1.canRemove)(status, rows.length) : false;
|
|
324
|
+
var listMax = (0, card_1.versionsMaxHeight)({
|
|
325
|
+
rows: rows.length,
|
|
326
|
+
rowHeight: rowHeight,
|
|
327
|
+
gap: ROW_GAP,
|
|
328
|
+
available: maxHeight ? Math.max(rowHeight, maxHeight - CHROME_HEIGHT) : undefined,
|
|
329
|
+
});
|
|
330
|
+
return ((0, jsx_runtime_1.jsxs)(reqore_1.ReqorePanel, { className: 'reqraft-experiment-card', label: title, showLabelTooltip: true, labelMaxLines: 1, icon: 'FlaskLine', badge: __assign({ label: tag.label, icon: tag.icon }, soft(tag.intent)), size: 'small', rounded: true, flat: false, padded: true, collapsible: false, onClose: onClose, closeTooltip: 'Close (Esc)', closeButtonProps: { 'aria-label': 'Close the test card', minimal: true }, actions: [
|
|
331
|
+
{
|
|
332
|
+
icon: 'QuestionLine',
|
|
333
|
+
minimal: true,
|
|
334
|
+
flat: true,
|
|
335
|
+
tooltip: 'How A/B tests work',
|
|
336
|
+
'aria-label': 'How A/B tests work',
|
|
337
|
+
onClick: function () { return setHelpOpen(true); },
|
|
338
|
+
},
|
|
339
|
+
], bottomActions: footer.length ? footer : undefined, children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreCallout, { size: 'small', flat: true, icon: 'LineChartLine', intent: unreachable ? 'warning' : (0, card_1.verdictIntent)(entry), effect: { weight: 500 }, children: (0, card_1.verdictLine)(entry, { unreachable: unreachable, notSetUpText: notSetUpText }) }), note && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 6 }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreP, { size: 'small', effect: { opacity: 0.7 }, children: note })] })), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 10 }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreP, { size: 'small', effect: { opacity: 0.75, weight: 500 }, children: "".concat(rows.length, " versions").concat((metric === null || metric === void 0 ? void 0 : metric.name) ? " \u00B7 goal: \u201C".concat(metric.name, "\u201D") : '') }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 6 }), (0, jsx_runtime_1.jsx)(StyledVersions, { ref: versionsList, "$max": listMax, role: 'list', "aria-label": 'Versions', className: 'reqraft-experiment-versions', children: rows.map(function (r, i) {
|
|
340
|
+
var _a, _b;
|
|
341
|
+
var isShown = r.key === shown;
|
|
342
|
+
var highlight = (leader === null || leader === void 0 ? void 0 : leader.key) === r.key ? leader.kind : undefined;
|
|
343
|
+
var badges = [];
|
|
344
|
+
if ((leader === null || leader === void 0 ? void 0 : leader.key) === r.key) {
|
|
345
|
+
badges.push(leader.kind === 'winner' ? __assign({ label: 'Winner', icon: 'TrophyLine' }, soft('success')) : __assign({ label: 'Leading', icon: 'ArrowUpLine' }, soft('success')));
|
|
346
|
+
}
|
|
347
|
+
if (r.control)
|
|
348
|
+
badges.push(__assign({ label: 'Original', icon: 'HomeLine' }, soft('muted')));
|
|
349
|
+
if (!r.inCode)
|
|
350
|
+
badges.push(__assign({ label: 'Not in the code', icon: 'AlertLine' }, soft('warning')));
|
|
351
|
+
// The whole row shows its version; Accept and Remove stop the click (Reqore's row actions do).
|
|
352
|
+
var showable = !!onShow && r.inCode && !isShown;
|
|
353
|
+
var actions = [];
|
|
354
|
+
var analytics = entry ? versionDetailsUrl === null || versionDetailsUrl === void 0 ? void 0 : versionDetailsUrl(r.key) : null;
|
|
355
|
+
if (analytics) {
|
|
356
|
+
actions.push(__assign(__assign({ size: 'small', icon: 'BarChartBoxLine', label: 'Show analytics' }, quiet()), { tooltip: "Show analytics for \u201C".concat(r.name, "\u201D (a new tab)"), 'aria-label': "Show analytics for ".concat(r.name), onClick: function () { return window.open(analytics, '_blank', 'noopener'); } }));
|
|
357
|
+
}
|
|
358
|
+
if (entry && onAccept) {
|
|
359
|
+
actions.push(__assign(__assign({ size: 'small', icon: 'TrophyLine', intent: 'success', label: 'Accept' }, quiet('success')), { disabled: !accept || !!busy, loading: busyOn('accept', r.key), tooltip: accept ?
|
|
360
|
+
"End the test: every visitor sees \u201C".concat(r.name, "\u201D")
|
|
361
|
+
: 'Only a started test can be accepted', onClick: function () { return act('accept', r.key); } }));
|
|
362
|
+
}
|
|
363
|
+
if (entry && onRemove) {
|
|
364
|
+
actions.push(__assign(__assign({ size: 'small', icon: 'DeleteBinLine', intent: 'danger' }, quiet('danger')), { 'aria-label': "Remove version ".concat(r.name),
|
|
365
|
+
// At the far end of the row, away from Accept, so it is not clicked by accident.
|
|
366
|
+
style: { marginLeft: 'auto' }, disabled: !remove || !!busy, loading: busyOn('remove', r.key), tooltip: remove ?
|
|
367
|
+
"Remove version: drop \u201C".concat(r.name, "\u201D from the test")
|
|
368
|
+
: 'This version cannot be removed now', onClick: function () { return act('remove', r.key); } }));
|
|
369
|
+
}
|
|
370
|
+
return ((0, jsx_runtime_1.jsx)(StyledVersion, { role: 'listitem', ref: i === 0 ? firstRow : undefined, "data-version": r.key, "data-highlight": highlight, "aria-current": isShown || undefined, "$highlight": highlight, "$shown": isShown, "$success": (_a = theme.intents) === null || _a === void 0 ? void 0 : _a.success, "$info": (_b = theme.intents) === null || _b === void 0 ? void 0 : _b.info, "$radius": sizes_1.RADIUS_FROM_SIZE.small, children: (0, jsx_runtime_1.jsx)(reqore_1.ReqoreEntityRow, __assign({ size: 'small', rounded: true, flat: true, label: r.name, badge: badges, description: (0, card_1.numbersLine)(r, metric === null || metric === void 0 ? void 0 : metric.name, status), metadata: r.visitors ?
|
|
371
|
+
(0, jsx_runtime_1.jsx)(reqore_1.ReqoreProgress, { value: bars[i], size: 'tiny', rounded: true, flat: true, style: { width: '100%', minWidth: 160 }, intent: isShown ? 'info'
|
|
372
|
+
: r.control ?
|
|
373
|
+
'muted'
|
|
374
|
+
: 'success', "aria-label": "".concat(r.name, ": conversion compared with the best version") })
|
|
375
|
+
: undefined, iconHasBackground: false, showIcon: false, actions: actions.length ? actions : undefined }, (showable ?
|
|
376
|
+
{
|
|
377
|
+
role: 'button',
|
|
378
|
+
tabIndex: 0,
|
|
379
|
+
'aria-label': "Show \u201C".concat(r.name, "\u201D on this page"),
|
|
380
|
+
tooltip: "Show \u201C".concat(r.name, "\u201D on this page (only here, never counted)"),
|
|
381
|
+
onClick: function () { return onShow(r.key); },
|
|
382
|
+
onKeyDown: function (e) {
|
|
383
|
+
if (!(0, anchor_1.activatesRow)(e.key, e.target === e.currentTarget))
|
|
384
|
+
return;
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
onShow(r.key);
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
: {}))) }, r.key));
|
|
390
|
+
}) }), !entry && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 8 }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreP, { size: 'small', effect: { opacity: 0.7 }, children: unreachable ?
|
|
391
|
+
'Start, Accept and Remove come back once the analytics service answers.'
|
|
392
|
+
: 'Start, Accept and Remove appear once the test is set up.' })] })), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreModal, { isOpen: helpOpen, onClose: function () { return setHelpOpen(false); }, label: 'How A/B tests work', icon: 'QuestionLine', width: '640px', blur: 2, className: 'reqraft-experiment-help', children: (0, card_1.helpSections)(entry, analyticsLabel).map(function (section) { return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreH4, { children: section.title }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 4 }), section.paragraphs.map(function (text) { return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreP, { size: 'small', effect: { weight: 500 }, children: text }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 6 })] }, text)); }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 14 })] }, section.title)); }) })] }));
|
|
393
|
+
};
|
|
394
|
+
exports.ExperimentCard = ExperimentCard;
|
|
395
|
+
var templateObject_1, templateObject_2;
|
|
396
|
+
//# sourceMappingURL=ExperimentCard.js.map
|