@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
package/dist/catalog.css
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
.vac-app,
|
|
2
|
+
.twig-browser {
|
|
3
|
+
--vac-paper: #f7eddf;
|
|
4
|
+
--vac-ink: #2d2b29;
|
|
5
|
+
--vac-muted: #6d6258;
|
|
6
|
+
--vac-line: #d7c8b6;
|
|
7
|
+
--vac-fill: #efe1ce;
|
|
8
|
+
}
|
|
9
|
+
.vac-filters {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
flex-wrap: wrap;
|
|
13
|
+
gap: 8px;
|
|
14
|
+
border: 0;
|
|
15
|
+
margin: 20px 0 12px;
|
|
16
|
+
}
|
|
17
|
+
.vac-filter {
|
|
18
|
+
padding: 8px 16px;
|
|
19
|
+
border: 1px solid var(--vac-line);
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
}
|
|
22
|
+
.vac-filter[aria-pressed="true"] {
|
|
23
|
+
background: var(--vac-ink);
|
|
24
|
+
color: var(--vac-paper);
|
|
25
|
+
border-color: var(--vac-ink);
|
|
26
|
+
}
|
|
27
|
+
.vac-card {
|
|
28
|
+
min-width: 0;
|
|
29
|
+
}
|
|
30
|
+
.vac-card a {
|
|
31
|
+
display: block;
|
|
32
|
+
border-radius: 4px;
|
|
33
|
+
}
|
|
34
|
+
.vac-card a:hover h3 {
|
|
35
|
+
text-decoration: underline;
|
|
36
|
+
text-underline-offset: 4px;
|
|
37
|
+
}
|
|
38
|
+
.vac-card-body {
|
|
39
|
+
padding: 14px 0;
|
|
40
|
+
}
|
|
41
|
+
.vac-image {
|
|
42
|
+
position: relative;
|
|
43
|
+
display: grid;
|
|
44
|
+
place-items: center;
|
|
45
|
+
aspect-ratio: 4 / 3;
|
|
46
|
+
background: var(--vac-fill);
|
|
47
|
+
border: 1px solid var(--vac-line);
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
.vac-image span {
|
|
52
|
+
color: var(--vac-muted);
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
}
|
|
55
|
+
.vac-image img {
|
|
56
|
+
object-fit: cover;
|
|
57
|
+
}
|
|
58
|
+
.vac-image-large {
|
|
59
|
+
aspect-ratio: 2.3 / 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@font-face {
|
|
63
|
+
font-family: TwigHalfre;
|
|
64
|
+
src: url("./assets/Halfre.ttf") format("truetype");
|
|
65
|
+
font-display: swap;
|
|
66
|
+
}
|
|
67
|
+
.twig-browser {
|
|
68
|
+
container-type: inline-size;
|
|
69
|
+
background: var(--vac-paper);
|
|
70
|
+
color: var(--vac-ink);
|
|
71
|
+
font: 15px/1.6 TwigHalfre, sans-serif;
|
|
72
|
+
border: 1px solid var(--vac-line);
|
|
73
|
+
border-radius: 4px;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
}
|
|
76
|
+
.twig-browser * {
|
|
77
|
+
box-sizing: border-box;
|
|
78
|
+
}
|
|
79
|
+
.twig-browser header {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
gap: 16px;
|
|
84
|
+
padding: 16px 20px;
|
|
85
|
+
border-bottom: 1px solid var(--vac-line);
|
|
86
|
+
}
|
|
87
|
+
.twig-browser header img {
|
|
88
|
+
width: 54px;
|
|
89
|
+
height: auto;
|
|
90
|
+
}
|
|
91
|
+
.twig-browser header span {
|
|
92
|
+
font-size: 12px;
|
|
93
|
+
text-decoration: underline;
|
|
94
|
+
}
|
|
95
|
+
.twig-browser main {
|
|
96
|
+
padding: 20px;
|
|
97
|
+
}
|
|
98
|
+
.twig-browser :is(h2, h3, p) {
|
|
99
|
+
font-family: inherit;
|
|
100
|
+
color: inherit;
|
|
101
|
+
letter-spacing: normal;
|
|
102
|
+
}
|
|
103
|
+
.twig-browser h2 {
|
|
104
|
+
font-size: 26px;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
line-height: 1.2;
|
|
107
|
+
margin: 4px 0 16px;
|
|
108
|
+
}
|
|
109
|
+
.twig-browser h3 {
|
|
110
|
+
font-size: 19px;
|
|
111
|
+
font-weight: 500;
|
|
112
|
+
line-height: 1.3;
|
|
113
|
+
margin: 0;
|
|
114
|
+
}
|
|
115
|
+
.twig-browser p {
|
|
116
|
+
margin: 8px 0;
|
|
117
|
+
}
|
|
118
|
+
.twig-browser .vac-muted {
|
|
119
|
+
color: var(--vac-muted);
|
|
120
|
+
font-size: 13px;
|
|
121
|
+
}
|
|
122
|
+
.twig-browser .vac-row {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
justify-content: space-between;
|
|
126
|
+
gap: 16px;
|
|
127
|
+
}
|
|
128
|
+
.twig-browser .vac-filters {
|
|
129
|
+
padding: 0;
|
|
130
|
+
}
|
|
131
|
+
.twig-browser .vac-filter {
|
|
132
|
+
font: inherit;
|
|
133
|
+
color: inherit;
|
|
134
|
+
background: transparent;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
.twig-browser .vac-filter[aria-pressed="true"] {
|
|
138
|
+
background: var(--vac-ink);
|
|
139
|
+
color: var(--vac-paper);
|
|
140
|
+
}
|
|
141
|
+
.twig-browser button:focus-visible {
|
|
142
|
+
outline: 3px solid var(--vac-ink);
|
|
143
|
+
outline-offset: 3px;
|
|
144
|
+
}
|
|
145
|
+
.twig-browser .vac-image img {
|
|
146
|
+
width: 100%;
|
|
147
|
+
height: 100%;
|
|
148
|
+
position: absolute;
|
|
149
|
+
}
|
|
150
|
+
.twig-browser-grid {
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
153
|
+
gap: 20px;
|
|
154
|
+
margin-top: 20px;
|
|
155
|
+
}
|
|
156
|
+
.twig-browser-hero {
|
|
157
|
+
position: relative;
|
|
158
|
+
padding: 8px 0 28px;
|
|
159
|
+
margin-bottom: 24px;
|
|
160
|
+
border-bottom: 1px solid var(--vac-line);
|
|
161
|
+
}
|
|
162
|
+
.twig-browser-hero h2 {
|
|
163
|
+
font-size: 34px;
|
|
164
|
+
letter-spacing: -0.035em;
|
|
165
|
+
}
|
|
166
|
+
.twig-browser-hero p {
|
|
167
|
+
max-width: 270px;
|
|
168
|
+
}
|
|
169
|
+
.twig-browser-hero img {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
@container (min-width: 500px) {
|
|
173
|
+
.twig-browser-hero {
|
|
174
|
+
padding-right: 170px;
|
|
175
|
+
}
|
|
176
|
+
.twig-browser-hero img {
|
|
177
|
+
display: block;
|
|
178
|
+
position: absolute;
|
|
179
|
+
right: 0;
|
|
180
|
+
bottom: 0;
|
|
181
|
+
width: 150px;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
@container (max-width: 340px) {
|
|
185
|
+
.twig-browser-grid {
|
|
186
|
+
grid-template-columns: minmax(0, 1fr);
|
|
187
|
+
}
|
|
188
|
+
.twig-browser .vac-filter {
|
|
189
|
+
padding: 6px 10px;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Read-only Twig excerpt. All selectors stay inside this Twig-owned root. */
|
|
194
|
+
.twig-stays-preview {
|
|
195
|
+
--twig-paper: #f7eddf;
|
|
196
|
+
--twig-ink: #2d2b29;
|
|
197
|
+
--twig-muted: #6d6258;
|
|
198
|
+
--twig-line: #d7c8b6;
|
|
199
|
+
--twig-fill: #efe1ce;
|
|
200
|
+
box-sizing: border-box;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
background: var(--twig-paper);
|
|
203
|
+
color: var(--twig-ink);
|
|
204
|
+
border: 1px solid var(--twig-line);
|
|
205
|
+
border-radius: 4px;
|
|
206
|
+
font: 16px/1.45 TwigHalfre, sans-serif;
|
|
207
|
+
}
|
|
208
|
+
.twig-stays-preview *,
|
|
209
|
+
.twig-stays-preview *::before,
|
|
210
|
+
.twig-stays-preview *::after {
|
|
211
|
+
box-sizing: border-box;
|
|
212
|
+
}
|
|
213
|
+
.twig-stays-preview-inner {
|
|
214
|
+
padding: 20px;
|
|
215
|
+
}
|
|
216
|
+
.twig-stays-preview-kicker {
|
|
217
|
+
color: var(--twig-muted);
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
text-transform: uppercase;
|
|
220
|
+
}
|
|
221
|
+
.twig-stays-preview h3 {
|
|
222
|
+
margin: 4px 0 18px;
|
|
223
|
+
color: var(--twig-ink);
|
|
224
|
+
font: 600 27px/1.2 TwigHalfre, sans-serif;
|
|
225
|
+
}
|
|
226
|
+
.twig-stays-preview-filters {
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-wrap: wrap;
|
|
229
|
+
gap: 8px;
|
|
230
|
+
}
|
|
231
|
+
.twig-stays-preview-filter {
|
|
232
|
+
padding: 9px 14px;
|
|
233
|
+
border: 1px solid var(--twig-line);
|
|
234
|
+
border-radius: 4px;
|
|
235
|
+
}
|
|
236
|
+
.twig-stays-preview-filter[data-selected="true"] {
|
|
237
|
+
background: var(--twig-ink);
|
|
238
|
+
border-color: var(--twig-ink);
|
|
239
|
+
color: var(--twig-paper);
|
|
240
|
+
}
|
|
241
|
+
.twig-stays-preview-count {
|
|
242
|
+
margin: 16px 0 11px;
|
|
243
|
+
color: var(--twig-muted);
|
|
244
|
+
}
|
|
245
|
+
.twig-stays-preview-stay {
|
|
246
|
+
display: flex;
|
|
247
|
+
align-items: center;
|
|
248
|
+
gap: 14px;
|
|
249
|
+
padding: 9px;
|
|
250
|
+
border: 1px solid var(--twig-line);
|
|
251
|
+
border-radius: 4px;
|
|
252
|
+
}
|
|
253
|
+
.twig-stays-preview-image {
|
|
254
|
+
flex: none;
|
|
255
|
+
width: 68px;
|
|
256
|
+
height: 50px;
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
background: var(--twig-fill);
|
|
259
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Fictional stays. Adiron-shack content supplied by Sarah; other entries remain fixtures. */
|
|
2
|
+
export declare const guestType: "human";
|
|
3
|
+
export type CharacterContent = {
|
|
4
|
+
id: string;
|
|
5
|
+
revision: string | null;
|
|
6
|
+
name: string | null;
|
|
7
|
+
species: string | null;
|
|
8
|
+
portrait: {
|
|
9
|
+
src: string;
|
|
10
|
+
alt: string;
|
|
11
|
+
} | null;
|
|
12
|
+
biography: string | null;
|
|
13
|
+
voiceGuidance: string | null;
|
|
14
|
+
approvedExamples: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
export type Stay = {
|
|
17
|
+
id: string;
|
|
18
|
+
hostId: string;
|
|
19
|
+
title: string | null;
|
|
20
|
+
setting: "Forest" | "Coast" | "Mountain";
|
|
21
|
+
location: string | null;
|
|
22
|
+
images: readonly {
|
|
23
|
+
src: string;
|
|
24
|
+
alt: string;
|
|
25
|
+
source: string;
|
|
26
|
+
creditUrl?: string;
|
|
27
|
+
}[];
|
|
28
|
+
nightlyRate: number;
|
|
29
|
+
currency: "USD";
|
|
30
|
+
capacity: number | null;
|
|
31
|
+
amenities: readonly string[];
|
|
32
|
+
cancellationPolicy: string | null;
|
|
33
|
+
accommodationType?: string;
|
|
34
|
+
vibe?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
bedrooms?: number;
|
|
37
|
+
bathrooms?: number;
|
|
38
|
+
sleepingArrangements?: readonly string[];
|
|
39
|
+
lakeAccess?: string;
|
|
40
|
+
photoCredit?: string;
|
|
41
|
+
};
|
|
42
|
+
export declare const characters: readonly CharacterContent[];
|
|
43
|
+
export declare const conciergeContent: {
|
|
44
|
+
revision: null;
|
|
45
|
+
name: null;
|
|
46
|
+
voiceGuidance: null;
|
|
47
|
+
approvedExamples: never[];
|
|
48
|
+
};
|
|
49
|
+
export declare const stays: readonly Stay[];
|
|
50
|
+
export declare function stayLabel(stay: Stay): string;
|
|
51
|
+
export declare function nightlyPrice(stay: Stay): string;
|
|
52
|
+
export declare function filterStays(setting: "All" | Stay["setting"], search?: string): Stay[];
|
|
53
|
+
export declare const discoveryCopy: {
|
|
54
|
+
title: string;
|
|
55
|
+
subtitle: string;
|
|
56
|
+
introduction: string;
|
|
57
|
+
};
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Fictional stays. Adiron-shack content supplied by Sarah; other entries remain fixtures. */
|
|
2
|
+
export const guestType = "human";
|
|
3
|
+
// Sarah supplies all character fields. IDs are technical references, not names.
|
|
4
|
+
export const characters = [
|
|
5
|
+
{
|
|
6
|
+
id: "woodrow-sparks",
|
|
7
|
+
revision: "2026-09-21",
|
|
8
|
+
name: "Woodrow “Woody” Sparks",
|
|
9
|
+
species: "Pileated woodpecker",
|
|
10
|
+
portrait: null,
|
|
11
|
+
biography: "I came up for Woodstock and never quite got around to leaving. I believe in sharing the harvest, looking after the land, and not letting a perfectly good afternoon become productive. You humans seem to struggle with that last one. Come sit by the fire. We’ll work on it. The chickens have asked me to clarify that they are residents, not amenities.",
|
|
12
|
+
voiceGuidance: "Sarah’s character direction: a laid-back hippie who never left upstate New York after Woodstock, but migrated to a cabin upstate. Keeps chickens; big into sharecropping and saving the environment. Write in first person with warm, deadpan observations about human habits from an animal’s perspective. Be a welcoming host who finds humans mildly baffling. Keep practical facts clear; use occasional understated jokes, not a punchline in every sentence. Sarah requested Bear & Breakfast as a tonal reference; write original wording and retain Woody’s own character.",
|
|
13
|
+
approvedExamples: [],
|
|
14
|
+
},
|
|
15
|
+
...[1, 2, 3].map((index) => ({
|
|
16
|
+
id: `host-${index}`,
|
|
17
|
+
revision: null,
|
|
18
|
+
name: null,
|
|
19
|
+
species: null,
|
|
20
|
+
portrait: null,
|
|
21
|
+
biography: null,
|
|
22
|
+
voiceGuidance: null,
|
|
23
|
+
approvedExamples: [],
|
|
24
|
+
})),
|
|
25
|
+
];
|
|
26
|
+
export const conciergeContent = {
|
|
27
|
+
revision: null,
|
|
28
|
+
name: null,
|
|
29
|
+
voiceGuidance: null,
|
|
30
|
+
approvedExamples: [],
|
|
31
|
+
};
|
|
32
|
+
export const stays = [
|
|
33
|
+
{
|
|
34
|
+
id: "stay-01",
|
|
35
|
+
hostId: "woodrow-sparks",
|
|
36
|
+
setting: "Forest",
|
|
37
|
+
title: "Adiron-shack",
|
|
38
|
+
location: "Adirondacks, New York",
|
|
39
|
+
accommodationType: "A-frame cabin",
|
|
40
|
+
vibe: "Get me out of the city. Preferably near a fire pit.",
|
|
41
|
+
description: "An A-frame cabin in the Adirondacks for four humans. Two king bedrooms, each with a private bathroom. Humans have strong feelings about doors. There’s a path down to the lake, a grill, and a back patio with a fire pit. Woody’s chickens live on the property, too. They’ve been enjoying the outdoors without booking anything.",
|
|
42
|
+
nightlyRate: 355,
|
|
43
|
+
currency: "USD",
|
|
44
|
+
capacity: 4,
|
|
45
|
+
bedrooms: 2,
|
|
46
|
+
bathrooms: 2,
|
|
47
|
+
sleepingArrangements: [
|
|
48
|
+
"King bed · Private bathroom",
|
|
49
|
+
"King bed · Private bathroom",
|
|
50
|
+
],
|
|
51
|
+
amenities: [
|
|
52
|
+
"Lake access via a walking path",
|
|
53
|
+
"Fireplace / fire pit",
|
|
54
|
+
"Grill",
|
|
55
|
+
"Back patio",
|
|
56
|
+
],
|
|
57
|
+
lakeAccess: "The cabin is not lakefront. A walking path leads down to the lake.",
|
|
58
|
+
cancellationPolicy: null,
|
|
59
|
+
images: [
|
|
60
|
+
{
|
|
61
|
+
src: "/twig/stays/adiron-shack/cabin.jpg",
|
|
62
|
+
alt: "Dark A-frame cabin among autumn trees, with a gravel path leading to the entrance at dusk",
|
|
63
|
+
source: "clay-banks-gmQr2-qWkYQ-unsplash.jpg",
|
|
64
|
+
creditUrl: "https://unsplash.com/photos/a-small-black-cabin-in-the-middle-of-a-forest-gmQr2-qWkYQ",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
src: "/twig/stays/adiron-shack/patio.jpg",
|
|
68
|
+
alt: "A-frame cabin with a back patio, string lights, and chairs around an outdoor fire pit",
|
|
69
|
+
source: "clay-banks-AZDyU1hXtCE-unsplash.jpg",
|
|
70
|
+
creditUrl: "https://unsplash.com/photos/a-frame-cabin-in-the-woods-with-a-green-door-DS2rtg-Nbgk",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
photoCredit: "Clay Banks / Unsplash",
|
|
74
|
+
},
|
|
75
|
+
...[
|
|
76
|
+
{ id: "stay-02", hostId: "host-2", setting: "Coast", nightlyRate: 180 },
|
|
77
|
+
{ id: "stay-03", hostId: "host-3", setting: "Mountain", nightlyRate: 160 },
|
|
78
|
+
{ id: "stay-04", hostId: "host-1", setting: "Forest", nightlyRate: 190 },
|
|
79
|
+
].map((stay) => ({
|
|
80
|
+
...stay,
|
|
81
|
+
setting: stay.setting,
|
|
82
|
+
title: null,
|
|
83
|
+
location: null,
|
|
84
|
+
images: [],
|
|
85
|
+
currency: "USD",
|
|
86
|
+
capacity: null,
|
|
87
|
+
amenities: [],
|
|
88
|
+
cancellationPolicy: null,
|
|
89
|
+
})),
|
|
90
|
+
];
|
|
91
|
+
export function stayLabel(stay) {
|
|
92
|
+
return stay.title ?? `${stay.setting} stay`;
|
|
93
|
+
}
|
|
94
|
+
export function nightlyPrice(stay) {
|
|
95
|
+
return new Intl.NumberFormat("en-US", {
|
|
96
|
+
style: "currency",
|
|
97
|
+
currency: stay.currency,
|
|
98
|
+
maximumFractionDigits: 0,
|
|
99
|
+
}).format(stay.nightlyRate);
|
|
100
|
+
}
|
|
101
|
+
export function filterStays(setting, search = "") {
|
|
102
|
+
return stays.filter((stay) => (setting === "All" || stay.setting === setting) &&
|
|
103
|
+
`${stay.title ?? ""} ${stay.location ?? ""} ${stay.setting}`
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.includes(search.toLowerCase()));
|
|
106
|
+
}
|
|
107
|
+
export const discoveryCopy = {
|
|
108
|
+
title: "Vacation rentals.",
|
|
109
|
+
subtitle: "Run by birds.",
|
|
110
|
+
introduction: "Birds know how to build a home. Now they’re designing your next getaway.",
|
|
111
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type LabConfig = {
|
|
2
|
+
eventName: string;
|
|
3
|
+
source: "clicked" | "fixed";
|
|
4
|
+
fixedValue: string;
|
|
5
|
+
};
|
|
6
|
+
export type LabEvent = {
|
|
7
|
+
id: number;
|
|
8
|
+
event: string;
|
|
9
|
+
clicked: string;
|
|
10
|
+
properties: {
|
|
11
|
+
destination_type: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type LabState = {
|
|
15
|
+
config: LabConfig;
|
|
16
|
+
applied: LabConfig | null;
|
|
17
|
+
step: number;
|
|
18
|
+
selecting: boolean;
|
|
19
|
+
selected: boolean;
|
|
20
|
+
events: LabEvent[];
|
|
21
|
+
before: LabEvent[];
|
|
22
|
+
};
|
|
23
|
+
export declare const initialLabState: LabState;
|
|
24
|
+
export type LabAction = {
|
|
25
|
+
type: "example";
|
|
26
|
+
} | {
|
|
27
|
+
type: "repair";
|
|
28
|
+
} | {
|
|
29
|
+
type: "select";
|
|
30
|
+
} | {
|
|
31
|
+
type: "cancel";
|
|
32
|
+
} | {
|
|
33
|
+
type: "target";
|
|
34
|
+
} | {
|
|
35
|
+
type: "edit";
|
|
36
|
+
config: Partial<LabConfig>;
|
|
37
|
+
} | {
|
|
38
|
+
type: "step";
|
|
39
|
+
step: number;
|
|
40
|
+
} | {
|
|
41
|
+
type: "apply";
|
|
42
|
+
} | {
|
|
43
|
+
type: "filter";
|
|
44
|
+
setting: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: "clear";
|
|
47
|
+
} | {
|
|
48
|
+
type: "reset";
|
|
49
|
+
};
|
|
50
|
+
export declare function validEventName(name: string): boolean;
|
|
51
|
+
export declare function filterLabReducer(state: LabState, action: LabAction): LabState;
|
|
52
|
+
export declare function labPassed(state: LabState): boolean;
|
|
53
|
+
export declare function latestFilterFeedback(state: LabState): string | null;
|
|
54
|
+
export declare function filterLabCode(config: LabConfig): string;
|
|
55
|
+
export declare function summarizeFilterEvents(events: readonly LabEvent[]): {
|
|
56
|
+
rows: {
|
|
57
|
+
setting: string;
|
|
58
|
+
actual: number;
|
|
59
|
+
recorded: number;
|
|
60
|
+
}[];
|
|
61
|
+
mismatches: number;
|
|
62
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export const initialLabState = {
|
|
2
|
+
config: { eventName: "", source: "fixed", fixedValue: "Forest" },
|
|
3
|
+
applied: null,
|
|
4
|
+
step: 0,
|
|
5
|
+
selecting: false,
|
|
6
|
+
selected: false,
|
|
7
|
+
events: [],
|
|
8
|
+
before: [],
|
|
9
|
+
};
|
|
10
|
+
export function validEventName(name) {
|
|
11
|
+
return /^[a-z][a-z0-9_]{0,63}$/.test(name);
|
|
12
|
+
}
|
|
13
|
+
export function filterLabReducer(state, action) {
|
|
14
|
+
switch (action.type) {
|
|
15
|
+
case "example": {
|
|
16
|
+
const config = {
|
|
17
|
+
eventName: "stay_filter_selected",
|
|
18
|
+
source: "fixed",
|
|
19
|
+
fixedValue: "Forest",
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
...initialLabState,
|
|
23
|
+
config,
|
|
24
|
+
applied: { ...config },
|
|
25
|
+
selected: true,
|
|
26
|
+
step: 3,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
case "repair":
|
|
30
|
+
return {
|
|
31
|
+
...state,
|
|
32
|
+
before: state.applied?.source === "fixed" && state.events.length
|
|
33
|
+
? [...state.events]
|
|
34
|
+
: state.before,
|
|
35
|
+
applied: null,
|
|
36
|
+
events: [],
|
|
37
|
+
step: 2,
|
|
38
|
+
};
|
|
39
|
+
case "reset":
|
|
40
|
+
return initialLabState;
|
|
41
|
+
case "clear":
|
|
42
|
+
return { ...state, events: [] };
|
|
43
|
+
case "select":
|
|
44
|
+
return { ...state, selecting: true };
|
|
45
|
+
case "cancel":
|
|
46
|
+
return { ...state, selecting: false };
|
|
47
|
+
case "target":
|
|
48
|
+
return state.selecting
|
|
49
|
+
? {
|
|
50
|
+
...state,
|
|
51
|
+
selected: true,
|
|
52
|
+
selecting: false,
|
|
53
|
+
step: 1,
|
|
54
|
+
applied: null,
|
|
55
|
+
events: [],
|
|
56
|
+
}
|
|
57
|
+
: state;
|
|
58
|
+
case "edit":
|
|
59
|
+
return {
|
|
60
|
+
...state,
|
|
61
|
+
config: { ...state.config, ...action.config },
|
|
62
|
+
applied: null,
|
|
63
|
+
events: [],
|
|
64
|
+
};
|
|
65
|
+
case "step":
|
|
66
|
+
return { ...state, step: action.step, selecting: false };
|
|
67
|
+
case "apply":
|
|
68
|
+
return state.selected &&
|
|
69
|
+
validEventName(state.config.eventName) &&
|
|
70
|
+
(state.config.source === "clicked" || state.config.fixedValue.trim())
|
|
71
|
+
? { ...state, applied: { ...state.config }, events: [], step: 3 }
|
|
72
|
+
: state;
|
|
73
|
+
case "filter":
|
|
74
|
+
return !state.applied || state.events.length >= 20
|
|
75
|
+
? state
|
|
76
|
+
: {
|
|
77
|
+
...state,
|
|
78
|
+
events: [
|
|
79
|
+
...state.events,
|
|
80
|
+
{
|
|
81
|
+
id: state.events.length + 1,
|
|
82
|
+
event: state.applied.eventName,
|
|
83
|
+
clicked: action.setting,
|
|
84
|
+
properties: {
|
|
85
|
+
destination_type: state.applied.source === "clicked"
|
|
86
|
+
? action.setting
|
|
87
|
+
: state.applied.fixedValue,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function labPassed(state) {
|
|
95
|
+
return (["Forest", "Coast"].every((setting) => state.events.some((event) => event.clicked === setting &&
|
|
96
|
+
event.properties.destination_type === setting)) &&
|
|
97
|
+
state.events.every((event) => event.clicked === event.properties.destination_type));
|
|
98
|
+
}
|
|
99
|
+
export function latestFilterFeedback(state) {
|
|
100
|
+
const latest = state.events.at(-1);
|
|
101
|
+
if (!latest)
|
|
102
|
+
return null;
|
|
103
|
+
const recorded = latest.properties.destination_type;
|
|
104
|
+
if (latest.clicked !== recorded) {
|
|
105
|
+
return `You clicked ${latest.clicked}, but recorded ${recorded}. A fixed value stays the same for every click. Change the property source, apply, and test again.`;
|
|
106
|
+
}
|
|
107
|
+
if (state.applied?.source === "fixed") {
|
|
108
|
+
const other = latest.clicked === "Coast" ? "Forest" : "Coast";
|
|
109
|
+
return `Correct for this click: ${latest.clicked} recorded ${recorded}. But clicking ${other} would also record ${recorded}, because the value is fixed. Use the clicked filter’s value to handle both.`;
|
|
110
|
+
}
|
|
111
|
+
return `Correct: ${latest.clicked} recorded ${recorded}. The property follows the filter you clicked.`;
|
|
112
|
+
}
|
|
113
|
+
export function filterLabCode(config) {
|
|
114
|
+
return `// Inside the filter's click handler\nposthog.capture(${JSON.stringify(config.eventName || "your_event_name")}, {\n destination_type: ${config.source === "clicked"
|
|
115
|
+
? "selectedDestinationType"
|
|
116
|
+
: JSON.stringify(config.fixedValue)},\n});`;
|
|
117
|
+
}
|
|
118
|
+
export function summarizeFilterEvents(events) {
|
|
119
|
+
const rows = new Map();
|
|
120
|
+
const row = (setting) => {
|
|
121
|
+
if (!rows.has(setting))
|
|
122
|
+
rows.set(setting, { setting, actual: 0, recorded: 0 });
|
|
123
|
+
return rows.get(setting);
|
|
124
|
+
};
|
|
125
|
+
for (const event of events) {
|
|
126
|
+
row(event.clicked).actual++;
|
|
127
|
+
row(event.properties.destination_type).recorded++;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
rows: [...rows.values()],
|
|
131
|
+
mismatches: events.filter((event) => event.clicked !== event.properties.destination_type).length,
|
|
132
|
+
};
|
|
133
|
+
}
|