tripkit 1.0.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,446 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TripKit — Trip Visualizer</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='13' r='6' fill='%231b5e3b'/%3E%3Ccircle cx='16' cy='13' r='2.2' fill='%23fff'/%3E%3Cpath d='M16 19 L11 28 L21 28 Z' fill='%231b5e3b'/%3E%3C/svg%3E"/>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css"/>
9
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300;1,9..40,400&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
10
+ <style>
11
+ *{margin:0;padding:0;box-sizing:border-box}
12
+ :root{--bg:#f4f1eb;--card:#fff;--text:#1a1a18;--muted:#7a7768;--subtle:#b5b1a4;
13
+ --accent:#1b5e3b;--accent-soft:#dceee4;--warn:#c44b25;--warn-soft:#fdeee8;
14
+ --blue:#2563a8;--blue-soft:#e8f0fa;--purple:#5b44b0;--purple-soft:#eeeafb;
15
+ --amber:#a16207;--amber-soft:#fdf4e3;--coral:#c44b25;
16
+ --border:rgba(0,0,0,.08);--font:'DM Sans',system-ui,sans-serif;--serif:'Instrument Serif',Georgia,serif;--r:10px}
17
+ html,body{height:100%;overflow:hidden}
18
+ body{font-family:var(--font);background:var(--bg);color:var(--text)}
19
+ .app{display:grid;grid-template-columns:460px 1fr;height:100vh}
20
+ @media(max-width:960px){.app{grid-template-columns:1fr;grid-template-rows:55vh 45vh}}
21
+ .sidebar{display:flex;flex-direction:column;background:var(--card);border-right:1px solid var(--border);overflow:hidden}
22
+ .sidebar-scroll{overflow-y:auto;flex:1;scroll-behavior:smooth}
23
+ .sidebar-scroll::-webkit-scrollbar{width:5px}
24
+ .sidebar-scroll::-webkit-scrollbar-thumb{background:var(--subtle);border-radius:3px}
25
+ .hero{position:relative;height:165px;overflow:hidden;flex-shrink:0}
26
+ .hero img{width:100%;height:100%;object-fit:cover;transition:opacity .4s}
27
+ .hero::after{content:'';position:absolute;inset:0;background:linear-gradient(transparent 15%,rgba(0,0,0,.7))}
28
+ .hero-text{position:absolute;bottom:0;left:0;right:0;padding:16px 22px;z-index:2;color:#fff}
29
+ .hero-text h1{font-family:var(--serif);font-size:23px;font-weight:400;line-height:1.2}
30
+ .hero-text p{font-size:11px;opacity:.8;margin-top:3px}
31
+ .pills{display:flex;gap:5px;margin-top:7px;flex-wrap:wrap}
32
+ .pill{background:rgba(255,255,255,.16);backdrop-filter:blur(6px);padding:3px 9px;border-radius:16px;font-size:10px;font-weight:500;color:#fff;border:1px solid rgba(255,255,255,.2)}
33
+ .day-nav{display:flex;gap:2px;padding:10px 16px;border-bottom:1px solid var(--border);overflow-x:auto;flex-shrink:0;background:var(--card)}
34
+ .day-nav::-webkit-scrollbar{height:0}
35
+ .day-btn{border:none;background:transparent;font-family:var(--font);font-size:11px;font-weight:500;color:var(--muted);padding:6px 10px;cursor:pointer;white-space:nowrap;transition:all .15s;border-radius:6px}
36
+ .day-btn:hover{background:var(--bg);color:var(--text)}
37
+ .day-btn.active{background:var(--accent);color:#fff}
38
+ .day-btn.has-alert{box-shadow:inset 0 -2px 0 var(--warn)}
39
+ .day-panel{display:none;padding:16px 22px 40px}
40
+ .day-panel.active{display:block;animation:fadeIn .25s ease}
41
+ @keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
42
+ .day-head{display:flex;gap:12px;align-items:flex-start;margin-bottom:10px}
43
+ .day-num{font-family:var(--serif);font-size:44px;font-weight:400;line-height:1;color:var(--accent);opacity:.6}
44
+ .day-info h2{font-size:15px;font-weight:600;line-height:1.3}
45
+ .day-info .date{font-size:12px;color:var(--muted)}
46
+ .chips{display:flex;gap:5px;flex-wrap:wrap;margin:8px 0 12px}
47
+ .chip{display:inline-flex;align-items:center;gap:3px;font-size:10px;color:var(--muted);background:var(--bg);padding:4px 9px;border-radius:5px}
48
+ .chip b{color:var(--text);font-weight:500}
49
+ .callout{border-radius:8px;padding:10px 12px;margin-bottom:10px;font-size:12px;line-height:1.5}
50
+ .callout.warn{background:var(--warn-soft);border-left:3px solid var(--warn)}
51
+ .callout.tip{background:var(--accent-soft);border-left:3px solid var(--accent)}
52
+ .callout.weather{background:var(--blue-soft);border-left:3px solid var(--blue)}
53
+ .callout.eat{background:var(--amber-soft);border-left:3px solid var(--amber)}
54
+ .callout.sleep{background:var(--purple-soft);border-left:3px solid var(--purple)}
55
+ .cl{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px}
56
+ .callout.warn .cl{color:var(--warn)}.callout.tip .cl{color:var(--accent)}.callout.weather .cl{color:var(--blue)}.callout.eat .cl{color:var(--amber)}.callout.sleep .cl{color:var(--purple)}
57
+ .stop{display:flex;gap:10px;padding:10px 0;border-top:1px solid var(--border);cursor:pointer;transition:background .12s}
58
+ .stop:hover{background:var(--bg);margin:0 -10px;padding:10px;border-radius:8px}
59
+ .stop:first-of-type{border-top:none}
60
+ .stop-img{width:72px;height:54px;border-radius:7px;object-fit:cover;flex-shrink:0;background:linear-gradient(135deg,var(--accent-soft),var(--blue-soft))}
61
+ .stop-body{flex:1;min-width:0}
62
+ .stop-name{font-weight:500;font-size:13px;display:flex;align-items:center;gap:6px}
63
+ .badge{font-size:8px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;padding:2px 6px;border-radius:3px;flex-shrink:0}
64
+ .b-hike{background:#dceee4;color:#1b5e3b}.b-scenic{background:#e8f0fa;color:#2563a8}.b-food{background:#fdeee8;color:#c44b25}
65
+ .b-city{background:#eeeafb;color:#5b44b0}.b-activity{background:#fdf4e3;color:#a16207}.b-beach{background:#e0f7fa;color:#00838f}
66
+ .b-museum{background:#fce4ec;color:#c62828}.b-shopping{background:#f3e5f5;color:#7b1fa2}
67
+ .stop-desc{font-size:11px;color:var(--muted);line-height:1.45;margin-top:2px}
68
+ .stop-meta{display:flex;gap:8px;margin-top:3px;font-size:10px;color:var(--subtle)}
69
+ .sec-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--subtle);margin:14px 0 6px;padding-top:6px;border-top:1px solid var(--border)}
70
+ .map-wrap{position:relative;height:100%}
71
+ #map{height:100%;z-index:1}
72
+ .map-ctrl{position:absolute;top:12px;right:12px;z-index:1000;display:flex;gap:0}
73
+ .mbtn{border:none;background:var(--card);font-family:var(--font);font-size:10px;font-weight:500;padding:6px 10px;cursor:pointer;color:var(--muted);border:1px solid var(--border);transition:all .12s}
74
+ .mbtn:first-child{border-radius:5px 0 0 5px}.mbtn:last-child{border-radius:0 5px 5px 0}.mbtn:not(:first-child){border-left:none}
75
+ .mbtn.active{background:var(--text);color:#fff;border-color:var(--text)}
76
+ .legend{position:absolute;bottom:16px;left:12px;background:var(--card);border-radius:8px;padding:10px 14px;box-shadow:0 2px 10px rgba(0,0,0,.1);border:1px solid var(--border);z-index:1000;font-size:10px;line-height:1.8}
77
+ .legend b{display:block;font-size:11px;margin-bottom:2px}
78
+ .lr{display:flex;align-items:center;gap:6px}.lr i{width:16px;height:3px;border-radius:2px;display:inline-block}
79
+ .fullbtn{position:absolute;bottom:16px;right:12px;z-index:1000;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:6px 12px;font-family:var(--font);font-size:11px;font-weight:500;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.1)}
80
+ .fullbtn:hover{background:var(--text);color:#fff}
81
+ .leaflet-popup-content-wrapper{border-radius:10px!important;box-shadow:0 4px 16px rgba(0,0,0,.12)!important;border:1px solid var(--border)!important;padding:0!important;overflow:hidden}
82
+ .leaflet-popup-content{margin:0!important;width:280px!important;font-family:var(--font)!important}
83
+ .leaflet-popup-tip{border-top-color:var(--card)!important}
84
+ .pp-img{width:100%;height:140px;object-fit:cover;display:block;background:linear-gradient(135deg,var(--accent-soft),var(--blue-soft))}
85
+ .pp-body{padding:12px 14px}
86
+ .pp-day{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:3px}
87
+ .pp-name{font-family:var(--serif);font-size:16px;line-height:1.25;margin-bottom:5px}
88
+ .pp-desc{font-size:11px;color:var(--muted);line-height:1.5}
89
+ .cm{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;font-weight:600;font-size:10px;color:#fff;box-shadow:0 2px 5px rgba(0,0,0,.3);border:2px solid #fff;font-family:var(--font);cursor:pointer;transition:transform .12s}
90
+ .cm:hover{transform:scale(1.2)}
91
+ .hm{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;
92
+ font-weight:700;font-size:9px;color:#1b5e3b;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.35);
93
+ border:2px solid #1b5e3b;font-family:var(--font);cursor:pointer;transition:transform .12s;letter-spacing:-.02em}
94
+ .hm:hover{transform:scale(1.15)}
95
+ .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}
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <div class="app">
100
+ <div class="sidebar">
101
+ <div class="hero"><img id="heroImg" alt="">
102
+ <div class="hero-text"><h1 id="tripTitle"></h1><p id="tripSubtitle"></p>
103
+ <div class="pills" id="tripPills"></div></div></div>
104
+ <div class="day-nav" id="nav"></div>
105
+ <div class="sidebar-scroll" id="scroll"></div>
106
+ </div>
107
+ <div class="map-wrap">
108
+ <div id="map"></div>
109
+ <div class="map-ctrl"><button class="mbtn active" data-s="topo">Terrain</button><button class="mbtn" data-s="sat">Satellite</button><button class="mbtn" data-s="natgeo">Nat Geo</button></div>
110
+ <div class="legend" id="legend"></div>
111
+ <button class="fullbtn" onclick="showAll()">Full route</button>
112
+ </div></div>
113
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
114
+ <script>
115
+ // ============================================================
116
+ // TripKit Renderer v1.0
117
+ // Reads TRIP_DATA (JSON) and renders interactive trip visualizer
118
+ // ============================================================
119
+
120
+ // ▼▼▼ PASTE YOUR TRIP DATA HERE (JSON format) ▼▼▼
121
+ // The agent generates this from the YAML schema.
122
+ // Replace the empty object below with your trip JSON.
123
+ const TRIP_DATA = {};
124
+ // ▲▲▲ END TRIP DATA ▲▲▲
125
+
126
+ // ============================================================
127
+ // RENDERING ENGINE — do not modify below this line
128
+ // ============================================================
129
+
130
+ (function TripKitRenderer(data) {
131
+ if (!data.trip || !data.days) {
132
+ document.body.innerHTML = '<div style="padding:40px;font-family:system-ui;text-align:center"><h2>No trip data loaded</h2><p>Paste your trip JSON into the TRIP_DATA variable, or load a YAML file.</p></div>';
133
+ return;
134
+ }
135
+
136
+ const trip = data.trip;
137
+ const days = data.days;
138
+ const routes = data.routes || [];
139
+ const theme = data.theme || {};
140
+
141
+ // --- Apply theme ---
142
+ if (theme.accent_color) document.documentElement.style.setProperty('--accent', theme.accent_color);
143
+ if (theme.font_family) document.documentElement.style.setProperty('--font', theme.font_family);
144
+
145
+ // --- Default hero images by stop type ---
146
+ const defaultImages = {
147
+ hike: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=640&h=320&fit=crop',
148
+ scenic: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=640&h=320&fit=crop',
149
+ food: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=640&h=320&fit=crop',
150
+ city: 'https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=640&h=320&fit=crop',
151
+ activity: 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=640&h=320&fit=crop',
152
+ beach: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=640&h=320&fit=crop',
153
+ museum: 'https://images.unsplash.com/photo-1554907984-15263bfd63bd?w=640&h=320&fit=crop',
154
+ shopping: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=640&h=320&fit=crop',
155
+ default: 'https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=640&h=320&fit=crop'
156
+ };
157
+
158
+ function getImage(stop) {
159
+ return stop.image || defaultImages[stop.type] || defaultImages.default;
160
+ }
161
+
162
+ function getDayHero(day) {
163
+ // Use first stop's image as day hero
164
+ if (day.stops && day.stops.length > 0) return getImage(day.stops[0]);
165
+ return defaultImages.default;
166
+ }
167
+
168
+ // --- Populate header ---
169
+ const hImg = document.getElementById('heroImg');
170
+ document.getElementById('tripTitle').textContent = trip.title;
171
+ document.getElementById('tripSubtitle').textContent = trip.subtitle || '';
172
+ hImg.src = getDayHero(days[0]);
173
+
174
+ const pillsEl = document.getElementById('tripPills');
175
+ pillsEl.innerHTML = [
176
+ `${trip.total_days} days · ${trip.dates}`,
177
+ trip.total_miles,
178
+ `${trip.total_stops} stops`
179
+ ].filter(Boolean).map(t => `<span class="pill">${t}</span>`).join('');
180
+
181
+ // --- Build sidebar ---
182
+ const nav = document.getElementById('nav');
183
+ const scr = document.getElementById('scroll');
184
+
185
+ days.forEach((d, i) => {
186
+ const hasAlerts = d.alerts && d.alerts.length > 0;
187
+ const statusIcon = d.status === 'completed' ? '✅ ' : d.status === 'active' ? '🔵 ' : '';
188
+
189
+ // Nav button
190
+ const b = document.createElement('button');
191
+ b.className = 'day-btn' + (i === 0 ? ' active' : '') + (hasAlerts ? ' has-alert' : '');
192
+ b.textContent = `Day ${d.number}`;
193
+ b.onclick = () => sel(i);
194
+ nav.appendChild(b);
195
+
196
+ // Day panel
197
+ const pan = document.createElement('div');
198
+ pan.className = 'day-panel' + (i === 0 ? ' active' : '');
199
+ pan.id = 'p' + i;
200
+
201
+ let h = '';
202
+
203
+ // Header
204
+ h += `<div class="day-head"><div class="day-num">${d.number}</div>`;
205
+ h += `<div class="day-info"><h2>${statusIcon}${d.title}</h2>`;
206
+ h += `<div class="date">${d.date}</div></div></div>`;
207
+
208
+ // Summary chips
209
+ if (d.summary) {
210
+ h += `<div class="chips">`;
211
+ if (d.summary.drive) h += `<span class="chip">🚗 <b>${d.summary.drive}</b></span>`;
212
+ if (d.summary.hike) h += `<span class="chip">🥾 <b>${d.summary.hike}</b></span>`;
213
+ if (d.summary.miles) h += `<span class="chip">📏 <b>${d.summary.miles}</b></span>`;
214
+ h += `</div>`;
215
+ }
216
+
217
+ // Weather
218
+ if (d.weather) {
219
+ h += `<div class="callout weather"><div class="cl">🌤 Weather</div>`;
220
+ h += `<b>${d.weather.high || ''}`;
221
+ if (d.weather.low) h += ` / ${d.weather.low}`;
222
+ h += `</b>`;
223
+ if (d.weather.sky) h += ` · ${d.weather.sky}`;
224
+ if (d.weather.rain_chance) h += ` · ${d.weather.rain_chance} rain`;
225
+ if (d.weather.note) h += `<br><span style="color:var(--muted)">${d.weather.note}</span>`;
226
+ h += `</div>`;
227
+ }
228
+
229
+ // Alerts
230
+ if (hasAlerts) {
231
+ d.alerts.forEach(a => {
232
+ h += `<div class="callout warn"><div class="cl">⚠ Heads up</div>${a}</div>`;
233
+ });
234
+ }
235
+
236
+ // Tips
237
+ if (d.tips && d.tips.length > 0) {
238
+ d.tips.forEach(t => {
239
+ h += `<div class="callout tip"><div class="cl">💡 Pro tip</div>${t}</div>`;
240
+ });
241
+ }
242
+
243
+ // Stops
244
+ if (d.stops && d.stops.length > 0) {
245
+ h += `<div class="sec-label">Stops</div>`;
246
+ d.stops.forEach((s, si) => {
247
+ const bc = 'b-' + (s.type || 'scenic');
248
+ const img = getImage(s);
249
+ h += `<div class="stop" onclick="tripFocus(${i},${si})">`;
250
+ h += `<img class="stop-img" src="${img}" alt="${s.name}" loading="lazy" onerror="this.style.opacity=0">`;
251
+ h += `<div class="stop-body">`;
252
+ h += `<div class="stop-name"><span class="badge ${bc}">${s.label || s.type}</span>${s.name}</div>`;
253
+ h += `<div class="stop-desc">${s.description || ''}</div>`;
254
+ const meta = [];
255
+ if (s.duration) meta.push(`⏱ ${s.duration}`);
256
+ if (s.parking_fee) meta.push(`🅿 ${s.parking_fee}`);
257
+ if (s.kid_friendly === false) meta.push(`⚠ Not kid-friendly`);
258
+ if (meta.length) h += `<div class="stop-meta">${meta.map(m => `<span>${m}</span>`).join('')}</div>`;
259
+ h += `</div></div>`;
260
+ });
261
+ }
262
+
263
+ // Meals
264
+ h += `<div class="sec-label">Eat & sleep</div>`;
265
+ if (d.meals) {
266
+ h += `<div class="callout eat"><div class="cl">🍽 Where to eat</div>`;
267
+ for (const [k, v] of Object.entries(d.meals)) {
268
+ if (v) h += `<b style="text-transform:capitalize">${k}:</b> ${v}<br>`;
269
+ }
270
+ h += `</div>`;
271
+ }
272
+
273
+ // Lodging
274
+ if (d.lodging && d.lodging.name) {
275
+ h += `<div class="callout sleep"><div class="cl">🛏 Where to stay</div>`;
276
+ h += `<b>${d.lodging.name}</b>`;
277
+ if (d.lodging.location) h += `, ${d.lodging.location}`;
278
+ if (d.lodging.price_estimate) h += ` — ${d.lodging.price_estimate}`;
279
+ h += `<br>`;
280
+ if (d.lodging.booked && d.lodging.confirmation) {
281
+ h += `<span class="pp-conf" style="margin-top:4px;margin-bottom:4px">✅ BOOKED — Conf# ${d.lodging.confirmation}</span><br>`;
282
+ }
283
+ if (d.lodging.notes) h += `<span style="color:var(--muted)">${d.lodging.notes}</span>`;
284
+ if (d.lodging.navigate_url) h += `<br><a href="${d.lodging.navigate_url}" target="_blank" style="font-size:11px;color:var(--accent)">📍 Navigate</a>`;
285
+ h += `</div>`;
286
+ }
287
+
288
+ pan.innerHTML = h;
289
+ scr.appendChild(pan);
290
+ });
291
+
292
+ // === MAP ===
293
+ const tiles = {
294
+ topo: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', { attribution: 'Esri, USGS', maxZoom: 18 }),
295
+ sat: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Esri, Maxar', maxZoom: 18 }),
296
+ natgeo: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}', { attribution: 'Esri, NatGeo', maxZoom: 16 })
297
+ };
298
+ const defaultTile = (theme.map_style === 'satellite') ? 'sat' : (theme.map_style === 'topo') ? 'natgeo' : 'topo';
299
+ const map = L.map('map', { zoomControl: true }).setView([43, -122.5], 6);
300
+ let curTile = tiles[defaultTile];
301
+ curTile.addTo(map);
302
+
303
+ document.querySelectorAll('.mbtn').forEach(b => b.addEventListener('click', function () {
304
+ document.querySelectorAll('.mbtn').forEach(x => x.classList.remove('active'));
305
+ this.classList.add('active');
306
+ map.removeLayer(curTile);
307
+ curTile = tiles[this.dataset.s];
308
+ curTile.addTo(map);
309
+ }));
310
+
311
+ // --- Routes ---
312
+ if (routes.length > 0) {
313
+ // Use explicit routes from data
314
+ routes.forEach(r => {
315
+ L.polyline(r.points, { color: '#fff', weight: (r.width || 4) + 3, opacity: 0.4 }).addTo(map);
316
+ L.polyline(r.points, { color: r.color || '#666', weight: r.width || 4, opacity: 0.8 }).addTo(map);
317
+ });
318
+ } 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);
326
+ }
327
+ }
328
+ });
329
+ }
330
+
331
+ // --- Stop markers ---
332
+ const markers = [];
333
+ days.forEach((d, di) => {
334
+ const dm = [];
335
+ d.stops.forEach((s, si) => {
336
+ if (!s.lat || !s.lng) return;
337
+ const icon = L.divIcon({
338
+ className: '',
339
+ html: `<div class="cm" style="background:${d.color || '#666'}">${d.number}</div>`,
340
+ iconSize: [26, 26], iconAnchor: [13, 13]
341
+ });
342
+ const m = L.marker([s.lat, s.lng], { icon }).addTo(map);
343
+ const img = getImage(s);
344
+ m.bindPopup(`<div>
345
+ <img class="pp-img" src="${img}" alt="${s.name}" onerror="this.style.opacity=0">
346
+ <div class="pp-body">
347
+ <div class="pp-day">Day ${d.number} · ${d.date}</div>
348
+ <div class="pp-name">${s.name}</div>
349
+ <div class="pp-desc">${s.description || ''}</div>
350
+ ${s.navigate_url ? `<a href="${s.navigate_url}" target="_blank" style="display:inline-block;margin-top:6px;font-size:11px;color:var(--accent);text-decoration:none">📍 Navigate →</a>` : ''}
351
+ </div>
352
+ </div>`, { maxWidth: 280 });
353
+ m.on('click', () => sel(di));
354
+ dm.push(m);
355
+ });
356
+ markers.push(dm);
357
+ });
358
+
359
+ // --- Hotel markers ---
360
+ const hotels = days
361
+ .filter(d => d.lodging && d.lodging.lat && d.lodging.lng && d.lodging.name !== 'Home')
362
+ .map((d, i) => ({
363
+ name: d.lodging.name,
364
+ loc: d.lodging.location || '',
365
+ lat: d.lodging.lat,
366
+ lng: d.lodging.lng,
367
+ conf: d.lodging.confirmation || '',
368
+ day: d.date,
369
+ night: i + 1,
370
+ booked: d.lodging.booked,
371
+ navigate_url: d.lodging.navigate_url
372
+ }));
373
+
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 => {
379
+ const icon = L.divIcon({
380
+ className: '',
381
+ html: `<div class="hm">${hotelLabel}</div>`,
382
+ iconSize: [32, 32], iconAnchor: [16, 16]
383
+ });
384
+ const m = L.marker([h.lat, h.lng], { icon, zIndexOffset: 1000 }).addTo(map);
385
+ let popupHTML = `<div class="pp-body" style="padding:16px">
386
+ <div class="pp-day">Night ${h.night} · ${h.day}</div>
387
+ <div class="pp-name">${h.name}</div>
388
+ <div style="font-size:12px;color:var(--muted);margin-bottom:6px">${h.loc}</div>`;
389
+ if (h.booked && h.conf) popupHTML += `<div class="pp-conf">Conf# ${h.conf}</div>`;
390
+ if (h.navigate_url) popupHTML += `<br><a href="${h.navigate_url}" target="_blank" style="font-size:11px;color:var(--accent);text-decoration:none">📍 Navigate →</a>`;
391
+ popupHTML += `</div>`;
392
+ m.bindPopup(popupHTML, { maxWidth: 280 });
393
+ });
394
+
395
+ // --- Legend ---
396
+ const legendEl = document.getElementById('legend');
397
+ let legendHTML = '<b>Route segments</b>';
398
+ days.forEach(d => {
399
+ legendHTML += `<div class="lr"><i style="background:${d.color || '#666'}"></i>Day ${d.number} · ${d.title.split('→')[0].replace('✅ ','').trim()}</div>`;
400
+ });
401
+ if (hotels.length > 0) {
402
+ legendHTML += `<div style="border-top:1px solid var(--border);margin:5px 0 3px;padding-top:5px">`;
403
+ legendHTML += `<div class="lr"><div style="width:18px;height:16px;border-radius:3px;border:1.5px solid #1b5e3b;background:#fff;font-size:7px;font-weight:700;color:#1b5e3b;display:flex;align-items:center;justify-content:center;flex-shrink:0">${hotelLabel}</div>`;
404
+ legendHTML += `Booked hotels (${hotels.length})</div></div>`;
405
+ }
406
+ legendEl.innerHTML = legendHTML;
407
+
408
+ // --- Fit bounds ---
409
+ const allPts = days.flatMap(d => (d.stops || []).filter(s => s.lat && s.lng).map(s => [s.lat, s.lng]));
410
+ const fullBounds = L.latLngBounds(allPts).pad(0.06);
411
+ map.fitBounds(fullBounds);
412
+
413
+ // --- Navigation ---
414
+ function sel(i) {
415
+ document.querySelectorAll('.day-btn').forEach((b, j) => b.classList.toggle('active', j === i));
416
+ document.querySelectorAll('.day-panel').forEach((p, j) => p.classList.toggle('active', j === i));
417
+ hImg.src = getDayHero(days[i]);
418
+ const d = days[i];
419
+ const pts = (d.stops || []).filter(s => s.lat && s.lng).map(s => [s.lat, s.lng]);
420
+ if (pts.length > 0) {
421
+ const bounds = L.latLngBounds(pts);
422
+ map.fitBounds(bounds.pad(0.3), { animate: true, maxZoom: 11 });
423
+ }
424
+ nav.querySelector('.day-btn.active')?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
425
+ scr.scrollTo({ top: 0, behavior: 'smooth' });
426
+ }
427
+
428
+ window.tripFocus = function (di, si) {
429
+ const s = days[di].stops[si];
430
+ if (s.lat && s.lng) {
431
+ map.setView([s.lat, s.lng], 12, { animate: true });
432
+ setTimeout(() => markers[di][si]?.openPopup(), 300);
433
+ }
434
+ };
435
+
436
+ window.showAll = function () {
437
+ map.fitBounds(fullBounds, { animate: true });
438
+ };
439
+
440
+ // Set page title
441
+ document.title = trip.title + ' — TripKit';
442
+
443
+ })(TRIP_DATA);
444
+ </script>
445
+ </body>
446
+ </html>
@@ -0,0 +1,132 @@
1
+ # TripKit Schema Specification v1.0
2
+ # This file defines the data contract for trip plans.
3
+ # Any renderer (HTML, React, mobile) can consume this format.
4
+ # Any agent (Claude, GPT, Gemini, human) can produce it.
5
+
6
+ # ============================================================
7
+ # TRIP METADATA
8
+ # ============================================================
9
+ trip:
10
+ title: string # e.g. "Oregon Spring Break 2026"
11
+ subtitle: string # e.g. "Redwoods · Coast · Waterfalls"
12
+ dates: string # e.g. "April 4–9, 2026"
13
+ total_days: integer
14
+ total_miles: string # e.g. "~1,600 mi"
15
+ total_stops: integer
16
+ travelers: # Optional — for agent context
17
+ adults: integer
18
+ children: integer
19
+ ages: string # e.g. "Kids: 14, 17"
20
+ origin: string # e.g. "Folsom, CA"
21
+ vehicle: string # e.g. "SUV", "Sedan", "RV"
22
+
23
+ # ============================================================
24
+ # DAYS — the core data structure
25
+ # ============================================================
26
+ days:
27
+ - number: integer # 1, 2, 3...
28
+ title: string # e.g. "Stout Grove → Boardman → Florence"
29
+ date: string # e.g. "Sunday, April 5"
30
+ status: string # "completed" | "active" | "upcoming"
31
+ color: string # Hex color for map route, e.g. "#2e7db5"
32
+
33
+ summary:
34
+ drive: string # e.g. "5–5.5 hrs"
35
+ hike: string # e.g. "1–2 hrs" or "—"
36
+ miles: string # e.g. "~230 mi"
37
+
38
+ weather: # Optional — filled by agent closer to trip
39
+ high: string # e.g. "59°F"
40
+ low: string # e.g. "37°F"
41
+ sky: string # e.g. "Patchy fog AM, then sunny"
42
+ rain_chance: string # e.g. "10%"
43
+ note: string # Human-readable context
44
+
45
+ meals:
46
+ breakfast: string # Restaurant name + note
47
+ lunch: string
48
+ dinner: string
49
+ snack: string # Optional
50
+
51
+ lodging:
52
+ name: string # e.g. "Best Western Plus Cascade Inn & Suites"
53
+ location: string # e.g. "Troutdale, Oregon"
54
+ price_estimate: string # e.g. "~$150/night"
55
+ confirmation: string # e.g. "ABC123456" — filled after booking
56
+ booked: boolean # true once reserved
57
+ lat: number
58
+ lng: number
59
+ notes: string # Free-form: pool, breakfast, special features
60
+ navigate_url: string # Google Maps nav link
61
+
62
+ alerts: # Optional array of important warnings
63
+ - string # e.g. "⏰ Dad has a 7:30 PM meeting..."
64
+
65
+ tips: # Optional array of pro tips
66
+ - string # e.g. "Stout Grove is 28 min via Howland Hill Road..."
67
+
68
+ stops:
69
+ - name: string # e.g. "Multnomah Falls"
70
+ lat: number # 45.576
71
+ lng: number # -122.116
72
+ type: string # "hike" | "scenic" | "food" | "city" | "activity" | "beach" | "museum" | "shopping"
73
+ label: string # Short display label, e.g. "Hike", "Sunset", "Lunch"
74
+ description: string # 2–3 sentence description with insider tips
75
+ duration: string # e.g. "1–1.5 hrs", "30–45 min"
76
+ image: string # URL or local path (optional)
77
+ parking_fee: string # e.g. "$12" (optional)
78
+ reservation_required: boolean # Optional
79
+ reservation_url: string # Optional
80
+ navigate_url: string # Google Maps nav link
81
+ hours: string # e.g. "Dawn to dusk" (optional)
82
+ kid_friendly: boolean # Optional
83
+ accessibility: string # Optional — wheelchair, stroller notes
84
+
85
+ # ============================================================
86
+ # ROUTES — map polylines connecting stops
87
+ # Optional: if omitted, renderer draws straight lines between stops
88
+ # ============================================================
89
+ routes:
90
+ - day: integer # Which day this route belongs to
91
+ color: string # Hex color (usually matches day color)
92
+ width: number # Line width in pixels
93
+ points: # Array of [lat, lng] waypoints
94
+ - [number, number]
95
+
96
+ # ============================================================
97
+ # THEME — optional visual customization
98
+ # ============================================================
99
+ theme:
100
+ font_family: string # e.g. "DM Sans, sans-serif"
101
+ accent_color: string # Primary accent, e.g. "#2d7a50"
102
+ map_style: string # "terrain" | "satellite" | "topo" | "street"
103
+ dark_mode: boolean # Default: false
104
+ hero_style: string # "photo" | "gradient" | "minimal"
105
+
106
+ # ============================================================
107
+ # AGENT CONTEXT — metadata for the planning agent
108
+ # Not rendered, but preserved for refinement iterations
109
+ # ============================================================
110
+ agent_context:
111
+ preferences:
112
+ pace: string # "relaxed" | "moderate" | "packed"
113
+ budget: string # "budget" | "mid-range" | "luxury"
114
+ accommodation_chain: string # e.g. "Best Western" (loyalty program)
115
+ interests: # Ranked list
116
+ - string # e.g. "waterfalls", "beaches", "food", "hiking"
117
+ dietary: string # Optional
118
+ mobility: string # Optional constraints
119
+ constraints:
120
+ max_drive_per_day: string # e.g. "6 hours"
121
+ must_see: # Non-negotiable stops
122
+ - string
123
+ avoid: # Things to skip
124
+ - string
125
+ schedule_blocks: # Work meetings, commitments
126
+ - day: integer
127
+ time: string
128
+ duration: string
129
+ note: string
130
+ iteration_log: # Track changes for context
131
+ - date: string
132
+ change: string # e.g. "Swapped Seaside for Astoria — Goonies house"