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.
- package/LICENSE +21 -0
- package/README.md +213 -0
- package/agent/AGENT-SKILL.md +105 -0
- package/agent/questionnaire.yaml +182 -0
- package/convert.js +65 -0
- package/docs/README.md +1 -0
- package/docs/screenshot.png +0 -0
- package/examples/oregon-spring-2026.yaml +588 -0
- package/package.json +52 -0
- package/renderers/html/tripkit-renderer.html +446 -0
- package/schema/tripkit.schema.yaml +132 -0
|
@@ -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"
|