tripkit 1.0.1 → 1.2.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.
@@ -0,0 +1,463 @@
1
+ # ============================================================
2
+ # Southwest National Parks Loop 2026
3
+ # 5-day Utah / Arizona red-rock loop from Las Vegas
4
+ # Showcases: heavy hiking, desert palette, multi-park logistics
5
+ # ============================================================
6
+
7
+ trip:
8
+ title: "Southwest Parks Loop 2026"
9
+ subtitle: "Zion · Bryce · Page · Grand Canyon"
10
+ dates: "May 2–6, 2026"
11
+ total_days: 5
12
+ total_miles: "~900 mi"
13
+ total_stops: 13
14
+ travelers:
15
+ adults: 2
16
+ children: 0
17
+ origin: "Las Vegas, NV (fly in / fly out)"
18
+ origin_lat: 36.084
19
+ origin_lng: -115.154
20
+ vehicle: "Rental SUV"
21
+
22
+ days:
23
+ # ========== DAY 1 ==========
24
+ - number: 1
25
+ title: "Las Vegas → Zion"
26
+ date: "Saturday, May 2"
27
+ status: upcoming
28
+ color: "#c25e1f"
29
+
30
+ summary:
31
+ drive: "3 hrs"
32
+ hike: "1–2 hrs"
33
+ miles: "~165 mi"
34
+
35
+ weather:
36
+ high: "82°F"
37
+ low: "55°F"
38
+ sky: "Sunny, dry"
39
+ rain_chance: "0%"
40
+ note: "Bring a hat and 2L of water minimum — desert sun is no joke."
41
+
42
+ meals:
43
+ breakfast: "In-N-Out near LAS rental car return — quick fuel"
44
+ lunch: "Valley of Fire visitor center picnic area"
45
+ dinner: "Oscar's Cafe, Springdale — patio overlooks the canyon at sunset"
46
+
47
+ lodging:
48
+ name: "Best Western Plus Zion Canyon Inn & Suites"
49
+ location: "Springdale, Utah"
50
+ price_estimate: "~$220/night"
51
+ confirmation: "XXXXX1234"
52
+ booked: true
53
+ lat: 37.1924
54
+ lng: -112.9870
55
+ notes: "Walk to Zion shuttle stop. Pool, free breakfast, mountain views."
56
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=Best+Western+Plus+Zion+Canyon+Inn+Springdale+UT"
57
+
58
+ tips:
59
+ - "Park the rental in Springdale — Zion's canyon is shuttle-only Mar–Nov."
60
+ - "Watchman Trail is the perfect golden-hour hike — start at 5 PM, summit by sunset."
61
+ - "Refill water bottles at every visitor center; spigots are everywhere."
62
+
63
+ stops:
64
+ - name: "Valley of Fire State Park"
65
+ lat: 36.4307
66
+ lng: -114.5147
67
+ type: scenic
68
+ label: "Detour"
69
+ description: "60-min detour off I-15. Mars-red Aztec sandstone formations, petroglyphs, the Fire Wave. Drive the loop and stop at Mouse's Tank."
70
+ duration: "1.5–2 hrs"
71
+ parking_fee: "$15 NV non-resident"
72
+ kid_friendly: true
73
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=36.4307,-114.5147"
74
+
75
+ - name: "Zion Visitor Center"
76
+ lat: 37.2002
77
+ lng: -112.9871
78
+ type: scenic
79
+ label: "Stop"
80
+ description: "Pick up the shuttle here. Grab a Zion map, fill water bottles, check Angel's Landing permit status board."
81
+ duration: "20–30 min"
82
+ parking_fee: "$35 park fee (7-day) or America the Beautiful pass"
83
+ kid_friendly: true
84
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=37.2002,-112.9871"
85
+
86
+ - name: "Watchman Trail (sunset)"
87
+ lat: 37.1996
88
+ lng: -112.9871
89
+ type: hike
90
+ label: "Sunset"
91
+ description: "3.3-mile out-and-back from the visitor center. Modest 370 ft climb to a viewpoint over the West Temple and Watchman peak. Light up like fire at sunset."
92
+ duration: "2 hrs"
93
+ parking_fee: "Included with park fee"
94
+ kid_friendly: true
95
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=37.1996,-112.9871"
96
+
97
+ # ========== DAY 2 ==========
98
+ - number: 2
99
+ title: "Zion full day"
100
+ date: "Sunday, May 3"
101
+ status: upcoming
102
+ color: "#a04020"
103
+
104
+ summary:
105
+ drive: "Shuttle only"
106
+ hike: "5–7 hrs"
107
+ miles: "0"
108
+
109
+ weather:
110
+ high: "84°F"
111
+ low: "58°F"
112
+ sky: "Sunny, light wind"
113
+ rain_chance: "0%"
114
+ note: "Start hikes by 7 AM — the canyon walls trap heat by 11."
115
+
116
+ meals:
117
+ breakfast: "Hotel breakfast or Deep Creek Coffee, Springdale"
118
+ lunch: "Zion Lodge Red Rock Grill (sit on the lawn under cottonwoods)"
119
+ dinner: "Bit & Spur — Mexican, locally famous since the 80s"
120
+
121
+ lodging:
122
+ name: "Best Western Plus Zion Canyon Inn & Suites"
123
+ location: "Springdale, Utah"
124
+ price_estimate: "~$220/night"
125
+ confirmation: "XXXXX1234"
126
+ booked: true
127
+ lat: 37.1924
128
+ lng: -112.9870
129
+ notes: "Second night — keep the same room"
130
+
131
+ alerts:
132
+ - "🪨 Angel's Landing requires a permit (lottery + day-before). If you didn't get one, do Observation Point via East Mesa Trail instead — same view, no permit, less crowded."
133
+
134
+ tips:
135
+ - "Take the first shuttle (6 AM) to beat heat and crowds for Angel's Landing."
136
+ - "The Narrows from the bottom (Riverside Walk → Mystery Falls) is wadeable in May with rented neoprene boots from Zion Outfitter."
137
+ - "Emerald Pools loop is a perfect afternoon recovery hike — shaded, gentle, gorgeous."
138
+
139
+ stops:
140
+ - name: "Angel's Landing"
141
+ lat: 37.2581
142
+ lng: -112.9514
143
+ type: hike
144
+ label: "Big hike"
145
+ description: "5.4 miles, 1,488 ft climb. Walter's Wiggles switchbacks then the chain section ridge. Permit required. One of the most exposed hikes in any US park — not for vertigo."
146
+ duration: "4–5 hrs"
147
+ reservation_required: true
148
+ reservation_url: "https://www.recreation.gov/permits/angels-landing"
149
+ kid_friendly: false
150
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=37.2581,-112.9514"
151
+
152
+ - name: "Emerald Pools Loop"
153
+ lat: 37.2497
154
+ lng: -112.9588
155
+ type: hike
156
+ label: "Easy"
157
+ description: "Lower (paved, 1.2 mi) → Middle → Upper. Hanging gardens, seep springs, water curtain over an alcove. Best in the afternoon when sun hits the upper bowl."
158
+ duration: "1.5–2 hrs"
159
+ kid_friendly: true
160
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=37.2497,-112.9588"
161
+
162
+ - name: "Riverside Walk → Narrows mouth"
163
+ lat: 37.2856
164
+ lng: -112.9479
165
+ type: scenic
166
+ label: "Stroll"
167
+ description: "Paved 2-mile out-and-back to where the Virgin River disappears between 1,000 ft cliffs. Wade in 50 ft if you brought water shoes."
168
+ duration: "1–1.5 hrs"
169
+ kid_friendly: true
170
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=37.2856,-112.9479"
171
+
172
+ # ========== DAY 3 ==========
173
+ - number: 3
174
+ title: "Zion → Bryce → Page"
175
+ date: "Monday, May 4"
176
+ status: upcoming
177
+ color: "#d97a3c"
178
+
179
+ summary:
180
+ drive: "5 hrs"
181
+ hike: "2 hrs"
182
+ miles: "~270 mi"
183
+
184
+ weather:
185
+ high: "72°F (Bryce 8000 ft)"
186
+ low: "38°F"
187
+ sky: "Sunny, breezy at altitude"
188
+ rain_chance: "10%"
189
+ note: "Bryce is 4,000 ft higher than Zion — bring a fleece even in May."
190
+
191
+ meals:
192
+ breakfast: "Hotel breakfast"
193
+ lunch: "Bryce Canyon Lodge dining room — cafeteria + sit-down side"
194
+ dinner: "El Tapatio, Page — best Mexican in town, walk from hotel"
195
+
196
+ lodging:
197
+ name: "Best Western View of Lake Powell"
198
+ location: "Page, Arizona"
199
+ price_estimate: "~$165/night"
200
+ confirmation: "XXXXX5678"
201
+ booked: true
202
+ lat: 36.9097
203
+ lng: -111.4587
204
+ notes: "Hilltop with Lake Powell views. Pool, free breakfast, easy Antelope Canyon shuttle pickup."
205
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=Best+Western+View+of+Lake+Powell+Page+AZ"
206
+
207
+ tips:
208
+ - "Drive Hwy 12 — one of America's prettiest scenic byways. Slot in 30 min for the Hogsback ridge between Boulder and Escalante."
209
+ - "At Bryce, walk the rim from Sunrise to Sunset Point (1 mi) for the best free view of the amphitheater."
210
+ - "Time the Page arrival for sunset at Horseshoe Bend tomorrow morning — too crowded after 9 AM."
211
+
212
+ stops:
213
+ - name: "Bryce Canyon — Sunrise & Sunset Points"
214
+ lat: 37.6248
215
+ lng: -112.1620
216
+ type: scenic
217
+ label: "Rim walk"
218
+ description: "Two of the most-photographed viewpoints on the rim. 1-mile flat connector along the edge looks down on Thor's Hammer and the Silent City of hoodoos."
219
+ duration: "30–45 min"
220
+ kid_friendly: true
221
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=37.6248,-112.1620"
222
+
223
+ - name: "Navajo Loop / Queen's Garden"
224
+ lat: 37.6238
225
+ lng: -112.1640
226
+ type: hike
227
+ label: "Hoodoo hike"
228
+ description: "Best 3-mile hike in any US park, full stop. Drop down Wall Street switchbacks, weave through hoodoos, come back up Queen's Garden. Do it counter-clockwise."
229
+ duration: "2 hrs"
230
+ kid_friendly: true
231
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=37.6238,-112.1640"
232
+
233
+ # ========== DAY 4 ==========
234
+ - number: 4
235
+ title: "Page → Grand Canyon South Rim"
236
+ date: "Tuesday, May 5"
237
+ status: upcoming
238
+ color: "#7d3c1a"
239
+
240
+ summary:
241
+ drive: "3 hrs"
242
+ hike: "1.5 hrs"
243
+ miles: "~140 mi"
244
+
245
+ weather:
246
+ high: "75°F (rim)"
247
+ low: "42°F"
248
+ sky: "Clear"
249
+ rain_chance: "5%"
250
+ note: "Sunset at Hopi Point is the picture you've seen on every postcard."
251
+
252
+ meals:
253
+ breakfast: "Hotel breakfast — early start for Horseshoe Bend"
254
+ lunch: "Cameron Trading Post (en route) — Navajo tacos in a 1916 stone post"
255
+ dinner: "El Tovar Dining Room (book ahead) or Bright Angel Restaurant"
256
+
257
+ lodging:
258
+ name: "Best Western Premier Grand Canyon Squire Inn"
259
+ location: "Tusayan, Arizona"
260
+ price_estimate: "~$280/night"
261
+ confirmation: "XXXXX9012"
262
+ booked: true
263
+ lat: 35.9684
264
+ lng: -112.1218
265
+ notes: "1 mile south of the park entrance. Bowling alley, two pools, free shuttle into the park."
266
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=Best+Western+Premier+Grand+Canyon+Squire+Inn+Tusayan+AZ"
267
+
268
+ alerts:
269
+ - "📅 Antelope Canyon entry is by Navajo guide only — book the Lower Canyon tour 4–6 weeks ahead through Ken's Tours or Dixie Ellis'."
270
+
271
+ tips:
272
+ - "Horseshoe Bend opens at sunrise — be there by 6:30 AM to avoid tour bus crush."
273
+ - "Stop at the Watchtower (Desert View) on the way in — eastern entrance to GCNP saves 30 min vs going through Tusayan first."
274
+ - "Sunset shuttle from Bright Angel Lodge to Hopi Point runs every 15 min, free."
275
+
276
+ stops:
277
+ - name: "Horseshoe Bend"
278
+ lat: 36.8625
279
+ lng: -111.5103
280
+ type: scenic
281
+ label: "Sunrise"
282
+ description: "1.5-mile round trip from the parking lot to the overlook. The Colorado River carves a 270° loop around a sandstone fin 1,000 ft below. Bring a wide-angle lens."
283
+ duration: "1 hr"
284
+ parking_fee: "$10 City of Page"
285
+ kid_friendly: true
286
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=36.8625,-111.5103"
287
+
288
+ - name: "Lower Antelope Canyon"
289
+ lat: 36.8619
290
+ lng: -111.4133
291
+ type: scenic
292
+ label: "Slot canyon"
293
+ description: "Guided tour through a slot canyon carved by flash floods. Wavy walls glow orange and purple in shafts of sunlight. Easier than Upper for photos (less crowded, cheaper)."
294
+ duration: "1.5 hrs"
295
+ reservation_required: true
296
+ reservation_url: "https://lowerantelope.com"
297
+ parking_fee: "Included in tour"
298
+ kid_friendly: true
299
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=36.8619,-111.4133"
300
+
301
+ - name: "Mather Point (sunset)"
302
+ lat: 36.0613
303
+ lng: -112.1080
304
+ type: scenic
305
+ label: "Sunset"
306
+ description: "First view of the Grand Canyon for most arriving visitors, and still the best for impact. 5-min walk from the visitor center."
307
+ duration: "30–45 min"
308
+ kid_friendly: true
309
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=36.0613,-112.1080"
310
+
311
+ # ========== DAY 5 ==========
312
+ - number: 5
313
+ title: "Grand Canyon → Las Vegas"
314
+ date: "Wednesday, May 6"
315
+ status: upcoming
316
+ color: "#5c2a14"
317
+
318
+ summary:
319
+ drive: "4.5 hrs"
320
+ hike: "1.5–2 hrs"
321
+ miles: "~280 mi"
322
+
323
+ weather:
324
+ high: "78°F"
325
+ low: "44°F"
326
+ sky: "Clear, gusty PM"
327
+ rain_chance: "0%"
328
+ note: "Last day — pace it for the LAS evening flight."
329
+
330
+ meals:
331
+ breakfast: "Bright Angel Lodge — sunrise window seat if you can get one"
332
+ lunch: "Hoover Dam visitor center cafeteria, or push through to Boulder City for In-N-Out"
333
+ dinner: "Vegas — depends on flight time"
334
+
335
+ lodging:
336
+ name: "Home"
337
+ location: "Returning home"
338
+ booked: true
339
+ lat: 36.0840
340
+ lng: -115.1537
341
+ notes: "Flight out of LAS — return rental by 7 PM"
342
+
343
+ tips:
344
+ - "Bright Angel Trail down to 1.5-mile Resthouse and back is a perfect taste of below-the-rim without committing to Plateau Point."
345
+ - "Hoover Dam adds 20 min off Hwy 93 — totally worth it. Don't bother with the dam tour, just walk across the bridge."
346
+ - "Drop the rental 2 hrs before your flight — LAS rental return shuttle runs slow."
347
+
348
+ stops:
349
+ - name: "Bright Angel Trail (1.5-mile Resthouse)"
350
+ lat: 36.0578
351
+ lng: -112.1438
352
+ type: hike
353
+ label: "Half-day"
354
+ description: "Out-and-back to the 1.5-mile Resthouse — 3 miles, 1,100 ft of elevation (mostly down on the way in, all up on the way out). Mules pass you. Real canyon experience."
355
+ duration: "2 hrs"
356
+ kid_friendly: true
357
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=36.0578,-112.1438"
358
+
359
+ - name: "Hoover Dam"
360
+ lat: 36.0162
361
+ lng: -114.7377
362
+ type: scenic
363
+ label: "Engineering"
364
+ description: "Walk across the Mike O'Callaghan–Pat Tillman Memorial Bridge for the best photo of the dam. Free, 15 min, blows minds."
365
+ duration: "30–45 min"
366
+ parking_fee: "$10 dam parking, or free at the bridge lot"
367
+ kid_friendly: true
368
+ navigate_url: "https://www.google.com/maps/dir/?api=1&destination=36.0162,-114.7377"
369
+
370
+ # ============================================================
371
+ # ROUTES — explicit polylines that follow real road geometry
372
+ # (I-15, US-89, UT-9, UT-12, US-93). Auto-generation works fine
373
+ # without this block, but hand-curated waypoints look smoother.
374
+ # ============================================================
375
+ routes:
376
+ # Day 1: Vegas → Valley of Fire → I-15 → St. George → Springdale
377
+ - day: 1
378
+ color: "#c25e1f"
379
+ width: 4
380
+ points:
381
+ - [36.084, -115.154]
382
+ - [36.430, -114.515]
383
+ - [37.097, -113.568]
384
+ - [37.175, -113.283]
385
+ - [37.192, -112.987]
386
+
387
+ # Day 2: Zion canyon shuttle loop
388
+ - day: 2
389
+ color: "#a04020"
390
+ width: 4
391
+ points:
392
+ - [37.192, -112.987]
393
+ - [37.258, -112.951]
394
+ - [37.250, -112.959]
395
+ - [37.286, -112.948]
396
+ - [37.192, -112.987]
397
+
398
+ # Day 3: Springdale → Bryce → Kanab → Page
399
+ - day: 3
400
+ color: "#d97a3c"
401
+ width: 4
402
+ points:
403
+ - [37.192, -112.987]
404
+ - [37.342, -112.642]
405
+ - [37.524, -112.227]
406
+ - [37.624, -112.162]
407
+ - [37.524, -112.227]
408
+ - [37.045, -112.527]
409
+ - [36.910, -111.459]
410
+
411
+ # Day 4: Page → Horseshoe → Antelope → Cameron → Grand Canyon → Tusayan
412
+ - day: 4
413
+ color: "#7d3c1a"
414
+ width: 4
415
+ points:
416
+ - [36.910, -111.459]
417
+ - [36.862, -111.510]
418
+ - [36.862, -111.413]
419
+ - [35.875, -111.413]
420
+ - [36.061, -112.108]
421
+ - [35.968, -112.122]
422
+
423
+ # Day 5: Tusayan → Bright Angel → Williams → Kingman → Hoover → Vegas
424
+ - day: 5
425
+ color: "#5c2a14"
426
+ width: 4
427
+ points:
428
+ - [35.968, -112.122]
429
+ - [36.058, -112.144]
430
+ - [35.250, -112.190]
431
+ - [35.189, -114.053]
432
+ - [36.016, -114.738]
433
+ - [36.084, -115.154]
434
+
435
+ theme:
436
+ font_family: "DM Sans, sans-serif"
437
+ accent_color: "#c25e1f"
438
+ map_style: "terrain"
439
+
440
+ agent_context:
441
+ preferences:
442
+ pace: "moderate"
443
+ budget: "mid-range"
444
+ accommodation_chain: "Best Western"
445
+ interests:
446
+ - "national parks"
447
+ - "hiking"
448
+ - "geology"
449
+ - "photography"
450
+ constraints:
451
+ max_drive_per_day: "5 hours"
452
+ must_see:
453
+ - "Angel's Landing or Observation Point"
454
+ - "Bryce hoodoos"
455
+ - "Horseshoe Bend at sunrise"
456
+ - "Grand Canyon South Rim sunset"
457
+ iteration_log:
458
+ - date: "2026-04-15"
459
+ change: "Original plan included North Rim — dropped because it doesn't open until May 15."
460
+ - date: "2026-04-18"
461
+ change: "Swapped Upper Antelope for Lower Antelope — half the price, better photos, easier ladders."
462
+ - date: "2026-04-22"
463
+ change: "Added Hoover Dam to Day 5 — only adds 20 min and makes the LAS approach more interesting."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tripkit",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Open-source framework for AI-assisted trip planning with beautiful interactive visualizers",
5
5
  "main": "convert.js",
6
6
  "bin": {
@@ -8,9 +8,14 @@
8
8
  },
9
9
  "files": [
10
10
  "convert.js",
11
+ "validate.js",
12
+ "scripts/check-skill-coverage.js",
11
13
  "renderers/html/tripkit-renderer.html",
12
14
  "schema/tripkit.schema.yaml",
13
15
  "examples/oregon-spring-2026.yaml",
16
+ "examples/southwest-parks-2026.yaml",
17
+ "examples/nyc-long-weekend-2026.yaml",
18
+ "examples/new-england-fall-2026.yaml",
14
19
  "agent/questionnaire.yaml",
15
20
  "agent/AGENT-SKILL.md",
16
21
  "README.md",
@@ -20,7 +25,7 @@
20
25
  "scripts": {
21
26
  "convert": "node convert.js",
22
27
  "example": "node convert.js examples/oregon-spring-2026.yaml examples/oregon-spring-2026.html",
23
- "test": "node convert.js examples/oregon-spring-2026.yaml /tmp/tripkit-test.html && test -s /tmp/tripkit-test.html && echo 'OK'",
28
+ "test": "node convert.js examples/oregon-spring-2026.yaml /tmp/tripkit-test.html && test -s /tmp/tripkit-test.html && node scripts/check-skill-coverage.js && echo 'OK'",
24
29
  "prepublishOnly": "npm test"
25
30
  },
26
31
  "keywords": [
@@ -18,6 +18,7 @@ html,body{height:100%;overflow:hidden}
18
18
  body{font-family:var(--font);background:var(--bg);color:var(--text)}
19
19
  .app{display:grid;grid-template-columns:460px 1fr;height:100vh}
20
20
  @media(max-width:960px){.app{grid-template-columns:1fr;grid-template-rows:55vh 45vh}}
21
+ .legend-toggle{display:none}
21
22
  .sidebar{display:flex;flex-direction:column;background:var(--card);border-right:1px solid var(--border);overflow:hidden}
22
23
  .sidebar-scroll{overflow-y:auto;flex:1;scroll-behavior:smooth}
23
24
  .sidebar-scroll::-webkit-scrollbar{width:5px}
@@ -93,6 +94,22 @@ body{font-family:var(--font);background:var(--bg);color:var(--text)}
93
94
  border:2px solid #1b5e3b;font-family:var(--font);cursor:pointer;transition:transform .12s;letter-spacing:-.02em}
94
95
  .hm:hover{transform:scale(1.15)}
95
96
  .pp-conf{font-size:11px;font-weight:600;color:var(--accent);background:var(--accent-soft);padding:4px 8px;border-radius:4px;display:inline-block;margin-top:6px;letter-spacing:.02em}
97
+ .endpin{display:flex;align-items:center;justify-content:center;width:28px;height:34px;font-size:11px;font-weight:700;color:#fff;font-family:var(--font);border-radius:14px 14px 14px 2px;transform:rotate(-45deg);box-shadow:0 2px 6px rgba(0,0,0,.35);border:2px solid #fff;cursor:pointer}
98
+ .endpin span{transform:rotate(45deg);letter-spacing:.02em}
99
+ .endpin.start{background:#1b5e3b}
100
+ .endpin.end{background:#7a1f1f}
101
+ @media(max-width:480px){
102
+ .hero{height:140px}.hero-text{padding:12px 14px}.hero-text h1{font-size:20px}
103
+ .day-nav{padding:8px 10px;gap:1px}.day-btn{padding:5px 8px;font-size:10px}
104
+ .day-panel{padding:12px 14px 32px}
105
+ .legend{bottom:auto;top:52px;left:8px;right:auto;padding:6px 10px;font-size:9px;line-height:1.6;max-width:60%}
106
+ .legend.collapsed .lr,.legend.collapsed > div:not(.legend-toggle){display:none}
107
+ .legend-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:10px;font-weight:600;user-select:none;color:var(--text)}
108
+ .legend-toggle::after{content:'▾';font-size:9px;transition:transform .15s;color:var(--muted)}
109
+ .legend.collapsed .legend-toggle::after{transform:rotate(-90deg)}
110
+ .map-ctrl{top:8px;right:8px}.mbtn{padding:5px 8px;font-size:9px}
111
+ .fullbtn{bottom:10px;right:8px;padding:5px 10px;font-size:10px}
112
+ }
96
113
  </style>
97
114
  </head>
98
115
  <body>
@@ -316,15 +333,25 @@ const TRIP_DATA = {};
316
333
  L.polyline(r.points, { color: r.color || '#666', weight: r.width || 4, opacity: 0.8 }).addTo(map);
317
334
  });
318
335
  } else {
319
- // Auto-generate routes from consecutive stops
320
- days.forEach(d => {
321
- if (d.stops && d.stops.length > 1) {
322
- const pts = d.stops.filter(s => s.lat && s.lng).map(s => [s.lat, s.lng]);
323
- if (pts.length > 1) {
324
- L.polyline(pts, { color: '#fff', weight: 7, opacity: 0.4 }).addTo(map);
325
- L.polyline(pts, { color: d.color || '#666', weight: 4, opacity: 0.8 }).addTo(map);
336
+ // Auto-generate routes. For each day, the polyline runs:
337
+ // prev day's lodging (if any) → today's stops → today's lodging (if any)
338
+ // This stitches consecutive days together so the map reads as one continuous trip.
339
+ const isAnchorLodging = (l) => l && l.lat && l.lng && l.name !== 'Home';
340
+ days.forEach((d, di) => {
341
+ const pts = [];
342
+ const prevLodging = di > 0 ? days[di - 1].lodging : null;
343
+ if (isAnchorLodging(prevLodging)) pts.push([prevLodging.lat, prevLodging.lng]);
344
+ (d.stops || []).forEach(s => { if (s.lat && s.lng) pts.push([s.lat, s.lng]); });
345
+ if (isAnchorLodging(d.lodging)) {
346
+ const last = pts[pts.length - 1];
347
+ if (!last || last[0] !== d.lodging.lat || last[1] !== d.lodging.lng) {
348
+ pts.push([d.lodging.lat, d.lodging.lng]);
326
349
  }
327
350
  }
351
+ if (pts.length > 1) {
352
+ L.polyline(pts, { color: '#fff', weight: 7, opacity: 0.4 }).addTo(map);
353
+ L.polyline(pts, { color: d.color || '#666', weight: 4, opacity: 0.8 }).addTo(map);
354
+ }
328
355
  });
329
356
  }
330
357
 
@@ -356,8 +383,34 @@ const TRIP_DATA = {};
356
383
  markers.push(dm);
357
384
  });
358
385
 
386
+ // --- Start / End pins (origin & destination) ---
387
+ function endpoint(lat, lng, kind, name) {
388
+ const labelTxt = kind === 'start' ? 'A' : 'B';
389
+ const titleTxt = kind === 'start' ? 'Start' : 'End';
390
+ const icon = L.divIcon({
391
+ className: '',
392
+ html: `<div class="endpin ${kind}"><span>${labelTxt}</span></div>`,
393
+ iconSize: [28, 34], iconAnchor: [4, 32]
394
+ });
395
+ const m = L.marker([lat, lng], { icon, zIndexOffset: 800 }).addTo(map);
396
+ m.bindPopup(`<div class="pp-body" style="padding:14px">
397
+ <div class="pp-day">${titleTxt}</div>
398
+ <div class="pp-name">${name || titleTxt}</div>
399
+ </div>`, { maxWidth: 240 });
400
+ }
401
+ if (Number.isFinite(trip.origin_lat) && Number.isFinite(trip.origin_lng)) {
402
+ endpoint(trip.origin_lat, trip.origin_lng, 'start', trip.origin);
403
+ const destLat = Number.isFinite(trip.destination_lat) ? trip.destination_lat : trip.origin_lat;
404
+ const destLng = Number.isFinite(trip.destination_lng) ? trip.destination_lng : trip.origin_lng;
405
+ // Suppress the "End" pin if it's at the same coords as the Start (round trip).
406
+ if (Math.abs(destLat - trip.origin_lat) > 0.001 || Math.abs(destLng - trip.origin_lng) > 0.001) {
407
+ endpoint(destLat, destLng, 'end', trip.destination || trip.origin);
408
+ }
409
+ }
410
+
359
411
  // --- Hotel markers ---
360
- const hotels = days
412
+ // Build per-night list, then dedupe by lat/lng so multi-night stays render as one marker.
413
+ const nights = days
361
414
  .filter(d => d.lodging && d.lodging.lat && d.lodging.lng && d.lodging.name !== 'Home')
362
415
  .map((d, i) => ({
363
416
  name: d.lodging.name,
@@ -371,11 +424,24 @@ const TRIP_DATA = {};
371
424
  navigate_url: d.lodging.navigate_url
372
425
  }));
373
426
 
374
- const hotelLabel = (hotels.length > 0 && data.agent_context?.preferences?.accommodation_chain)
375
- ? data.agent_context.preferences.accommodation_chain.split(' ')[0].substring(0, 3).toUpperCase()
376
- : '🏨';
377
-
378
- hotels.forEach(h => {
427
+ const hotelGroups = new Map();
428
+ nights.forEach(n => {
429
+ const key = n.lat.toFixed(4) + ',' + n.lng.toFixed(4);
430
+ if (!hotelGroups.has(key)) hotelGroups.set(key, []);
431
+ hotelGroups.get(key).push(n);
432
+ });
433
+ const hotels = Array.from(hotelGroups.values());
434
+
435
+ const hotelLabel = theme.hotel_label
436
+ || (data.agent_context?.preferences?.accommodation_chain
437
+ ? data.agent_context.preferences.accommodation_chain.split(' ')[0].substring(0, 3).toUpperCase()
438
+ : '🏨');
439
+
440
+ hotels.forEach(group => {
441
+ const h = group[0];
442
+ const ns = group.map(g => g.night);
443
+ const nightLabel = ns.length === 1 ? `Night ${ns[0]}` : `Nights ${ns[0]}–${ns[ns.length - 1]}`;
444
+ const dateLabel = group.length === 1 ? h.day : `${h.day} – ${group[group.length - 1].day}`;
379
445
  const icon = L.divIcon({
380
446
  className: '',
381
447
  html: `<div class="hm">${hotelLabel}</div>`,
@@ -383,7 +449,7 @@ const TRIP_DATA = {};
383
449
  });
384
450
  const m = L.marker([h.lat, h.lng], { icon, zIndexOffset: 1000 }).addTo(map);
385
451
  let popupHTML = `<div class="pp-body" style="padding:16px">
386
- <div class="pp-day">Night ${h.night} · ${h.day}</div>
452
+ <div class="pp-day">${nightLabel} · ${dateLabel}</div>
387
453
  <div class="pp-name">${h.name}</div>
388
454
  <div style="font-size:12px;color:var(--muted);margin-bottom:6px">${h.loc}</div>`;
389
455
  if (h.booked && h.conf) popupHTML += `<div class="pp-conf">Conf# ${h.conf}</div>`;
@@ -394,7 +460,7 @@ const TRIP_DATA = {};
394
460
 
395
461
  // --- Legend ---
396
462
  const legendEl = document.getElementById('legend');
397
- let legendHTML = '<b>Route segments</b>';
463
+ let legendHTML = '<div class="legend-toggle"><span>Route segments</span></div>';
398
464
  days.forEach(d => {
399
465
  legendHTML += `<div class="lr"><i style="background:${d.color || '#666'}"></i>Day ${d.number} · ${d.title.split('→')[0].replace('✅ ','').trim()}</div>`;
400
466
  });
@@ -404,9 +470,17 @@ const TRIP_DATA = {};
404
470
  legendHTML += `Booked hotels (${hotels.length})</div></div>`;
405
471
  }
406
472
  legendEl.innerHTML = legendHTML;
473
+ if (window.matchMedia('(max-width: 480px)').matches) legendEl.classList.add('collapsed');
474
+ legendEl.querySelector('.legend-toggle')?.addEventListener('click', () => legendEl.classList.toggle('collapsed'));
407
475
 
408
476
  // --- Fit bounds ---
409
- const allPts = days.flatMap(d => (d.stops || []).filter(s => s.lat && s.lng).map(s => [s.lat, s.lng]));
477
+ // Include stops, hotels, and origin/destination pins so the initial fit covers everything.
478
+ const allPts = [
479
+ ...days.flatMap(d => (d.stops || []).filter(s => s.lat && s.lng).map(s => [s.lat, s.lng])),
480
+ ...days.flatMap(d => (d.lodging && d.lodging.lat && d.lodging.lng && d.lodging.name !== 'Home') ? [[d.lodging.lat, d.lodging.lng]] : []),
481
+ ];
482
+ if (Number.isFinite(trip.origin_lat) && Number.isFinite(trip.origin_lng)) allPts.push([trip.origin_lat, trip.origin_lng]);
483
+ if (Number.isFinite(trip.destination_lat) && Number.isFinite(trip.destination_lng)) allPts.push([trip.destination_lat, trip.destination_lng]);
410
484
  const fullBounds = L.latLngBounds(allPts).pad(0.06);
411
485
  map.fitBounds(fullBounds);
412
486
 
@@ -18,6 +18,10 @@ trip:
18
18
  children: integer
19
19
  ages: string # e.g. "Kids: 14, 17"
20
20
  origin: string # e.g. "Folsom, CA"
21
+ origin_lat: number # Optional — if set with origin_lng, renders a "Start" pin on the map
22
+ origin_lng: number
23
+ destination_lat: number # Optional — if absent, defaults to origin coords (round trip)
24
+ destination_lng: number # Set explicitly only if the trip ends somewhere different from where it started
21
25
  vehicle: string # e.g. "SUV", "Sedan", "RV"
22
26
 
23
27
  # ============================================================
@@ -102,6 +106,9 @@ theme:
102
106
  map_style: string # "terrain" | "satellite" | "topo" | "street"
103
107
  dark_mode: boolean # Default: false
104
108
  hero_style: string # "photo" | "gradient" | "minimal"
109
+ hotel_label: string # Optional 1–4 char label for hotel markers (e.g. "MAR", "BW", "INN").
110
+ # Falls back to first 3 chars of agent_context.preferences.accommodation_chain,
111
+ # then 🏨 emoji.
105
112
 
106
113
  # ============================================================
107
114
  # AGENT CONTEXT — metadata for the planning agent