@posthog/twig-components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/build-assets.mjs +18 -0
- package/dist/AiLab.d.ts +26 -0
- package/dist/AiLab.js +140 -0
- package/dist/BookingLab.d.ts +19 -0
- package/dist/BookingLab.js +100 -0
- package/dist/BrowseStaysPreview.d.ts +7 -0
- package/dist/BrowseStaysPreview.js +6 -0
- package/dist/FilterInspector.d.ts +7 -0
- package/dist/FilterInspector.js +27 -0
- package/dist/FilterLabExercise.d.ts +14 -0
- package/dist/FilterLabExercise.js +78 -0
- package/dist/FinishLabButton.d.ts +3 -0
- package/dist/FinishLabButton.js +5 -0
- package/dist/LabChecklist.d.ts +8 -0
- package/dist/LabChecklist.js +6 -0
- package/dist/LabChoices.d.ts +10 -0
- package/dist/LabChoices.js +7 -0
- package/dist/LabCompletionView.d.ts +8 -0
- package/dist/LabCompletionView.js +43 -0
- package/dist/LabNavigation.d.ts +9 -0
- package/dist/LabNavigation.js +7 -0
- package/dist/PlaygroundController.d.ts +31 -0
- package/dist/PlaygroundController.js +82 -0
- package/dist/PlaygroundPanels.d.ts +32 -0
- package/dist/PlaygroundPanels.js +33 -0
- package/dist/ReplayLab.d.ts +28 -0
- package/dist/ReplayLab.js +70 -0
- package/dist/ReplayRecorder.d.ts +28 -0
- package/dist/ReplayRecorder.js +357 -0
- package/dist/SessionPlayer.d.ts +5 -0
- package/dist/SessionPlayer.js +124 -0
- package/dist/StayCard.d.ts +7 -0
- package/dist/StayCard.js +5 -0
- package/dist/StayFilters.d.ts +14 -0
- package/dist/StayFilters.js +7 -0
- package/dist/StayLab.d.ts +16 -0
- package/dist/StayLab.js +47 -0
- package/dist/ai-lab.d.ts +55 -0
- package/dist/ai-lab.js +130 -0
- package/dist/assets/Halfre.ttf +0 -0
- package/dist/assets/RoundHog-Medium.woff2 +0 -0
- package/dist/assets/RoundHog-SemiBold.woff2 +0 -0
- package/dist/assets/RoundHog.woff2 +0 -0
- package/dist/assets/cabin.jpg +0 -0
- package/dist/assets/cliff.png +0 -0
- package/dist/assets/logo.svg +4 -0
- package/dist/booking-lab.d.ts +53 -0
- package/dist/booking-lab.js +134 -0
- package/dist/catalog.css +259 -0
- package/dist/catalog.d.ts +57 -0
- package/dist/catalog.js +111 -0
- package/dist/filter-lab.d.ts +62 -0
- package/dist/filter-lab.js +133 -0
- package/dist/lab.css +774 -0
- package/dist/playground.d.ts +167 -0
- package/dist/playground.js +91 -0
- package/dist/replay-lab.d.ts +19 -0
- package/dist/replay-lab.js +26 -0
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +1 -0
- package/dist/stay-lab.d.ts +32 -0
- package/dist/stay-lab.js +53 -0
- package/dist/trip-dates.d.ts +11 -0
- package/dist/trip-dates.js +49 -0
- package/docs/components.md +78 -0
- package/docs/integration.md +59 -0
- package/docs/workbench.md +37 -0
- package/package.json +241 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export declare function LabChoices<T extends string | number>({ label, value, options, onChange, }: {
|
|
3
|
+
label: ReactNode;
|
|
4
|
+
value: T;
|
|
5
|
+
options: {
|
|
6
|
+
value: T;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
}[];
|
|
9
|
+
onChange: (value: T) => void;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
export function LabChoices({ label, value, options, onChange, }) {
|
|
5
|
+
const name = useId();
|
|
6
|
+
return (_jsxs("fieldset", { className: "vac-source-picker", children: [_jsx("legend", { children: label }), _jsx("div", { className: "vac-lab-choice-list", children: options.map((option) => (_jsxs("label", { "data-selected": value === option.value, children: [_jsx("input", { type: "radio", name: name, checked: value === option.value, onChange: () => onChange(option.value) }), _jsx("span", { children: option.label })] }, option.value))) })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import type { TouchpointId } from "./playground.js";
|
|
3
|
+
export declare function LabCompletionView({ lab, onChoose, onReview, headingRef, }: {
|
|
4
|
+
lab: TouchpointId;
|
|
5
|
+
onChoose: () => void;
|
|
6
|
+
onReview: () => void;
|
|
7
|
+
headingRef: RefObject<HTMLHeadingElement | null>;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
const recaps = {
|
|
4
|
+
catalog: {
|
|
5
|
+
title: "You recorded the correct setting for each click.",
|
|
6
|
+
points: [
|
|
7
|
+
"Found why a fixed value mislabels filter clicks.",
|
|
8
|
+
"Recorded each filter’s actual setting and checked the results.",
|
|
9
|
+
],
|
|
10
|
+
},
|
|
11
|
+
discovery: {
|
|
12
|
+
title: "You captured both successful and failed AI requests.",
|
|
13
|
+
points: [
|
|
14
|
+
"Connected a request to the model’s response.",
|
|
15
|
+
"Recorded a failed request so the error is visible too.",
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
booking: {
|
|
19
|
+
title: "You counted confirmed bookings, not just clicks.",
|
|
20
|
+
points: [
|
|
21
|
+
"Followed a successful booking from attempt to confirmation.",
|
|
22
|
+
"Checked that a failed request does not count as a completed booking.",
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
stay: {
|
|
26
|
+
title: "You connected each view event to the right stay.",
|
|
27
|
+
points: [
|
|
28
|
+
"Saw why identical view events cannot distinguish stays.",
|
|
29
|
+
"Grouped view events by their listing ID.",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
replay: {
|
|
33
|
+
title: "You’ve captured a visitor’s journey.",
|
|
34
|
+
points: [
|
|
35
|
+
"Followed a ghost visitor across Twig.",
|
|
36
|
+
"Recorded your own visit and opened its replay with your masking setting.",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export function LabCompletionView({ lab, onChoose, onReview, headingRef, }) {
|
|
41
|
+
const recap = recaps[lab];
|
|
42
|
+
return (_jsxs("section", { className: "vac-lab vac-lab-completion", "aria-label": "Lab complete", children: [_jsxs("h3", { className: "vac-completion-title", ref: headingRef, tabIndex: -1, children: [_jsx("span", { className: "vac-completion-check", "aria-hidden": "true", children: "\u2713" }), _jsx("span", { children: "Lab complete!" })] }), _jsx("p", { children: recap.title }), _jsxs("div", { className: "vac-guide-example", children: [_jsx("h4", { children: "What you learned" }), _jsx("ul", { children: recap.points.map((point) => (_jsx("li", { children: point }, point))) })] }), _jsx("p", { children: "You\u2019re done with this lab. Your results are still here if you want another look." }), _jsx("button", { type: "button", className: "vac-button", onClick: onChoose, children: _jsx("span", { className: "vac-os-button-face", children: "Choose another lab" }) }), _jsx("button", { type: "button", className: "vac-text-button", onClick: onReview, children: "Review results" })] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TouchpointId } from "./playground.js";
|
|
2
|
+
export declare function LabNavigationView({ selected, stage, embedded, onAllLabs, onPrevious, onReset, }: {
|
|
3
|
+
selected: TouchpointId | null;
|
|
4
|
+
stage: number;
|
|
5
|
+
embedded: boolean;
|
|
6
|
+
onAllLabs: () => void;
|
|
7
|
+
onPrevious: () => void;
|
|
8
|
+
onReset: () => void;
|
|
9
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
export function LabNavigationView({ selected, stage, embedded, onAllLabs, onPrevious, onReset, }) {
|
|
4
|
+
if (!selected)
|
|
5
|
+
return null;
|
|
6
|
+
return (_jsxs("div", { className: "vac-lab-navigation", children: [!embedded && (_jsx("button", { className: "vac-text-button", onClick: onAllLabs, children: "All labs" })), ["replay", "catalog", "discovery", "booking", "stay"].includes(selected) && (_jsxs(_Fragment, { children: [stage > 0 && (!embedded || stage > 1) && (_jsxs("button", { className: "vac-text-button", title: "Go back without clearing your results", onClick: onPrevious, children: [_jsx("span", { "aria-hidden": "true", children: "\u2190 " }), "Previous step"] })), _jsx("button", { className: "vac-text-button vac-lab-reset", title: "Clear this lab\u2019s results and restore its defaults", onClick: onReset, children: embedded ? "Reset exercise" : "Reset lab" })] }))] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type LabAction, type LabState } from "./filter-lab.js";
|
|
3
|
+
import type { TouchpointId } from "./playground.js";
|
|
4
|
+
export declare function PlaygroundLabProvider({ children, active, bookingEnabled, initialSource, }: {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
active: boolean;
|
|
7
|
+
bookingEnabled: boolean;
|
|
8
|
+
initialSource?: "fixed" | "clicked";
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
export declare function usePlaygroundLab(): {
|
|
11
|
+
embedSource?: "fixed" | "clicked";
|
|
12
|
+
state: LabState;
|
|
13
|
+
dispatch: (action: LabAction) => void;
|
|
14
|
+
active: boolean;
|
|
15
|
+
stage: number;
|
|
16
|
+
setStage: (stage: number) => void;
|
|
17
|
+
selected: TouchpointId | null;
|
|
18
|
+
choose: (id: TouchpointId | null) => void;
|
|
19
|
+
highlighted: TouchpointId | null;
|
|
20
|
+
highlight: (id: TouchpointId | null) => void;
|
|
21
|
+
};
|
|
22
|
+
export declare function useFilterLab(): {
|
|
23
|
+
selected: "stay" | "discovery" | "catalog" | "replay" | "booking" | null;
|
|
24
|
+
choose: (id: TouchpointId | null) => void;
|
|
25
|
+
highlighted: "stay" | "discovery" | "catalog" | "replay" | "booking" | null;
|
|
26
|
+
highlight: (id: TouchpointId | null) => void;
|
|
27
|
+
active: boolean;
|
|
28
|
+
selecting: boolean;
|
|
29
|
+
stage: number;
|
|
30
|
+
recordFilter: (setting: string) => boolean;
|
|
31
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useReducer, useState, } from "react";
|
|
4
|
+
import { AiLabProvider } from "./AiLab.js";
|
|
5
|
+
import { BookingLabProvider } from "./BookingLab.js";
|
|
6
|
+
import { useStayLab } from "./StayLab.js";
|
|
7
|
+
import { useReplay } from "./ReplayRecorder.js";
|
|
8
|
+
import { filterLabReducer, initialLabState } from "./filter-lab.js";
|
|
9
|
+
const PlaygroundLabContext = createContext({
|
|
10
|
+
state: initialLabState,
|
|
11
|
+
dispatch: () => { },
|
|
12
|
+
active: false,
|
|
13
|
+
stage: 0,
|
|
14
|
+
setStage: () => { },
|
|
15
|
+
selected: null,
|
|
16
|
+
choose: () => { },
|
|
17
|
+
highlighted: null,
|
|
18
|
+
highlight: () => { },
|
|
19
|
+
});
|
|
20
|
+
export function PlaygroundLabProvider({ children, active, bookingEnabled, initialSource, }) {
|
|
21
|
+
const replay = useReplay();
|
|
22
|
+
const stayLab = useStayLab();
|
|
23
|
+
const resumeStay = bookingEnabled && stayLab.state.running;
|
|
24
|
+
const [state, dispatch] = useReducer(filterLabReducer, initialLabState);
|
|
25
|
+
const [stage, setStage] = useState(initialSource || replay.mode || resumeStay ? 1 : 0);
|
|
26
|
+
const [selected, setSelected] = useState(initialSource
|
|
27
|
+
? "catalog"
|
|
28
|
+
: replay.mode
|
|
29
|
+
? "replay"
|
|
30
|
+
: resumeStay
|
|
31
|
+
? "stay"
|
|
32
|
+
: null);
|
|
33
|
+
const [highlighted, highlight] = useState(null);
|
|
34
|
+
function choose(id) {
|
|
35
|
+
if (initialSource)
|
|
36
|
+
return;
|
|
37
|
+
if (id !== "stay")
|
|
38
|
+
stayLab.dispatch({ type: "pause" });
|
|
39
|
+
if (id !== "replay")
|
|
40
|
+
replay.stop();
|
|
41
|
+
setSelected(id);
|
|
42
|
+
setStage(0);
|
|
43
|
+
dispatch({ type: "cancel" });
|
|
44
|
+
}
|
|
45
|
+
return (_jsx(PlaygroundLabContext.Provider, { value: {
|
|
46
|
+
embedSource: initialSource,
|
|
47
|
+
state,
|
|
48
|
+
dispatch,
|
|
49
|
+
active: active && selected === "catalog" && stage > 0,
|
|
50
|
+
stage,
|
|
51
|
+
setStage,
|
|
52
|
+
selected,
|
|
53
|
+
choose,
|
|
54
|
+
highlighted,
|
|
55
|
+
highlight,
|
|
56
|
+
}, children: _jsx(AiLabProvider, { active: active && selected === "discovery" && stage > 0, children: _jsx(BookingLabProvider, { active: bookingEnabled && selected === "booking" && stage > 0, children: children }) }) }));
|
|
57
|
+
}
|
|
58
|
+
export function usePlaygroundLab() {
|
|
59
|
+
return useContext(PlaygroundLabContext);
|
|
60
|
+
}
|
|
61
|
+
export function useFilterLab() {
|
|
62
|
+
const { state, dispatch, active, stage, selected, choose, highlighted, highlight, } = useContext(PlaygroundLabContext);
|
|
63
|
+
return {
|
|
64
|
+
selected,
|
|
65
|
+
choose,
|
|
66
|
+
highlighted,
|
|
67
|
+
highlight,
|
|
68
|
+
active: active && (state.selecting || !!state.applied),
|
|
69
|
+
selecting: active && state.selecting,
|
|
70
|
+
stage,
|
|
71
|
+
recordFilter: (setting) => {
|
|
72
|
+
if (!active)
|
|
73
|
+
return false;
|
|
74
|
+
if (state.selecting) {
|
|
75
|
+
dispatch({ type: "target" });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
dispatch({ type: "filter", setting });
|
|
79
|
+
return false;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReactNode, type RefObject } from "react";
|
|
2
|
+
import type { PlaygroundPage, TouchpointId } from "./playground.js";
|
|
3
|
+
export declare function LabDirectory({ page, choose, showMarker }: {
|
|
4
|
+
page: PlaygroundPage;
|
|
5
|
+
choose: (id: TouchpointId) => void;
|
|
6
|
+
showMarker: (id: TouchpointId) => void;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare function SelectedLabHeader({ current }: {
|
|
9
|
+
current: PlaygroundPage["touchpoints"][number];
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
export declare function PlaygroundMarker({ touchpoint, highlighted, onHighlight, onChoose, }: {
|
|
12
|
+
touchpoint: PlaygroundPage["touchpoints"][number];
|
|
13
|
+
highlighted: boolean;
|
|
14
|
+
onHighlight: (id: TouchpointId | null) => void;
|
|
15
|
+
onChoose: (id: TouchpointId) => void;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export declare function PlaygroundInvitation({ dismissed, onDismiss, onOpen, toggleRef, }: {
|
|
18
|
+
dismissed: boolean;
|
|
19
|
+
onDismiss: () => void;
|
|
20
|
+
onOpen: () => void;
|
|
21
|
+
toggleRef: RefObject<HTMLButtonElement | null>;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
export declare function PlaygroundDock({ embedded, onClose, headingRef, logo, navigation, recordingBar, pageKey, guide, }: {
|
|
24
|
+
embedded: boolean;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
headingRef: RefObject<HTMLHeadingElement | null>;
|
|
27
|
+
logo: ReactNode;
|
|
28
|
+
navigation: ReactNode;
|
|
29
|
+
recordingBar: ReactNode;
|
|
30
|
+
pageKey: string;
|
|
31
|
+
guide: ReactNode;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
export function LabDirectory({ page, choose, showMarker }) {
|
|
5
|
+
const [query, setQuery] = useState("");
|
|
6
|
+
const visible = page.touchpoints.filter((item) => `${item.title} ${item.tool}`.toLowerCase().includes(query.toLowerCase()));
|
|
7
|
+
return (_jsxs("section", { id: "twig-playground-detail", className: "vac-lab vac-lab-directory", children: [_jsx("h3", { children: "Choose a lab" }), _jsx("p", { children: "Different interactions on this page use different PostHog tools. Choose an interaction below to learn how to instrument it." }), page.touchpoints.length > 4 && (_jsxs("label", { className: "vac-lab-search", children: ["Find a lab or PostHog tool", _jsx("input", { value: query, onChange: (event) => setQuery(event.target.value), type: "search" })] })), _jsx("ol", { className: "vac-lab-directory-list", children: visible.map((item) => (_jsxs("li", { "data-lab-color": item.color, children: [_jsxs("div", { className: "vac-lab-card-heading", children: [_jsx("span", { className: "vac-marker", children: item.number }), _jsx("span", { className: "vac-eyebrow", children: item.tool })] }), _jsx("h4", { children: item.title }), _jsx("p", { children: item.lesson }), _jsxs("div", { className: "vac-lab-card-actions", children: [_jsx("button", { className: "vac-text-button", type: "button", onClick: () => showMarker(item.id), "aria-label": `Show me where: ${item.title}`, children: "Show me where" }), !item.lab && (_jsx("span", { className: "vac-muted", children: "Lab not built yet" })), _jsx("button", { className: "vac-button", onClick: () => {
|
|
8
|
+
choose(item.id);
|
|
9
|
+
showMarker(item.id);
|
|
10
|
+
}, "aria-label": `${item.lab ? "Start lab" : "View details"}: ${item.title}`, children: _jsx("span", { className: "vac-os-button-face", children: item.lab ? "Start lab →" : "View details" }) })] })] }, item.id))) }), !visible.length && (_jsx("p", { children: page.touchpoints.length
|
|
11
|
+
? "No labs match your search."
|
|
12
|
+
: "No instrumentation points are defined for this page yet." }))] }));
|
|
13
|
+
}
|
|
14
|
+
export function SelectedLabHeader({ current }) {
|
|
15
|
+
return (_jsxs("div", { className: "vac-selected-lab", children: [_jsx("span", { className: "vac-eyebrow", children: current.tool === "Product analytics"
|
|
16
|
+
? "Product Analytics"
|
|
17
|
+
: current.tool }), _jsxs("span", { className: "vac-lab-context", children: [current.title, " Lab"] })] }));
|
|
18
|
+
}
|
|
19
|
+
export function PlaygroundMarker({ touchpoint, highlighted, onHighlight, onChoose, }) {
|
|
20
|
+
return (_jsx("button", { id: `twig-marker-${touchpoint.id}`, className: "vac-marker", "data-lab-color": touchpoint.color, "data-highlighted": highlighted, onMouseEnter: () => onHighlight(touchpoint.id), onMouseLeave: () => onHighlight(null), onFocus: () => onHighlight(touchpoint.id), onBlur: () => onHighlight(null), type: "button", onClick: () => onChoose(touchpoint.id), "aria-label": `Open lab: ${touchpoint.title}`, "aria-controls": "twig-playground-detail", children: touchpoint.number }));
|
|
21
|
+
}
|
|
22
|
+
export function PlaygroundInvitation({ dismissed, onDismiss, onOpen, toggleRef, }) {
|
|
23
|
+
return (_jsxs("aside", { className: `vac-playground-invite${dismissed ? " vac-invite-compact" : ""}`, "aria-label": "Explore PostHog", onKeyDown: (event) => {
|
|
24
|
+
if (event.key === "Escape")
|
|
25
|
+
onDismiss();
|
|
26
|
+
}, children: [!dismissed && (_jsxs(_Fragment, { children: [_jsx("button", { className: "vac-invite-dismiss", type: "button", onClick: onDismiss, "aria-label": "Dismiss playground invitation", children: "\u00D7" }), _jsx("span", { className: "vac-eyebrow", children: "Interactive playground" }), _jsx("h2", { children: "Want to explore the PostHog instrumentation?" }), _jsx("p", { children: "Build an event, try it on Twig, and see what happens." })] })), _jsx("button", { ref: toggleRef, className: "vac-button", type: "button", "aria-expanded": false, onClick: onOpen, children: dismissed ? "Explore PostHog ↗" : "Open playground ↗" })] }));
|
|
27
|
+
}
|
|
28
|
+
export function PlaygroundDock({ embedded, onClose, headingRef, logo, navigation, recordingBar, pageKey, guide, }) {
|
|
29
|
+
return (_jsxs("aside", { id: "twig-playground", className: "vac-dock vac-developer-theme", "aria-label": "Twig playground", onKeyDown: (event) => {
|
|
30
|
+
if (event.key === "Escape" && !embedded)
|
|
31
|
+
onClose();
|
|
32
|
+
}, children: [_jsxs("div", { className: "vac-dock-header", children: [_jsxs("div", { className: "vac-row", children: [_jsxs("h2", { id: "twig-playground-heading", ref: headingRef, tabIndex: -1, children: [logo, "PostHog Playground"] }), !embedded && (_jsx("button", { className: "vac-text-button", onClick: onClose, children: "Hide" }))] }), navigation, recordingBar] }), _jsx("div", { className: "vac-dock-scroll", role: "region", "aria-label": "Instrumentation touchpoints and details", tabIndex: 0, children: guide }, pageKey)] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ReplayFrame } from "./replay-lab.js";
|
|
3
|
+
export type ReplayLabViewState = {
|
|
4
|
+
exercise: "ghost" | "manual";
|
|
5
|
+
recording: readonly unknown[];
|
|
6
|
+
starting: boolean;
|
|
7
|
+
frames: ReplayFrame[];
|
|
8
|
+
mode: "ghost" | "manual" | null;
|
|
9
|
+
masked: boolean;
|
|
10
|
+
setMasked: (value: boolean) => void;
|
|
11
|
+
capturedMasked: boolean;
|
|
12
|
+
message: string;
|
|
13
|
+
start: (mode: "ghost" | "manual") => void;
|
|
14
|
+
stop: () => void;
|
|
15
|
+
clear: () => void;
|
|
16
|
+
setExercise: (value: "ghost" | "manual") => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function ReplayRecordingBar({ replay }: {
|
|
19
|
+
replay: ReplayLabViewState;
|
|
20
|
+
}): import("react").JSX.Element | null;
|
|
21
|
+
export declare function ReplayLab({ stage, onStage, replay, pathname, returnHome, sessionPlayer, }: {
|
|
22
|
+
stage: number;
|
|
23
|
+
onStage: (stage: number) => void;
|
|
24
|
+
replay: ReplayLabViewState;
|
|
25
|
+
pathname: string;
|
|
26
|
+
returnHome: ReactNode;
|
|
27
|
+
sessionPlayer: ReactNode;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { FinishLabButton } from "./FinishLabButton.js";
|
|
5
|
+
export function ReplayRecordingBar({ replay }) {
|
|
6
|
+
if (!replay.mode)
|
|
7
|
+
return null;
|
|
8
|
+
return (_jsxs("div", { className: "vac-replay-recording", role: "status", children: [_jsx("span", { children: replay.mode === "ghost"
|
|
9
|
+
? "Ghost visit running"
|
|
10
|
+
: "Recording your visit" }), replay.mode === "manual" && (_jsx("button", { className: "vac-text-button", onClick: replay.stop, children: "End recording and review" }))] }));
|
|
11
|
+
}
|
|
12
|
+
export function ReplayLab({ stage, onStage, replay, pathname, returnHome, sessionPlayer, }) {
|
|
13
|
+
const source = replay.exercise;
|
|
14
|
+
const hasVisit = replay.recording.length > 1;
|
|
15
|
+
const previousMode = useRef(replay.mode);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (previousMode.current === "manual" &&
|
|
18
|
+
!replay.mode &&
|
|
19
|
+
hasVisit &&
|
|
20
|
+
stage === 1) {
|
|
21
|
+
onStage(2);
|
|
22
|
+
}
|
|
23
|
+
previousMode.current = replay.mode;
|
|
24
|
+
}, [replay.mode, hasVisit, stage, onStage]);
|
|
25
|
+
if (replay.mode === "ghost" || (replay.starting && source === "ghost")) {
|
|
26
|
+
const latest = [...replay.frames].reverse().find((frame) => frame.kind === "click" ||
|
|
27
|
+
frame.kind === "input" ||
|
|
28
|
+
frame.kind === "page");
|
|
29
|
+
const update = replay.starting
|
|
30
|
+
? "Starting the recorder…"
|
|
31
|
+
: latest?.kind === "input"
|
|
32
|
+
? "Typing a search for Adiron-shack…"
|
|
33
|
+
: latest?.kind === "click"
|
|
34
|
+
? latest.label.startsWith("Filter: ")
|
|
35
|
+
? `Selected ${latest.label.slice(8)}. Browsing the filtered stays…`
|
|
36
|
+
: "Opening Adiron-shack…"
|
|
37
|
+
: latest?.page.startsWith("/stays/")
|
|
38
|
+
? "Viewing the stay details. Finishing the recording…"
|
|
39
|
+
: "Starting on Find a stay…";
|
|
40
|
+
return (_jsxs("section", { className: "vac-lab vac-ghost-progress", "aria-label": "Ghost simulation in progress", children: [_jsx("h3", { children: "Watch the ghost visitor" }), _jsx("div", { className: "vac-ghost-loader", "aria-hidden": "true" }), _jsx("p", { role: "status", "aria-live": "polite", "aria-atomic": "true", children: update }), _jsx("p", { className: "vac-muted", children: "The simulation runs automatically. You can watch the recording when it finishes." })] }));
|
|
41
|
+
}
|
|
42
|
+
if (stage === 2 && replay.mode)
|
|
43
|
+
return (_jsxs("section", { className: "vac-lab vac-builder", children: [_jsx("h3", { children: "Finish the visit first" }), _jsx("p", { children: "Explore Twig however you like, then end the recording to watch your visit." }), replay.mode === "manual" && (_jsx("button", { className: "vac-button", onClick: replay.stop, children: _jsx("span", { className: "vac-os-button-face", children: "End recording and review \u2192" }) }))] }));
|
|
44
|
+
if (stage === 0)
|
|
45
|
+
return (_jsxs("div", { className: "vac-guide-intro", children: [_jsx("h3", { children: "What happened between the clicks?" }), _jsx("p", { children: "Events tell you a visitor selected Coast. Replay adds the sequence: where they moved, what they typed, and which stay they opened." }), _jsx("div", { className: "vac-guide-example", children: _jsx("p", { children: "Follow a ghost visitor from filters to a stay. Then record your own visit." }) }), _jsx("button", { className: "vac-button", onClick: () => onStage(1), children: _jsx("span", { className: "vac-os-button-face", children: "Set up the ghost visit \u2192" }) })] }));
|
|
46
|
+
return (_jsxs("section", { className: "vac-lab vac-builder", children: [stage === 1 ? (_jsxs(_Fragment, { children: [_jsx("h3", { children: replay.mode === "manual"
|
|
47
|
+
? "Record your visit on Twig"
|
|
48
|
+
: hasVisit
|
|
49
|
+
? "Your recording is ready"
|
|
50
|
+
: source === "ghost"
|
|
51
|
+
? "Set up the ghost visit"
|
|
52
|
+
: "Set up your own recording" }), replay.mode || hasVisit ? (_jsxs(_Fragment, { children: [_jsx("p", { children: replay.mode === "manual" ? (_jsxs(_Fragment, { children: ["Explore Twig however you like. Click around, browse stays, or try a search. When you\u2019re ready, click", " ", _jsx("strong", { children: "End recording and review" }), " to watch your visit."] })) : ("Your recording is ready. Play it back to see the interactions you captured.") }), replay.mode === "manual" && (_jsx("button", { className: "vac-button", onClick: replay.stop, children: _jsx("span", { className: "vac-os-button-face", children: "End recording and review \u2192" }) })), !replay.mode && hasVisit && (_jsx("button", { className: "vac-button", onClick: () => onStage(2), children: _jsx("span", { className: "vac-os-button-face", children: "Watch the recording \u2192" }) }))] })) : (_jsxs(_Fragment, { children: [_jsx("p", { children: source === "ghost"
|
|
53
|
+
? "First, capture a ghost visitor filtering stays, searching, and opening Adiron-shack."
|
|
54
|
+
: "Start recording, then explore Twig however you like. When you’re ready, end the recording to watch it back. Starting replaces the ghost recording." }), _jsxs("label", { className: "vac-replay-mask", children: [_jsx("input", { type: "checkbox", "aria-describedby": "replay-masking-help", checked: replay.masked, onChange: (event) => replay.setMasked(event.target.checked) }), " ", "Hide search text in the replay"] }), _jsxs("p", { id: "replay-masking-help", className: "vac-muted", children: ["When enabled, search words appear as ", _jsx("code", { children: "[masked]" }), " in the recording. Your recording stays in this browser tab."] }), source === "ghost" && pathname !== "/" ? ({ returnHome }) : (_jsx("button", { className: "vac-button", disabled: replay.starting, onClick: () => replay.start(source), children: _jsx("span", { className: "vac-os-button-face", children: replay.starting
|
|
55
|
+
? "Starting recorder…"
|
|
56
|
+
: source === "ghost"
|
|
57
|
+
? "Run ghost simulation →"
|
|
58
|
+
: "Start my recording →" }) })), replay.message && _jsx("p", { role: "status", children: replay.message })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("h3", { children: "Watch your actual visit" }), replay.recording.length < 2 ? (_jsxs(_Fragment, { children: [_jsx("p", { children: "Record a new visit to capture the page for playback." }), _jsx("button", { className: "vac-button", onClick: () => {
|
|
59
|
+
replay.clear();
|
|
60
|
+
onStage(1);
|
|
61
|
+
}, children: _jsx("span", { className: "vac-os-button-face", children: "Continue the exercise \u2192" }) })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "vac-muted", children: "Actual page recording, stored in this tab. Nothing sent to PostHog." }), sessionPlayer, _jsxs("div", { className: "vac-lab-output", children: [_jsx("h4", { children: "Look for the search" }), _jsx("p", { children: replay.capturedMasked
|
|
62
|
+
? "Search words were masked before storage. You can still watch the typing and surrounding interactions."
|
|
63
|
+
: "Search words are visible in this recording. Record another visit with masking on to compare." })] }), _jsxs("section", { className: "vac-lab-step", "aria-label": "Captured interactions", children: [_jsx("h4", { children: "Captured interactions" }), _jsx("ol", { children: replay.frames
|
|
64
|
+
.filter((item) => item.kind !== "move" && item.kind !== "scroll")
|
|
65
|
+
.map((item, i) => (_jsxs("li", { children: [(item.at / 1000).toFixed(1), "s \u00B7 ", item.label, item.value !== undefined ? `: ${item.value}` : ""] }, i))) })] }), source === "ghost" ? (_jsx("button", { className: "vac-button", onClick: () => {
|
|
66
|
+
replay.clear();
|
|
67
|
+
replay.setExercise("manual");
|
|
68
|
+
onStage(1);
|
|
69
|
+
}, children: _jsx("span", { className: "vac-os-button-face", children: "Record my own visit \u2192" }) })) : (_jsx(FinishLabButton, { onClick: () => onStage(3) }))] }))] })), _jsxs("details", { className: "vac-lab-step", children: [_jsx("summary", { children: "About this exercise" }), _jsx("p", { children: "This uses rrweb to record and replay actual page changes, clicks, scrolling, and typing. It stays in memory in this tab. The playground and other form fields are excluded. No recording is uploaded to PostHog. Reloading clears it." }), _jsx("a", { href: "https://posthog.com/docs/session-replay", target: "_blank", rel: "noreferrer", children: "Session Replay docs \u2197" })] })] }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type ReplayFrame } from "./replay-lab.js";
|
|
3
|
+
import type { eventWithTime } from "@rrweb/types";
|
|
4
|
+
type Mode = "ghost" | "manual" | null;
|
|
5
|
+
type ReplayContextValue = {
|
|
6
|
+
exercise: "ghost" | "manual";
|
|
7
|
+
setExercise: (value: "ghost" | "manual") => void;
|
|
8
|
+
recording: eventWithTime[];
|
|
9
|
+
starting: boolean;
|
|
10
|
+
frames: ReplayFrame[];
|
|
11
|
+
mode: Mode;
|
|
12
|
+
masked: boolean;
|
|
13
|
+
setMasked: (value: boolean) => void;
|
|
14
|
+
capturedMasked: boolean;
|
|
15
|
+
message: string;
|
|
16
|
+
start: (mode: Exclude<Mode, null>) => void;
|
|
17
|
+
stop: () => void;
|
|
18
|
+
clear: () => void;
|
|
19
|
+
reset: () => void;
|
|
20
|
+
resetCount: number;
|
|
21
|
+
};
|
|
22
|
+
export declare const useReplay: () => ReplayContextValue;
|
|
23
|
+
export declare function ReplayProvider({ children, open, pathname, }: {
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
open: boolean;
|
|
26
|
+
pathname: string;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
28
|
+
export {};
|