@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,167 @@
|
|
|
1
|
+
/** Stable IDs identify instrumentation; displayed marker numbers are page-local. */
|
|
2
|
+
export declare const touchpointDefinitions: {
|
|
3
|
+
readonly discovery: {
|
|
4
|
+
readonly title: "AI trip discovery";
|
|
5
|
+
readonly color: "yellow";
|
|
6
|
+
readonly tool: "AI observability";
|
|
7
|
+
readonly lesson: "Inspect a trip-planning request and its model response.";
|
|
8
|
+
readonly lab: "ai-discovery";
|
|
9
|
+
readonly description: "Configure AI observability fields, then test a simulated recommendation or timeout.";
|
|
10
|
+
readonly evidence: "Local generation events only; no model or PostHog connection.";
|
|
11
|
+
};
|
|
12
|
+
readonly catalog: {
|
|
13
|
+
readonly title: "Stay filters";
|
|
14
|
+
readonly color: "blue";
|
|
15
|
+
readonly tool: "Product analytics";
|
|
16
|
+
readonly lesson: "Capture filter clicks and attach the selected setting.";
|
|
17
|
+
readonly lab: "filter-events";
|
|
18
|
+
readonly description: "Setting filters narrow the catalog. Select a stay to see its details and nightly rate.";
|
|
19
|
+
readonly evidence: "No analytics capture connected.";
|
|
20
|
+
};
|
|
21
|
+
readonly replay: {
|
|
22
|
+
readonly title: "Follow a visitor";
|
|
23
|
+
readonly color: "red";
|
|
24
|
+
readonly tool: "Session Replay";
|
|
25
|
+
readonly lesson: "Watch a ghost visit, then record and inspect your own interactions.";
|
|
26
|
+
readonly lab: "session-replay";
|
|
27
|
+
readonly description: "Explore interaction sequences and input masking.";
|
|
28
|
+
readonly evidence: "Local interaction reconstruction; no PostHog recording.";
|
|
29
|
+
};
|
|
30
|
+
readonly stay: {
|
|
31
|
+
readonly title: "Stay details";
|
|
32
|
+
readonly color: "blue";
|
|
33
|
+
readonly tool: "Product analytics";
|
|
34
|
+
readonly lesson: "Connect a stay’s ID to its view event.";
|
|
35
|
+
readonly lab: "stay-views";
|
|
36
|
+
readonly description: "This page reads the selected stay’s catalog entry and host profile. Photos, amenities, and prices belong to this stay.";
|
|
37
|
+
readonly evidence: "Locally simulated view events from navigation between stay pages.";
|
|
38
|
+
};
|
|
39
|
+
readonly booking: {
|
|
40
|
+
readonly title: "Booking";
|
|
41
|
+
readonly color: "yellow";
|
|
42
|
+
readonly tool: "Product analytics";
|
|
43
|
+
readonly lesson: "Record a completed booking only after confirmation.";
|
|
44
|
+
readonly lab: "booking-events";
|
|
45
|
+
readonly description: "Compare a failed booking with its captured events, then move completion capture to the successful response.";
|
|
46
|
+
readonly evidence: "Local simulated availability and booking responses. No reservation or payment created.";
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export type TouchpointId = keyof typeof touchpointDefinitions;
|
|
50
|
+
type PageKind = "discover" | "stay" | "about";
|
|
51
|
+
export declare function playgroundPage(pathname: string, catalog: readonly {
|
|
52
|
+
id: string;
|
|
53
|
+
title: string | null;
|
|
54
|
+
}[]): {
|
|
55
|
+
key: string;
|
|
56
|
+
kind: PageKind | null;
|
|
57
|
+
stayId: string | null;
|
|
58
|
+
label: string;
|
|
59
|
+
touchpoints: ({
|
|
60
|
+
title: "AI trip discovery";
|
|
61
|
+
color: "yellow";
|
|
62
|
+
tool: "AI observability";
|
|
63
|
+
lesson: "Inspect a trip-planning request and its model response.";
|
|
64
|
+
lab: "ai-discovery";
|
|
65
|
+
description: "Configure AI observability fields, then test a simulated recommendation or timeout.";
|
|
66
|
+
evidence: "Local generation events only; no model or PostHog connection.";
|
|
67
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
68
|
+
number: number;
|
|
69
|
+
} | {
|
|
70
|
+
title: "Stay filters";
|
|
71
|
+
color: "blue";
|
|
72
|
+
tool: "Product analytics";
|
|
73
|
+
lesson: "Capture filter clicks and attach the selected setting.";
|
|
74
|
+
lab: "filter-events";
|
|
75
|
+
description: "Setting filters narrow the catalog. Select a stay to see its details and nightly rate.";
|
|
76
|
+
evidence: "No analytics capture connected.";
|
|
77
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
78
|
+
number: number;
|
|
79
|
+
} | {
|
|
80
|
+
title: "Follow a visitor";
|
|
81
|
+
color: "red";
|
|
82
|
+
tool: "Session Replay";
|
|
83
|
+
lesson: "Watch a ghost visit, then record and inspect your own interactions.";
|
|
84
|
+
lab: "session-replay";
|
|
85
|
+
description: "Explore interaction sequences and input masking.";
|
|
86
|
+
evidence: "Local interaction reconstruction; no PostHog recording.";
|
|
87
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
88
|
+
number: number;
|
|
89
|
+
} | {
|
|
90
|
+
title: "Stay details";
|
|
91
|
+
color: "blue";
|
|
92
|
+
tool: "Product analytics";
|
|
93
|
+
lesson: "Connect a stay’s ID to its view event.";
|
|
94
|
+
lab: "stay-views";
|
|
95
|
+
description: "This page reads the selected stay’s catalog entry and host profile. Photos, amenities, and prices belong to this stay.";
|
|
96
|
+
evidence: "Locally simulated view events from navigation between stay pages.";
|
|
97
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
98
|
+
number: number;
|
|
99
|
+
} | {
|
|
100
|
+
title: "Booking";
|
|
101
|
+
color: "yellow";
|
|
102
|
+
tool: "Product analytics";
|
|
103
|
+
lesson: "Record a completed booking only after confirmation.";
|
|
104
|
+
lab: "booking-events";
|
|
105
|
+
description: "Compare a failed booking with its captured events, then move completion capture to the successful response.";
|
|
106
|
+
evidence: "Local simulated availability and booking responses. No reservation or payment created.";
|
|
107
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
108
|
+
number: number;
|
|
109
|
+
})[];
|
|
110
|
+
};
|
|
111
|
+
export type PlaygroundPage = ReturnType<typeof playgroundPage>;
|
|
112
|
+
export type PlaygroundSelection = {
|
|
113
|
+
pageKey: string;
|
|
114
|
+
id: TouchpointId;
|
|
115
|
+
};
|
|
116
|
+
export declare function selectedTouchpoint(page: PlaygroundPage, selection: PlaygroundSelection | null): {
|
|
117
|
+
title: "AI trip discovery";
|
|
118
|
+
color: "yellow";
|
|
119
|
+
tool: "AI observability";
|
|
120
|
+
lesson: "Inspect a trip-planning request and its model response.";
|
|
121
|
+
lab: "ai-discovery";
|
|
122
|
+
description: "Configure AI observability fields, then test a simulated recommendation or timeout.";
|
|
123
|
+
evidence: "Local generation events only; no model or PostHog connection.";
|
|
124
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
125
|
+
number: number;
|
|
126
|
+
} | {
|
|
127
|
+
title: "Stay filters";
|
|
128
|
+
color: "blue";
|
|
129
|
+
tool: "Product analytics";
|
|
130
|
+
lesson: "Capture filter clicks and attach the selected setting.";
|
|
131
|
+
lab: "filter-events";
|
|
132
|
+
description: "Setting filters narrow the catalog. Select a stay to see its details and nightly rate.";
|
|
133
|
+
evidence: "No analytics capture connected.";
|
|
134
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
135
|
+
number: number;
|
|
136
|
+
} | {
|
|
137
|
+
title: "Follow a visitor";
|
|
138
|
+
color: "red";
|
|
139
|
+
tool: "Session Replay";
|
|
140
|
+
lesson: "Watch a ghost visit, then record and inspect your own interactions.";
|
|
141
|
+
lab: "session-replay";
|
|
142
|
+
description: "Explore interaction sequences and input masking.";
|
|
143
|
+
evidence: "Local interaction reconstruction; no PostHog recording.";
|
|
144
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
145
|
+
number: number;
|
|
146
|
+
} | {
|
|
147
|
+
title: "Stay details";
|
|
148
|
+
color: "blue";
|
|
149
|
+
tool: "Product analytics";
|
|
150
|
+
lesson: "Connect a stay’s ID to its view event.";
|
|
151
|
+
lab: "stay-views";
|
|
152
|
+
description: "This page reads the selected stay’s catalog entry and host profile. Photos, amenities, and prices belong to this stay.";
|
|
153
|
+
evidence: "Locally simulated view events from navigation between stay pages.";
|
|
154
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
155
|
+
number: number;
|
|
156
|
+
} | {
|
|
157
|
+
title: "Booking";
|
|
158
|
+
color: "yellow";
|
|
159
|
+
tool: "Product analytics";
|
|
160
|
+
lesson: "Record a completed booking only after confirmation.";
|
|
161
|
+
lab: "booking-events";
|
|
162
|
+
description: "Compare a failed booking with its captured events, then move completion capture to the successful response.";
|
|
163
|
+
evidence: "Local simulated availability and booking responses. No reservation or payment created.";
|
|
164
|
+
id: "stay" | "discovery" | "catalog" | "replay" | "booking";
|
|
165
|
+
number: number;
|
|
166
|
+
};
|
|
167
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Stable IDs identify instrumentation; displayed marker numbers are page-local. */
|
|
2
|
+
export const touchpointDefinitions = {
|
|
3
|
+
discovery: {
|
|
4
|
+
title: "AI trip discovery",
|
|
5
|
+
color: "yellow",
|
|
6
|
+
tool: "AI observability",
|
|
7
|
+
lesson: "Inspect a trip-planning request and its model response.",
|
|
8
|
+
lab: "ai-discovery",
|
|
9
|
+
description: "Configure AI observability fields, then test a simulated recommendation or timeout.",
|
|
10
|
+
evidence: "Local generation events only; no model or PostHog connection.",
|
|
11
|
+
},
|
|
12
|
+
catalog: {
|
|
13
|
+
title: "Stay filters",
|
|
14
|
+
color: "blue",
|
|
15
|
+
tool: "Product analytics",
|
|
16
|
+
lesson: "Capture filter clicks and attach the selected setting.",
|
|
17
|
+
lab: "filter-events",
|
|
18
|
+
description: "Setting filters narrow the catalog. Select a stay to see its details and nightly rate.",
|
|
19
|
+
evidence: "No analytics capture connected.",
|
|
20
|
+
},
|
|
21
|
+
replay: {
|
|
22
|
+
title: "Follow a visitor",
|
|
23
|
+
color: "red",
|
|
24
|
+
tool: "Session Replay",
|
|
25
|
+
lesson: "Watch a ghost visit, then record and inspect your own interactions.",
|
|
26
|
+
lab: "session-replay",
|
|
27
|
+
description: "Explore interaction sequences and input masking.",
|
|
28
|
+
evidence: "Local interaction reconstruction; no PostHog recording.",
|
|
29
|
+
},
|
|
30
|
+
stay: {
|
|
31
|
+
title: "Stay details",
|
|
32
|
+
color: "blue",
|
|
33
|
+
tool: "Product analytics",
|
|
34
|
+
lesson: "Connect a stay’s ID to its view event.",
|
|
35
|
+
lab: "stay-views",
|
|
36
|
+
description: "This page reads the selected stay’s catalog entry and host profile. Photos, amenities, and prices belong to this stay.",
|
|
37
|
+
evidence: "Locally simulated view events from navigation between stay pages.",
|
|
38
|
+
},
|
|
39
|
+
booking: {
|
|
40
|
+
title: "Booking",
|
|
41
|
+
color: "yellow",
|
|
42
|
+
tool: "Product analytics",
|
|
43
|
+
lesson: "Record a completed booking only after confirmation.",
|
|
44
|
+
lab: "booking-events",
|
|
45
|
+
description: "Compare a failed booking with its captured events, then move completion capture to the successful response.",
|
|
46
|
+
evidence: "Local simulated availability and booking responses. No reservation or payment created.",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const pageTouchpoints = {
|
|
50
|
+
discover: ["discovery", "catalog", "replay"],
|
|
51
|
+
about: [],
|
|
52
|
+
stay: ["stay", "booking", "replay"],
|
|
53
|
+
};
|
|
54
|
+
export function playgroundPage(pathname, catalog) {
|
|
55
|
+
const path = pathname.replace(/\/+$/, "") || "/";
|
|
56
|
+
const match = /^\/stays\/([^/]+)$/.exec(path);
|
|
57
|
+
const stay = match
|
|
58
|
+
? catalog.find((entry) => entry.id === match[1])
|
|
59
|
+
: undefined;
|
|
60
|
+
const kind = path === "/"
|
|
61
|
+
? "discover"
|
|
62
|
+
: path === "/about"
|
|
63
|
+
? "about"
|
|
64
|
+
: stay
|
|
65
|
+
? "stay"
|
|
66
|
+
: null;
|
|
67
|
+
const ids = kind ? pageTouchpoints[kind] : [];
|
|
68
|
+
return {
|
|
69
|
+
key: path,
|
|
70
|
+
kind,
|
|
71
|
+
stayId: stay?.id ?? null,
|
|
72
|
+
label: kind === "discover"
|
|
73
|
+
? "Find a stay"
|
|
74
|
+
: kind === "about"
|
|
75
|
+
? "About Twig"
|
|
76
|
+
: stay
|
|
77
|
+
? `Stay details${stay.title ? ` · ${stay.title}` : ""}`
|
|
78
|
+
: "This page",
|
|
79
|
+
touchpoints: ids.map((id, index) => ({
|
|
80
|
+
id,
|
|
81
|
+
number: index + 1,
|
|
82
|
+
...touchpointDefinitions[id],
|
|
83
|
+
})),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function selectedTouchpoint(page, selection) {
|
|
87
|
+
return ((selection?.pageKey === page.key &&
|
|
88
|
+
page.touchpoints.find((item) => item.id === selection.id)) ||
|
|
89
|
+
page.touchpoints[0] ||
|
|
90
|
+
null);
|
|
91
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ReplayFrame = {
|
|
2
|
+
at: number;
|
|
3
|
+
kind: "move" | "click" | "input" | "page" | "scroll" | "state";
|
|
4
|
+
page: string;
|
|
5
|
+
label: string;
|
|
6
|
+
x?: number;
|
|
7
|
+
y?: number;
|
|
8
|
+
value?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const REPLAY_LIMIT = 500;
|
|
11
|
+
export declare function replayPage(path: string): boolean;
|
|
12
|
+
export declare function searchValue(value: string, masked: boolean): string;
|
|
13
|
+
export declare function replayScene(frames: ReplayFrame[], index: number): {
|
|
14
|
+
page: string;
|
|
15
|
+
filter: string;
|
|
16
|
+
search: string;
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const REPLAY_LIMIT = 500;
|
|
2
|
+
export function replayPage(path) {
|
|
3
|
+
return path === "/" || /^\/stays\/[^/?#]+$/.test(path);
|
|
4
|
+
}
|
|
5
|
+
export function searchValue(value, masked) {
|
|
6
|
+
return masked ? "[masked]" : value.slice(0, 80);
|
|
7
|
+
}
|
|
8
|
+
export function replayScene(frames, index) {
|
|
9
|
+
let page = "/", filter = "All", search = "", x = 50, y = 30;
|
|
10
|
+
for (const frame of frames.slice(0, index + 1)) {
|
|
11
|
+
if (frame.page !== page) {
|
|
12
|
+
page = frame.page;
|
|
13
|
+
filter = "All";
|
|
14
|
+
search = "";
|
|
15
|
+
}
|
|
16
|
+
if (frame.label.startsWith("Filter: "))
|
|
17
|
+
filter = frame.label.slice(8);
|
|
18
|
+
if (frame.kind === "input")
|
|
19
|
+
search = frame.value ?? "";
|
|
20
|
+
if (frame.x !== undefined)
|
|
21
|
+
x = frame.x;
|
|
22
|
+
if (frame.y !== undefined)
|
|
23
|
+
y = frame.y;
|
|
24
|
+
}
|
|
25
|
+
return { page, filter, search, x, y };
|
|
26
|
+
}
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const staySettings = ["All", "Forest", "Coast", "Mountain"];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type StayView = {
|
|
2
|
+
actualStayId: string;
|
|
3
|
+
actualTitle: string;
|
|
4
|
+
event: "stay_viewed";
|
|
5
|
+
properties: {
|
|
6
|
+
stay_id?: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type StayLabState = {
|
|
10
|
+
applied: "anonymous" | "identified" | null;
|
|
11
|
+
running: boolean;
|
|
12
|
+
events: StayView[];
|
|
13
|
+
before: StayView[];
|
|
14
|
+
revision: number;
|
|
15
|
+
};
|
|
16
|
+
export declare const initialStayLab: StayLabState;
|
|
17
|
+
export type StayLabAction = {
|
|
18
|
+
type: "reset";
|
|
19
|
+
} | {
|
|
20
|
+
type: "pause";
|
|
21
|
+
} | {
|
|
22
|
+
type: "apply";
|
|
23
|
+
identified: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
type: "repair";
|
|
26
|
+
} | {
|
|
27
|
+
type: "view";
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function stayLabReducer(state: StayLabState, action: StayLabAction): StayLabState;
|
|
32
|
+
export declare function stayCode(identified: boolean): "posthog.capture(\"stay_viewed\", {\n stay_id: stay.id\n});" | "posthog.capture(\"stay_viewed\");";
|
package/dist/stay-lab.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const initialStayLab = {
|
|
2
|
+
applied: null,
|
|
3
|
+
running: false,
|
|
4
|
+
events: [],
|
|
5
|
+
before: [],
|
|
6
|
+
revision: 0,
|
|
7
|
+
};
|
|
8
|
+
export function stayLabReducer(state, action) {
|
|
9
|
+
switch (action.type) {
|
|
10
|
+
case "reset":
|
|
11
|
+
return { ...initialStayLab, revision: state.revision + 1 };
|
|
12
|
+
case "pause":
|
|
13
|
+
return { ...state, running: false };
|
|
14
|
+
case "apply":
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
applied: action.identified ? "identified" : "anonymous",
|
|
18
|
+
running: true,
|
|
19
|
+
events: [],
|
|
20
|
+
};
|
|
21
|
+
case "repair":
|
|
22
|
+
return {
|
|
23
|
+
...state,
|
|
24
|
+
before: state.events,
|
|
25
|
+
events: [],
|
|
26
|
+
applied: null,
|
|
27
|
+
running: false,
|
|
28
|
+
};
|
|
29
|
+
case "view":
|
|
30
|
+
return !state.running ||
|
|
31
|
+
!state.applied ||
|
|
32
|
+
!action.id ||
|
|
33
|
+
state.events.length >= 20
|
|
34
|
+
? state
|
|
35
|
+
: {
|
|
36
|
+
...state,
|
|
37
|
+
events: [
|
|
38
|
+
...state.events,
|
|
39
|
+
{
|
|
40
|
+
actualStayId: action.id,
|
|
41
|
+
actualTitle: action.title,
|
|
42
|
+
event: "stay_viewed",
|
|
43
|
+
properties: state.applied === "identified" ? { stay_id: action.id } : {},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function stayCode(identified) {
|
|
50
|
+
return identified
|
|
51
|
+
? 'posthog.capture("stay_viewed", {\n stay_id: stay.id\n});'
|
|
52
|
+
: 'posthog.capture("stay_viewed");';
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TripSelection = {
|
|
2
|
+
checkIn: string;
|
|
3
|
+
checkOut: string;
|
|
4
|
+
guests: string;
|
|
5
|
+
};
|
|
6
|
+
export type TripErrors = Partial<Record<keyof TripSelection, string>>;
|
|
7
|
+
export declare function todayAtStay(now?: Date): string;
|
|
8
|
+
export declare function validateTrip(selection: TripSelection, capacity: number, today: string): {
|
|
9
|
+
errors: Partial<Record<keyof TripSelection, string>>;
|
|
10
|
+
nights: number | null;
|
|
11
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const DAY_MS = 86_400_000;
|
|
2
|
+
/** Calendar dates are UTC day numbers, not instants in the guest's timezone. */
|
|
3
|
+
function calendarDay(value) {
|
|
4
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
5
|
+
return null;
|
|
6
|
+
const timestamp = Date.parse(`${value}T00:00:00Z`);
|
|
7
|
+
if (!Number.isFinite(timestamp))
|
|
8
|
+
return null;
|
|
9
|
+
if (new Date(timestamp).toISOString().slice(0, 10) !== value)
|
|
10
|
+
return null;
|
|
11
|
+
return timestamp / DAY_MS;
|
|
12
|
+
}
|
|
13
|
+
export function todayAtStay(now = new Date()) {
|
|
14
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
15
|
+
timeZone: "America/New_York",
|
|
16
|
+
year: "numeric",
|
|
17
|
+
month: "2-digit",
|
|
18
|
+
day: "2-digit",
|
|
19
|
+
}).formatToParts(now);
|
|
20
|
+
const part = (type) => parts.find((entry) => entry.type === type).value;
|
|
21
|
+
return `${part("year")}-${part("month")}-${part("day")}`;
|
|
22
|
+
}
|
|
23
|
+
export function validateTrip(selection, capacity, today) {
|
|
24
|
+
const errors = {};
|
|
25
|
+
const start = calendarDay(selection.checkIn);
|
|
26
|
+
const end = calendarDay(selection.checkOut);
|
|
27
|
+
const current = calendarDay(today);
|
|
28
|
+
if (current === null)
|
|
29
|
+
throw new Error("A valid current date is required.");
|
|
30
|
+
if (start === null)
|
|
31
|
+
errors.checkIn = "Choose a valid check-in date.";
|
|
32
|
+
else if (start < current)
|
|
33
|
+
errors.checkIn = "Check-in can’t be in the past.";
|
|
34
|
+
if (end === null)
|
|
35
|
+
errors.checkOut = "Choose a valid check-out date.";
|
|
36
|
+
else if (start !== null && end <= start)
|
|
37
|
+
errors.checkOut = "Check-out must be after check-in.";
|
|
38
|
+
const guests = Number(selection.guests);
|
|
39
|
+
if (!/^\d+$/.test(selection.guests) ||
|
|
40
|
+
!Number.isInteger(guests) ||
|
|
41
|
+
guests < 1 ||
|
|
42
|
+
guests > capacity) {
|
|
43
|
+
errors.guests = `Choose between 1 and ${capacity} guests.`;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
errors,
|
|
47
|
+
nights: start !== null && end !== null && end > start ? end - start : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Component reference
|
|
2
|
+
|
|
3
|
+
Import only the pieces you need from `@posthog/twig-components/<path>`. React is a peer dependency: the host supplies React 18 or 19. Import `catalog.css` for Twig views and `lab.css` for the playground. See [integration](integration.md) for styling and update boundaries.
|
|
4
|
+
|
|
5
|
+
“Used in Twig” below means the local workspace copy in `twig.com`; the standalone npm package is not published or installed yet. PostHog 101 does not currently import this package.
|
|
6
|
+
|
|
7
|
+
## Website views and data
|
|
8
|
+
|
|
9
|
+
| Import path | Export | What it does | Used in Twig / what the host supplies |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| `/filters` | `StayFilters`, `staySettings`, `StaySetting` | Controlled Forest/Coast/Mountain filter buttons; calls `onChange` on a click. | `Discover`; pass `value`, `onChange`, and optional CSS classes. The host filters the listings. |
|
|
12
|
+
| `/stay-card` | `StayCardContent` | Listing text, location, capacity, and nightly price. | `StayCard`; pass a `stay` and `image`. The host supplies the link or card wrapper; `linked` shows an arrow. |
|
|
13
|
+
| `/browse-stays-preview` | `BrowseStaysPreview` | Read-only Browse stays excerpt with one selected filter and stay. | Gallery only; intended for PostHog 101. Pass `selected="Forest"`, `"Coast"`, or `"Mountain"`. No click handler or analytics. |
|
|
14
|
+
| `/catalog` | `stays`, `characters`, `conciergeContent`, `discoveryCopy`, `filterStays`, `stayLabel`, `nightlyPrice`, types | Fictional catalog content and pure display/filter helpers. | Discover, stay pages, and lab fixtures; the host supplies page layout and navigation. |
|
|
15
|
+
| `/trip-dates` | `todayAtStay`, `validateTrip`, types | Date and guest validation for the booking exercise. | Booking flow; pass the visitor’s trip selection. |
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { StayFilters, type StaySetting } from "@posthog/twig-components/filters";
|
|
20
|
+
import { filterStays } from "@posthog/twig-components/catalog";
|
|
21
|
+
import "@posthog/twig-components/catalog.css";
|
|
22
|
+
|
|
23
|
+
function Browse() {
|
|
24
|
+
const [setting, setSetting] = useState<StaySetting>("All");
|
|
25
|
+
return <>
|
|
26
|
+
<StayFilters
|
|
27
|
+
value={setting}
|
|
28
|
+
onChange={setSetting}
|
|
29
|
+
className="vac-filters"
|
|
30
|
+
buttonClassName="vac-filter"
|
|
31
|
+
/>
|
|
32
|
+
<p>{filterStays(setting).length} stays</p>
|
|
33
|
+
</>;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Labs and playground
|
|
38
|
+
|
|
39
|
+
These are **local teaching simulations**. They do not send events to PostHog, make bookings, or run an AI model. Twig.com supplies routes, page content, focus/scroll actions, and navigation callbacks.
|
|
40
|
+
|
|
41
|
+
| Import path | Main exports | What it does / host input | Used in Twig |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| `/playground` | `playgroundPage`, `selectedTouchpoint`, `touchpointDefinitions`, types | Maps a Twig path and catalog to available lab markers; host passes the current path. | Playground and site adapters |
|
|
44
|
+
| `/playground-controller` | `PlaygroundLabProvider`, `usePlaygroundLab`, `useFilterLab` | Owns selected lab, stage, and filter state; wrap the playground and pass `active` and `bookingEnabled`. Requires `ReplayProvider` and `StayLabProvider` above it. | Playground / FilterLab |
|
|
45
|
+
| `/playground-panels` | `LabDirectory`, `SelectedLabHeader`, `PlaygroundMarker`, `PlaygroundInvitation`, `PlaygroundDock` | Lab list, page markers, invitation, and dock layout; host supplies callbacks, logo, navigation, guide content, and focus ref. | Playground / FilterLab |
|
|
46
|
+
| `/filter-lab-exercise` | `FilterLabIntroduction`, `FilterLabExercise` | Teaches fixed versus clicked filter values; host passes filter state, stage, filter URL, focus, and continuation callbacks. | FilterLab |
|
|
47
|
+
| `/filter-inspector` | `FilterInspector` | Compares filter clicks with locally recorded event values; needs state, a filter URL, and practice callback. | FilterLab exercise |
|
|
48
|
+
| `/ai-lab-ui` | `AiLabProvider`, `useAiLab`, `useResetAiLab`, `AiIntroduction`, `AiLab` | AI observability lesson with preset request, response, and timeout; provider holds local state. Host passes stage and planner focus callback. | AiLab |
|
|
49
|
+
| `/booking-lab-ui` | `BookingLabProvider`, `useBookingLab`, `BookingLab` | Booking attempt versus confirmed completion lesson; provider holds trip and event state. Host passes stage and booking focus callback. | BookingLab |
|
|
50
|
+
| `/stay-lab-ui` | `StayLabProvider`, `useStayLab`, `StayLab` | Connects stay view events to listing IDs; host passes path and renders stay links. | StayLab |
|
|
51
|
+
| `/replay-recorder` | `ReplayProvider`, `useReplay` | Records the current tab locally or runs a ghost visit with rrweb; pass `open` and `pathname`. | ReplayLab / Playground |
|
|
52
|
+
| `/replay-lab-ui` | `ReplayLab`, `ReplayRecordingBar`, `ReplayLabViewState` | Replay lesson and recording status; host passes stage, replay state, return link, and player. | ReplayLab |
|
|
53
|
+
| `/session-player` | `SessionPlayer` | Plays an rrweb recording; pass recorded `events`. | ReplayLab |
|
|
54
|
+
| `/lab-navigation` | `LabNavigationView` | All labs, previous step, and reset controls; host supplies actions. | FilterLab adapter |
|
|
55
|
+
| `/lab-completion-view` | `LabCompletionView` | Recap for one completed lab; host supplies choose/review actions and heading ref. | LabCompletion |
|
|
56
|
+
| `/lab-checklist` | `LabChecklist` | Step list with done/current/upcoming states; pass `label` and `items`. | Lab UI |
|
|
57
|
+
| `/lab-choices` | `LabChoices` | Accessible radio choice group; pass `label`, `value`, `options`, and `onChange`. | Lab UI |
|
|
58
|
+
| `/finish-lab-button` | `FinishLabButton` | Standard finish action; pass `onClick`. | Lab UI |
|
|
59
|
+
|
|
60
|
+
The state modules below contain reducers, types, fixtures, and formatting helpers. They have no React UI:
|
|
61
|
+
|
|
62
|
+
| Import path | Purpose | Used in Twig |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `/filter-lab` | Filter event state, validation, feedback, and example code | FilterLab and filter inspector |
|
|
65
|
+
| `/ai-lab` | AI request/response fixtures, event state, checks, and example code | AiLab |
|
|
66
|
+
| `/booking-lab` | Booking attempt/completion state and example code | BookingLab |
|
|
67
|
+
| `/stay-lab` | Stay view event state and example code | StayLab |
|
|
68
|
+
| `/replay-lab` | Replay frame types and display helpers | ReplayLab |
|
|
69
|
+
|
|
70
|
+
## Styles and assets
|
|
71
|
+
|
|
72
|
+
| Import path | Use |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `/catalog.css` | Twig website views and the read-only preview; includes Twig’s Halfre font. |
|
|
75
|
+
| `/lab.css` | Playground and lab UI; scope it under `.vac-app .vac-developer-theme`. Its PostHog-styled UI uses RoundHog. |
|
|
76
|
+
| `/assets/*` | Package font and image files referenced by the styles or explicitly imported by a host. |
|
|
77
|
+
|
|
78
|
+
Do not apply Twig typography to the PostHog event inspector in the pocket guide. That inspector, the guide prose, and “Explore Twig” links belong to PostHog.com.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Integration and updates
|
|
2
|
+
|
|
3
|
+
## What lives where
|
|
4
|
+
|
|
5
|
+
| Place | Owns |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| **This repo** | Reusable Twig views and catalog, the five labs’ UI and local state, playground panels, replay recorder/player, CSS, assets, tests, and gallery. |
|
|
8
|
+
| **Twig.com** | Full website pages, Next.js routes, stay images and links, playground placement, navigation, and focus/scroll behavior. It currently imports a **local workspace copy** of the package. |
|
|
9
|
+
| **PostHog.com** | PostHog 101 prose, code and event examples, PostHog event inspector, Twig introduction screenshot, and guide navigation. Importing the read-only Twig preview from this package is **planned**, not done. |
|
|
10
|
+
|
|
11
|
+
## Authoring scenarios
|
|
12
|
+
|
|
13
|
+
**Update a site only when that site needs the change.** Package releases do not change either site automatically. The sites may use different package versions while their needs differ.
|
|
14
|
+
|
|
15
|
+
| You want to… | Change here? | Change Twig.com? | Change PostHog.com? |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| Add a lab to Twig's playground only | Yes, for reusable lesson UI and local simulation logic | Add the lab to the page and update the package version | No |
|
|
18
|
+
| Add a Twig lab and teach it in PostHog 101 | Yes, for the lab and any focused, read-only view the guide needs | Add the lab and update the package version | Write the guide and update its package version **if** it uses new exports or behavior |
|
|
19
|
+
| Write PostHog 101 content with an existing package view | No | No | Edit the guide; keep its current version if that view is already available there |
|
|
20
|
+
|
|
21
|
+
Real event calls tied to a Twig page belong in Twig.com. They need a package change only when shared lab UI or behavior changes. If a guide needs a view that exists **only** inside Twig.com, extract that view into this package first. The guide should use focused views, not embed the full interactive lab.
|
|
22
|
+
|
|
23
|
+
During the current migration, Twig.com still uses a local workspace copy, and PostHog.com has not installed this package. The version-update steps above apply once the package is published and installed.
|
|
24
|
+
|
|
25
|
+
## How a host uses the package
|
|
26
|
+
|
|
27
|
+
1. Pick an export in the [component reference](components.md). A website control such as `StayFilters` needs state and callbacks from the host; a read-only view such as `BrowseStaysPreview` needs only its display props.
|
|
28
|
+
2. Load `catalog.css` for Twig views. Load `lab.css` for a full playground and wrap its UI in `.vac-app .vac-developer-theme`. Keep the guide’s PostHog UI outside that scope.
|
|
29
|
+
3. For the playground, put `ReplayProvider` and `StayLabProvider` above `PlaygroundLabProvider`. Pass the current path, navigation callbacks, and focus actions from the host. Twig.com’s adapters in `src/components/vacation/` show the current setup.
|
|
30
|
+
4. For a PostHog guide embed, use only the focused Twig view inside a Shadow DOM wrapper so host CSS does not change it. Keep click animation, if present, with that Twig view; place the PostHog event inspector and guide CTA outside the shadow root. The wrapper and package integration are not built yet.
|
|
31
|
+
|
|
32
|
+
The playground provider order is:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<ReplayProvider open={open} pathname={pathname}>
|
|
36
|
+
<StayLabProvider>
|
|
37
|
+
<PlaygroundLabProvider active={open && isDiscoverPage} bookingEnabled={open && isStayPage}>
|
|
38
|
+
<TwigPageAndPlayground />
|
|
39
|
+
</PlaygroundLabProvider>
|
|
40
|
+
</StayLabProvider>
|
|
41
|
+
</ReplayProvider>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How changes reach the sites
|
|
45
|
+
|
|
46
|
+
1. Change this repo. Use the [workbench](workbench.md) to click through the affected view or lab as you build it. Run `npm test`, `npm run workbench:build`, and `npm run gallery`; review the relevant preview. Run `npm pack --dry-run` before a release.
|
|
47
|
+
2. Until npm publishing is set up, mirror reviewed package changes into Twig.com’s `packages/twig-components/` workspace copy. Compare copies before calling them in sync. PostHog.com has no package integration to update yet.
|
|
48
|
+
3. After publishing is configured, release a version and update the pinned version in each site that needs the change. A published package does **not** update sites on its own.
|
|
49
|
+
4. Build each changed site and inspect the affected page at narrow and wide widths, in light and dark themes, and with reduced motion. Check that Twig styling stays inside Twig UI and the PostHog inspector keeps PostHog styling.
|
|
50
|
+
|
|
51
|
+
Dependabot proposes dependency updates each week after a seven-day cooldown for routine releases. Security updates are not delayed by that cooldown. Do not describe local workspace changes as a published package release.
|
|
52
|
+
|
|
53
|
+
## First npm release and later updates
|
|
54
|
+
|
|
55
|
+
The package is not published yet. The first `0.1.0` release must be published by an authorized `@posthog` npm maintainer with npm two-factor authentication after this PR is reviewed and merged. From a clean `main` checkout, run `npm ci`, `npm test`, and `npm pack --dry-run`; verify the package name, version, and contents. Then run `npm login` and `npm publish --access public`. Do not store an npm token in GitHub.
|
|
56
|
+
|
|
57
|
+
After the first release, configure npm trusted publishing for **PostHog/twig-components**, workflow **`publish.yml`**, environment **`npm-publish`**, and allow direct publishing. Protect that GitHub environment so only `main` may deploy and a reviewer must approve each run. Then release a new version by merging its reviewed version change to `main` and manually running **Publish to npm** on `main`. The workflow tests and previews the package before publishing with a short-lived identity token. Never run it from a feature branch.
|
|
58
|
+
|
|
59
|
+
After the first workflow release succeeds, set the package's npm **Publishing access** to **Require two-factor authentication and disallow tokens**. Verify the version and provenance on npm, then update only the consuming sites that need it. See [npm's trusted publishing guide](https://docs.npmjs.com/trusted-publishers/) for the npm settings.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Develop with the workbench
|
|
2
|
+
|
|
3
|
+
The workbench is a local browser preview for package components. It uses local examples, so you can build and click through a component without starting Twig.com or signing in to PostHog.
|
|
4
|
+
|
|
5
|
+
## Start it
|
|
6
|
+
|
|
7
|
+
From this repo:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm ci
|
|
11
|
+
npm run workbench
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Open the URL printed in the terminal (normally `http://127.0.0.1:5173`). Choose **Twig views** or a lab from the left menu. Changes in `src/`, `workbench/App.tsx`, and the CSS refresh the page while the server runs. Stop it with `Ctrl+C`.
|
|
15
|
+
|
|
16
|
+
## Add or change a component
|
|
17
|
+
|
|
18
|
+
1. Build the reusable UI in `src/`. Keep site routes, navigation, and real PostHog event calls in Twig.com.
|
|
19
|
+
2. If it is a new public component, add its import path to `package.json` → `exports` and describe it in [components.md](components.md). Use a `.js` extension for imports between TypeScript source files, as the existing components do.
|
|
20
|
+
3. Add a preview to `workbench/App.tsx`. Import directly from `../src/` so edits appear immediately. Add it to an existing view, or add a menu entry for a distinct component.
|
|
21
|
+
4. Pass example props and local callbacks. For a clickable component, let the preview own its state with React's `useState` or the package reducer. The **Filter lab** preview shows the pattern: click a Twig filter, update local state, then inspect the event.
|
|
22
|
+
5. Check the states someone will actually see: initial, clicked or changed, success or error, narrow width, and keyboard focus. For a lab, click through the whole lesson, not just its first screen.
|
|
23
|
+
|
|
24
|
+
The workbench's AI and booking actions use fixtures. The replay tab previews lesson states with a fixture; test actual recording and any site routing in Twig.com. Never put credentials or real visitor data in a fixture.
|
|
25
|
+
|
|
26
|
+
## Before review
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm test
|
|
30
|
+
npm run workbench:build
|
|
31
|
+
npm run gallery
|
|
32
|
+
npm pack --dry-run
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`npm test` checks the package behavior. `workbench:build` checks that previews type-check and bundle. `gallery` produces static snapshots for visual review. `npm pack --dry-run` shows what would ship; the workbench itself is development-only and is excluded from the npm package.
|
|
36
|
+
|
|
37
|
+
When the component is used on Twig.com, check it there too. If PostHog 101 uses it, also check the guide's Shadow DOM styling and keep its PostHog inspector outside that boundary. See [integration and updates](integration.md) for how changes reach each site.
|