vipcare 0.5.2 → 0.6.1

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/CLAUDE.md CHANGED
@@ -26,7 +26,6 @@ lib/
26
26
  templates.js — Profile templates
27
27
  tests/ — One test file per module
28
28
  profiles/ — Generated profile Markdown files
29
- web/ — Card HTML output
30
29
  skill/ — Claude Code slash command
31
30
  ```
32
31
 
package/lib/card.js CHANGED
@@ -72,6 +72,7 @@ export function generateCards(profiles, outputPath) {
72
72
  expertise: [],
73
73
  superpower: '',
74
74
  quote: summaryMatch ? summaryMatch[1] : (p.summary || ''),
75
+ updated: p.updated,
75
76
  annotations,
76
77
  last_connection: lastConnection,
77
78
  });
@@ -79,6 +80,7 @@ export function generateCards(profiles, outputPath) {
79
80
  }
80
81
 
81
82
  data.slug = p.slug;
83
+ data.updated = p.updated;
82
84
  data.twitter_handle = data.twitter_handle || twitterHandle;
83
85
  data.annotations = annotations;
84
86
  data.last_connection = lastConnection;
@@ -160,103 +162,87 @@ function buildHtml(cards) {
160
162
  <head>
161
163
  <meta charset="UTF-8">
162
164
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
163
- <title>VIPCare - Baseball Cards</title>
165
+ <title>VIPCare</title>
164
166
  <style>
165
167
  * { margin: 0; padding: 0; box-sizing: border-box; }
166
168
  html { scroll-behavior: smooth; }
167
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f8fafc; color: #1e293b; min-height: 100vh; padding: 20px; padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); }
168
- h1 { text-align: center; font-size: 1.8em; margin: 20px 0 30px; color: #2563eb; }
169
- .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; max-width: 1200px; margin: 0 auto; }
169
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f8fafc; color: #1e293b; min-height: 100vh; }
170
+ .layout { display: flex; min-height: 100vh; }
171
+ .main { flex: 1; padding: 20px; overflow-y: auto; transition: margin-right 0.3s; }
172
+ .main.panel-open { margin-right: 420px; }
173
+ header { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto 24px; }
174
+ header h1 { font-size: 1.5em; color: #2563eb; }
175
+ header .count { color: #94a3b8; font-size: 0.85em; }
176
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; max-width: 1200px; margin: 0 auto; }
170
177
 
171
178
  .card {
172
179
  background: #ffffff;
173
- border-radius: 16px;
174
- padding: 24px;
180
+ border-radius: 12px;
181
+ padding: 20px;
175
182
  border: 1px solid #e2e8f0;
176
183
  cursor: pointer;
177
- transition: transform 0.2s, box-shadow 0.2s;
184
+ transition: all 0.2s;
178
185
  position: relative;
179
- overflow: hidden;
180
- min-height: 44px;
181
- -webkit-tap-highlight-color: transparent;
182
186
  }
183
- .card:hover { transform: translateY(-4px); box-shadow: 0 12px 40px rgba(37,99,235,0.12); }
184
- .card:active { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(37,99,235,0.08); }
185
- .card::before {
186
- content: '';
187
- position: absolute;
188
- top: 0; left: 0; right: 0;
189
- height: 4px;
190
- background: linear-gradient(90deg, #2563eb, #7c3aed, #db2777);
191
- }
192
-
193
- .card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
194
- .card-name { font-size: 1.4em; font-weight: 700; color: #0f172a; }
195
- .card-role { font-size: 0.85em; color: #64748b; margin-top: 2px; }
196
- .card-badges { display: flex; gap: 6px; }
197
- .badge { padding: 4px 10px; border-radius: 6px; font-size: 0.75em; font-weight: 700; min-height: 28px; display: inline-flex; align-items: center; }
198
- .badge-disc { background: #2563eb; color: #ffffff; }
199
- .badge-mbti { background: #7c3aed; color: #ffffff; }
200
-
201
- .card-quote { font-style: italic; color: #64748b; font-size: 0.8em; margin: 10px 0; padding: 8px 12px; border-left: 3px solid #e2e8f0; }
202
-
203
- .radar-container { display: flex; justify-content: center; margin: 16px 0; }
204
- .radar { width: 200px; height: 200px; max-width: 100%; }
205
-
206
- .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
207
- .tag { background: #eff6ff; color: #2563eb; padding: 4px 12px; border-radius: 12px; font-size: 0.75em; min-height: 28px; display: inline-flex; align-items: center; }
208
-
209
- .expertise { margin: 10px 0; }
210
- .expertise-title { font-size: 0.75em; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
211
- .expertise-item { font-size: 0.8em; color: #475569; padding: 2px 0; }
212
- .superpower { color: #d97706; font-weight: 600; font-size: 0.85em; margin: 6px 0; }
213
-
214
- .tips { margin-top: 12px; border-top: 1px solid #e2e8f0; padding-top: 12px; }
215
- .tip-row { display: flex; gap: 4px; font-size: 0.8em; margin: 4px 0; color: #475569; min-height: 44px; align-items: center; }
216
- .tip-icon { width: 20px; text-align: center; }
217
- .tip-label { color: #94a3b8; min-width: 55px; }
218
-
219
- /* Modal */
220
- .modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); z-index: 100; justify-content: center; align-items: center; padding: 20px; }
221
- .modal-overlay.active { display: flex; }
222
- .modal {
223
- background: #ffffff; border-radius: 16px; max-width: 600px; width: 100%; max-height: 90vh; overflow-y: auto; padding: 32px;
224
- border: 1px solid #e2e8f0; box-shadow: 0 25px 50px rgba(0,0,0,0.15);
187
+ .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.08); border-color: #cbd5e1; }
188
+ .card.active { border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.2); }
189
+ .card-name { font-size: 1.1em; font-weight: 700; color: #0f172a; }
190
+ .card-role { font-size: 0.8em; color: #64748b; margin-top: 2px; }
191
+ .badge { padding: 3px 8px; border-radius: 4px; font-size: 0.7em; font-weight: 700; display: inline-flex; align-items: center; cursor: help; }
192
+ .badge-nt { background: #ede9fe; color: #7c3aed; }
193
+ .badge-nf { background: #d1fae5; color: #059669; }
194
+ .badge-sj { background: #dbeafe; color: #2563eb; }
195
+ .badge-sp { background: #fef3c7; color: #d97706; }
196
+ .badge-unknown { background: #f1f5f9; color: #94a3b8; }
197
+ .news-chip { background: #f0f9ff; padding: 6px 10px; border-radius: 6px; font-size: 0.75em; color: #1e40af; margin: 8px 0; }
198
+ .section-label { font-size: 0.65em; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; margin: 10px 0 4px; }
199
+ .tags { display: flex; flex-wrap: wrap; gap: 4px; margin: 8px 0; }
200
+ .tag { background: #f1f5f9; color: #64748b; padding: 2px 8px; border-radius: 4px; font-size: 0.7em; }
201
+ .do-dont { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 8px 0; }
202
+ .do-box, .dont-box { padding: 8px; border-radius: 6px; font-size: 0.75em; }
203
+ .do-box { background: #f0fdf4; color: #166534; }
204
+ .dont-box { background: #fef2f2; color: #991b1b; }
205
+
206
+ /* Side Panel */
207
+ .panel {
208
+ position: fixed; top: 0; right: -440px; width: 420px; height: 100vh;
209
+ background: #ffffff; border-left: 1px solid #e2e8f0;
210
+ overflow-y: auto; transition: right 0.3s ease;
211
+ box-shadow: -4px 0 20px rgba(0,0,0,0.05);
212
+ z-index: 50; padding: 24px;
225
213
  -webkit-overflow-scrolling: touch;
226
214
  }
227
- .modal-close { float: right; background: none; border: none; color: #94a3b8; font-size: 1.5em; cursor: pointer; min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
228
- .modal h2 { color: #2563eb; margin: 16px 0 8px; font-size: 1.1em; }
229
- .modal p, .modal li { color: #475569; font-size: 0.9em; line-height: 1.6; }
230
- .modal ul { padding-left: 20px; }
231
-
232
- /* Mobile: screens < 480px */
233
- @media (max-width: 480px) {
234
- body { padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)); }
235
- h1 { font-size: 1.5em; margin: 12px 0 20px; }
236
- .grid { grid-template-columns: 1fr; gap: 16px; }
237
- .card { padding: 18px; }
238
- .card-name { font-size: 1.25em; }
239
- .card-role { font-size: 0.9em; }
240
- .card-quote { font-size: 0.85em; }
241
- .tip-row { font-size: 0.85em; }
242
- .radar { width: 180px; height: 180px; }
243
- .badge { font-size: 0.8em; padding: 5px 12px; }
244
- .tag { font-size: 0.8em; padding: 5px 14px; }
245
-
246
- .modal-overlay { padding: 0; align-items: stretch; }
247
- .modal { max-width: 100%; max-height: 100vh; height: 100%; border-radius: 0; padding: 20px; padding-top: max(20px, env(safe-area-inset-top)); padding-bottom: max(20px, env(safe-area-inset-bottom)); }
248
- .modal h2 { font-size: 1.15em; }
249
- .modal p, .modal li { font-size: 0.95em; line-height: 1.7; }
215
+ .panel.open { right: 0; }
216
+ .panel-close { position: absolute; top: 16px; right: 16px; background: none; border: none; color: #94a3b8; font-size: 1.2em; cursor: pointer; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; }
217
+ .panel-close:hover { background: #f1f5f9; color: #475569; }
218
+ .panel h2 { color: #2563eb; font-size: 0.9em; margin: 20px 0 8px; padding-bottom: 4px; border-bottom: 1px solid #f1f5f9; }
219
+ .panel p, .panel li { color: #475569; font-size: 0.85em; line-height: 1.6; }
220
+ .panel ul { padding-left: 16px; }
221
+ .panel blockquote { border-left: 3px solid #2563eb; padding: 4px 12px; margin: 6px 0; color: #475569; font-style: italic; font-size: 0.85em; }
222
+ .radar { max-width: 100%; }
223
+
224
+ /* Mobile */
225
+ @media (max-width: 768px) {
226
+ .main.panel-open { margin-right: 0; }
227
+ .panel { width: 100%; right: -100%; }
228
+ .grid { grid-template-columns: 1fr; }
250
229
  }
251
230
  </style>
252
231
  </head>
253
232
  <body>
254
-
255
- <h1>VIPCare</h1>
256
- <div class="grid" id="grid"></div>
257
-
258
- <div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
259
- <div class="modal" id="modal-content"></div>
233
+ <div class="layout">
234
+ <div class="main" id="main">
235
+ <header>
236
+ <h1>VIPCare</h1>
237
+ <input type="text" id="search" placeholder="Search by name or tag..." style="padding:8px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:0.9em;width:260px;outline:none;color:#1e293b;background:#fff;" onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'">
238
+ <span class="count" id="count"></span>
239
+ </header>
240
+ <div class="grid" id="grid"></div>
241
+ </div>
242
+ <div class="panel" id="panel">
243
+ <button class="panel-close" onclick="closePanel()">&times;</button>
244
+ <div id="panel-content"></div>
245
+ </div>
260
246
  </div>
261
247
 
262
248
  <script>
@@ -314,124 +300,135 @@ function radarSvg(scores, size = 200) {
314
300
  return \`<svg viewBox="0 0 \${size} \${size}" class="radar">\${gridLines}\${axes}\${dataPolygon}\${labels}</svg>\`;
315
301
  }
316
302
 
303
+ function mbtiClass(mbti) {
304
+ if (!mbti || mbti === '?') return 'badge-unknown';
305
+ const t = mbti.toUpperCase();
306
+ if (t.includes('NT')) return 'badge-nt';
307
+ if (t.includes('NF')) return 'badge-nf';
308
+ if (t[1] === 'S' && t[3] === 'J') return 'badge-sj';
309
+ if (t[1] === 'S' && t[3] === 'P') return 'badge-sp';
310
+ return 'badge-nt';
311
+ }
312
+
313
+ let activeCard = null;
314
+
317
315
  function renderCard(card, index) {
318
316
  const avatarUrl = \`https://unavatar.io/twitter/\${card.twitter_handle || card.slug || 'unknown'}\`;
319
-
320
317
  return \`
321
- <div class="card" onclick="openModal(\${index})">
322
- <div class="card-header">
323
- <div style="display:flex;gap:12px;align-items:center">
324
- <img src="\${avatarUrl}" alt="" style="width:48px;height:48px;border-radius:50%;object-fit:cover;border:2px solid #e2e8f0" onerror="this.style.display='none'">
325
- <div>
326
- <div class="card-name">\${card.name || 'Unknown'}</div>
327
- <div class="card-role">\${card.one_liner || card.title || ''}</div>
328
- </div>
318
+ <div class="card" id="card-\${index}" onclick="openPanel(\${index})">
319
+ <div style="display:flex;gap:10px;align-items:center;margin-bottom:8px">
320
+ <img src="\${avatarUrl}" alt="" style="width:40px;height:40px;border-radius:50%;object-fit:cover;border:2px solid #e2e8f0" onerror="this.style.display='none'">
321
+ <div style="flex:1;min-width:0">
322
+ <div class="card-name">\${card.name || 'Unknown'}</div>
323
+ <div class="card-role">\${card.one_liner || card.title || ''}</div>
329
324
  </div>
330
- <span class="badge badge-mbti" title="\${card.mbti_reason || ''}">\${card.mbti || '?'}</span>
325
+ <span class="badge \${mbtiClass(card.mbti)}" title="\${card.mbti_reason || ''}">\${card.mbti || '?'}</span>
331
326
  </div>
332
- \${card.latest_news ? \`<div style="background:#f0f9ff;padding:8px 12px;border-radius:8px;font-size:0.8em;color:#1e40af;margin:8px 0">📰 \${card.latest_news.slice(0, 120)}\${card.latest_news.length > 120 ? '...' : ''}</div>\` : ''}
333
- \${card.current_focus ? \`<div style="font-size:0.85em;color:#475569;margin:6px 0"><strong>Focus:</strong> \${card.current_focus}</div>\` : ''}
334
- \${card.wants ? \`<div style="font-size:0.85em;color:#475569;margin:6px 0"><strong>Wants:</strong> \${card.wants}</div>\` : ''}
335
- \${card.last_connection ? \`<div style="font-size:0.8em;color:#2563eb;margin:6px 0">🤝 \${card.last_connection}</div>\` : \`<div style="font-size:0.8em;color:#94a3b8;margin:6px 0">💡 \${card.icebreakers?.[0] || 'No connection yet'}</div>\`}
336
- \${card.talking_points?.length ? \`<div style="margin:8px 0"><div style="font-size:0.75em;color:#94a3b8;text-transform:uppercase;margin-bottom:4px">Talking Points</div>\${card.talking_points.slice(0,2).map(t => \`<div style="font-size:0.8em;color:#475569;padding:2px 0">• \${t}</div>\`).join('')}</div>\` : ''}
337
- \${card.annotations?.length ? \`<div style="border-top:1px solid #e2e8f0;margin-top:8px;padding-top:8px;font-size:0.75em;color:#64748b">📝 \${card.annotations[card.annotations.length-1]}</div>\` : ''}
338
- <button onclick="event.stopPropagation();regenerateProfile('\${card.slug}',this)" style="margin-top:10px;width:100%;padding:6px;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;color:#64748b;font-size:0.75em;cursor:pointer;transition:all 0.2s" onmouseover="this.style.borderColor='#2563eb';this.style.color='#2563eb'" onmouseout="this.style.borderColor='#e2e8f0';this.style.color='#64748b'">🔄 Update Profile</button>
327
+ \${card.latest_news ? \`<div class="news-chip">📰 \${card.latest_news.slice(0, 100)}\${card.latest_news.length > 100 ? '...' : ''}</div>\` : ''}
328
+ \${card.current_focus ? \`<div style="font-size:0.8em;color:#475569;margin:4px 0">🎯 \${card.current_focus}</div>\` : ''}
329
+ \${card.last_connection ? \`<div style="font-size:0.75em;color:#2563eb;margin:4px 0">🤝 \${card.last_connection}</div>\` : (card.icebreakers?.[0] ? \`<div style="font-size:0.75em;color:#94a3b8;margin:4px 0">💡 \${card.icebreakers[0]}</div>\` : '')}
330
+ \${card.talking_points?.length ? \`<div class="section-label">Talking Points</div>\${card.talking_points.slice(0,2).map(t => \`<div style="font-size:0.75em;color:#475569">• \${t}</div>\`).join('')}\` : ''}
331
+ \${card.tags?.length ? \`<div class="tags">\${card.tags.slice(0,3).map(t => \`<span class="tag">\${t}</span>\`).join('')}</div>\` : ''}
339
332
  </div>\`;
340
333
  }
341
334
 
342
- function openModal(index) {
335
+ function openPanel(index) {
336
+ // Highlight active card
337
+ if (activeCard !== null) document.getElementById('card-' + activeCard)?.classList.remove('active');
338
+ activeCard = index;
339
+ document.getElementById('card-' + index)?.classList.add('active');
340
+
343
341
  const card = cards[index];
344
342
  const s = card.scores || {};
345
- const modal = document.getElementById('modal-content');
346
-
343
+ const panel = document.getElementById('panel-content');
347
344
  const avatarUrl = \`https://unavatar.io/twitter/\${card.twitter_handle || card.slug || 'unknown'}\`;
348
345
 
349
- modal.innerHTML = \`
350
- <button class="modal-close" onclick="closeModal()">&times;</button>
351
- <div style="display:flex;gap:16px;align-items:center;margin-bottom:12px">
352
- <img src="\${avatarUrl}" alt="" style="width:64px;height:64px;border-radius:50%;object-fit:cover;border:2px solid #e2e8f0" onerror="this.style.display='none'">
346
+ panel.innerHTML = \`
347
+ <div style="display:flex;gap:14px;align-items:center;margin-bottom:16px">
348
+ <img src="\${avatarUrl}" alt="" style="width:56px;height:56px;border-radius:50%;object-fit:cover;border:2px solid #e2e8f0" onerror="this.style.display='none'">
353
349
  <div>
354
- <h1 style="color:#2563eb;margin-bottom:4px;font-size:1.5em">\${card.name}</h1>
355
- <p style="color:#64748b">\${card.title || ''}\${card.company ? ' @ ' + card.company : ''}</p>
356
- \${card.previous_role ? \`<p style="color:#94a3b8;font-size:0.8em">Previously: \${card.previous_role}</p>\` : ''}
357
- \${card.twitter_handle ? \`<a href="https://twitter.com/\${card.twitter_handle}" target="_blank" style="color:#2563eb;font-size:0.85em;text-decoration:none">@\${card.twitter_handle}</a>\` : ''}
350
+ <div style="font-size:1.3em;font-weight:700;color:#0f172a">\${card.name}</div>
351
+ <div style="font-size:0.85em;color:#64748b">\${card.title || ''}\${card.company ? ' @ ' + card.company : ''}</div>
352
+ \${card.previous_role ? \`<div style="font-size:0.75em;color:#94a3b8">Previously: \${card.previous_role}</div>\` : ''}
353
+ <div style="margin-top:4px">
354
+ \${card.twitter_handle ? \`<a href="https://twitter.com/\${card.twitter_handle}" target="_blank" style="color:#2563eb;font-size:0.8em;text-decoration:none;margin-right:8px">@\${card.twitter_handle}</a>\` : ''}
355
+ <span class="badge \${mbtiClass(card.mbti)}" title="\${card.mbti_reason || ''}">\${card.mbti || '?'}</span>
356
+ </div>
358
357
  </div>
359
358
  </div>
360
359
 
361
- \${card.latest_news ? \`<div style="background:#f0f9ff;padding:10px 14px;border-radius:8px;font-size:0.85em;color:#1e40af;margin:12px 0">📰 \${card.latest_news}</div>\` : ''}
360
+ \${card.latest_news ? \`<div class="news-chip" style="margin-bottom:12px">📰 \${card.latest_news}</div>\` : ''}
362
361
 
363
- <h2>Current Focus</h2>
364
- \${card.current_focus ? \`<p>\${card.current_focus}</p>\` : '<p style="color:#94a3b8">No data available.</p>'}
365
- \${card.wants ? \`<p><strong>Wants:</strong> \${card.wants}</p>\` : ''}
362
+ <h2>Focus & Goals</h2>
363
+ \${card.current_focus ? \`<p>🎯 \${card.current_focus}</p>\` : ''}
364
+ \${card.wants ? \`<p>💎 \${card.wants}</p>\` : ''}
366
365
 
367
- \${card.philosophy?.length ? \`<h2>Core Philosophy</h2>\${card.philosophy.map(p => \`<blockquote style="border-left:3px solid #2563eb;padding:4px 12px;margin:8px 0;color:#475569;font-style:italic">"\${p}"</blockquote>\`).join('')}\` : ''}
368
-
369
- \${card.competition?.length ? \`<h2>Competition</h2><p>\${card.competition.join(', ')}</p>\` : ''}
366
+ \${card.philosophy?.length ? \`<h2>Philosophy</h2>\${card.philosophy.map(p => \`<blockquote>"\${p}"</blockquote>\`).join('')}\` : ''}
370
367
 
371
368
  \${card.talking_points?.length ? \`<h2>Talking Points</h2><ul>\${card.talking_points.map(t => \`<li>\${t}</li>\`).join('')}</ul>\` : ''}
372
369
 
373
- <h2>How to Work With Them</h2>
374
- \${card.last_connection ? \`<p><strong>🤝 Last:</strong> \${card.last_connection}</p>\` : ''}
375
- \${card.dos?.length ? \`<p><strong>✅ Do:</strong> \${card.dos.join(' · ')}</p>\` : ''}
376
- \${card.donts?.length ? \`<p><strong>❌ Don't:</strong> \${card.donts.join(' · ')}</p>\` : ''}
377
- \${card.gifts?.length ? \`<p><strong>🎁 Gifts:</strong> \${card.gifts.join(', ')}</p>\` : ''}
370
+ <h2>Approach</h2>
371
+ \${card.last_connection ? \`<p>🤝 <strong>Last:</strong> \${card.last_connection}</p>\` : '<p style="color:#94a3b8">No interactions yet</p>'}
372
+ \${card.dos?.length || card.donts?.length ? \`<div class="do-dont">\${card.dos?.length ? \`<div class="do-box"><strong>✅ Do</strong><br>\${card.dos.join('<br>')}</div>\` : ''}\${card.donts?.length ? \`<div class="dont-box"><strong>❌ Don't</strong><br>\${card.donts.join('<br>')}</div>\` : ''}</div>\` : ''}
373
+ \${card.gifts?.length ? \`<p style="font-size:0.85em">🎁 \${card.gifts.join(', ')}</p>\` : ''}
378
374
 
379
- \${card.key_quotes?.length ? \`<h2>Key Quotes</h2>\${card.key_quotes.slice(0,5).map(q => \`<p style="font-size:0.85em;color:#475569;padding:2px 0">• "\${q}"</p>\`).join('')}\` : ''}
375
+ \${card.competition?.length ? \`<h2>Competition</h2><p style="font-size:0.85em">\${card.competition.join(', ')}</p>\` : ''}
380
376
 
381
- <h2>Personality</h2>
382
- <p><strong>MBTI:</strong> <span style="cursor:help;border-bottom:1px dashed #94a3b8" title="\${card.mbti_reason || ''}">\${card.mbti || '?'}</span>\${card.mbti_reason ? \` — \${card.mbti_reason}\` : ''}</p>
383
- \${card.superpower ? \`<p><strong>⚡ Superpower:</strong> \${card.superpower}</p>\` : ''}
384
- <div style="display:flex;justify-content:center;margin:16px 0">\${radarSvg(s, 240)}</div>
377
+ \${card.key_quotes?.length ? \`<h2>Quotes</h2>\${card.key_quotes.slice(0,4).map(q => \`<p style="font-size:0.8em;color:#475569;margin:3px 0">"\${q}"</p>\`).join('')}\` : ''}
385
378
 
386
- \${card.annotations?.length ? \`<h2>Your Notes</h2>\${card.annotations.map(a => \`<p style="font-size:0.85em;color:#64748b">📝 \${a}</p>\`).join('')}\` : ''}
379
+ <h2>Personality</h2>
380
+ <p style="font-size:0.85em"><strong>MBTI:</strong> \${card.mbti || '?'}\${card.mbti_reason ? \` — \${card.mbti_reason}\` : ''}</p>
381
+ \${card.superpower ? \`<p style="font-size:0.85em">⚡ \${card.superpower}</p>\` : ''}
382
+ <div style="display:flex;justify-content:center;margin:12px 0">\${radarSvg(s, 220)}</div>
387
383
 
388
- \${card.tags?.length ? \`<div class="tags" style="margin-top:12px">\${card.tags.map(t => \`<span class="tag">\${t}</span>\`).join('')}</div>\` : ''}
384
+ \${card.annotations?.length ? \`<h2>Your Notes</h2>\${card.annotations.map(a => \`<p style="font-size:0.8em;color:#64748b">📝 \${a}</p>\`).join('')}\` : ''}
389
385
 
390
- <div style="display:flex;gap:10px;justify-content:center;margin-top:20px">
391
- \${card.slug ? \`<a href="\${card.slug}.html" style="color:#2563eb;text-decoration:none;padding:8px 20px;border:1px solid #2563eb;border-radius:8px;display:inline-block">View Full Profile →</a>\` : ''}
392
- \${card.slug ? \`<button onclick="regenerateProfile('\${card.slug}',this)" style="padding:8px 20px;border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;color:#64748b;cursor:pointer;font-size:0.9em">🔄 Update</button>\` : ''}
386
+ <div style="display:flex;gap:8px;margin-top:20px">
387
+ \${card.slug ? \`<a href="\${card.slug}.html" style="flex:1;text-align:center;color:#2563eb;text-decoration:none;padding:8px;border:1px solid #2563eb;border-radius:6px;font-size:0.85em">Full Profile</a>\` : ''}
388
+ \${card.slug ? \`<button onclick="regenerateProfile('\${card.slug}',this)" style="flex:1;padding:8px;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;color:#64748b;cursor:pointer;font-size:0.85em">🔄 Update</button>\` : ''}
393
389
  </div>
394
390
  \`;
395
391
 
396
- document.getElementById('modal').classList.add('active');
392
+ document.getElementById('panel').classList.add('open');
393
+ document.getElementById('main').classList.add('panel-open');
397
394
  }
398
395
 
399
- function closeModal() {
400
- document.getElementById('modal').classList.remove('active');
396
+ function closePanel() {
397
+ document.getElementById('panel').classList.remove('open');
398
+ document.getElementById('main').classList.remove('panel-open');
399
+ if (activeCard !== null) document.getElementById('card-' + activeCard)?.classList.remove('active');
400
+ activeCard = null;
401
401
  }
402
402
 
403
- document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
403
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closePanel(); });
404
404
 
405
405
  async function regenerateProfile(slug, btn) {
406
- const originalText = btn.textContent;
406
+ const orig = btn.textContent;
407
407
  btn.textContent = '⏳ Updating...';
408
408
  btn.disabled = true;
409
- btn.style.opacity = '0.6';
410
-
411
409
  try {
412
410
  const resp = await fetch(\`/api/regenerate/\${slug}\`);
413
- const data = await resp.json();
414
-
415
- if (resp.ok) {
416
- btn.textContent = '✅ Updated!';
417
- btn.style.color = '#16a34a';
418
- // Reload page after a short delay to show new data
419
- setTimeout(() => location.reload(), 1500);
420
- } else {
421
- btn.textContent = '❌ ' + (data.error || 'Failed');
422
- btn.style.color = '#dc2626';
423
- setTimeout(() => { btn.textContent = originalText; btn.disabled = false; btn.style.opacity = '1'; btn.style.color = ''; }, 3000);
424
- }
425
- } catch (e) {
426
- btn.textContent = '❌ Error';
427
- btn.style.color = '#dc2626';
428
- setTimeout(() => { btn.textContent = originalText; btn.disabled = false; btn.style.opacity = '1'; btn.style.color = ''; }, 3000);
429
- }
411
+ if (resp.ok) { btn.textContent = '✅ Done'; setTimeout(() => location.reload(), 1000); }
412
+ else { const d = await resp.json(); btn.textContent = '❌ ' + (d.error||'Failed'); setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 3000); }
413
+ } catch { btn.textContent = '❌ Error'; setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 3000); }
430
414
  }
431
415
 
416
+ // Sort by most recently updated
417
+ cards.sort((a, b) => (b.updated || '').localeCompare(a.updated || ''));
418
+
432
419
  // Render
433
420
  const grid = document.getElementById('grid');
434
421
  grid.innerHTML = cards.map((card, i) => renderCard(card, i)).join('');
422
+ document.getElementById('count').textContent = cards.length + ' contacts';
423
+
424
+ document.getElementById('search').addEventListener('input', (e) => {
425
+ const q = e.target.value.toLowerCase();
426
+ cards.forEach((card, i) => {
427
+ const el = document.getElementById('card-' + i);
428
+ const text = [card.name, card.company, card.title, ...(card.tags || [])].join(' ').toLowerCase();
429
+ el.style.display = text.includes(q) ? '' : 'none';
430
+ });
431
+ });
435
432
  </script>
436
433
  </body>
437
434
  </html>`;
@@ -12,8 +12,8 @@ function getBackend() {
12
12
  const config = loadConfig();
13
13
  if (config.ai_backend) return config.ai_backend.toLowerCase();
14
14
 
15
- if (checkTool('claude')) return 'claude-cli';
16
15
  if (process.env.ANTHROPIC_API_KEY || config.anthropic_api_key) return 'anthropic';
16
+ if (checkTool('claude')) return 'claude-cli';
17
17
  if (checkTool('gh') && copilotAvailable()) return 'copilot-cli';
18
18
 
19
19
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vipcare",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
5
  "type": "module",
6
6
  "bin": {
package/web/index.html DELETED
@@ -1,229 +0,0 @@
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>VIPCare - Baseball Cards</title>
7
- <style>
8
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
- html { scroll-behavior: smooth; }
10
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; padding: 20px; padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); }
11
- h1 { text-align: center; font-size: 1.8em; margin: 20px 0 30px; color: #38bdf8; }
12
- .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; max-width: 1200px; margin: 0 auto; }
13
-
14
- .card {
15
- background: linear-gradient(145deg, #1e293b, #334155);
16
- border-radius: 16px;
17
- padding: 24px;
18
- border: 1px solid #475569;
19
- cursor: pointer;
20
- transition: transform 0.2s, box-shadow 0.2s;
21
- position: relative;
22
- overflow: hidden;
23
- min-height: 44px;
24
- -webkit-tap-highlight-color: transparent;
25
- }
26
- .card:hover { transform: translateY(-4px); box-shadow: 0 12px 40px rgba(56,189,248,0.15); }
27
- .card:active { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(56,189,248,0.1); }
28
- .card::before {
29
- content: '';
30
- position: absolute;
31
- top: 0; left: 0; right: 0;
32
- height: 4px;
33
- background: linear-gradient(90deg, #38bdf8, #818cf8, #c084fc);
34
- }
35
-
36
- .card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
37
- .card-name { font-size: 1.4em; font-weight: 700; color: #f1f5f9; }
38
- .card-role { font-size: 0.85em; color: #94a3b8; margin-top: 2px; }
39
- .card-badges { display: flex; gap: 6px; }
40
- .badge { padding: 4px 10px; border-radius: 6px; font-size: 0.75em; font-weight: 700; min-height: 28px; display: inline-flex; align-items: center; }
41
- .badge-disc { background: #38bdf8; color: #0f172a; }
42
- .badge-mbti { background: #818cf8; color: #0f172a; }
43
-
44
- .card-quote { font-style: italic; color: #94a3b8; font-size: 0.8em; margin: 10px 0; padding: 8px 12px; border-left: 3px solid #475569; }
45
-
46
- .radar-container { display: flex; justify-content: center; margin: 16px 0; }
47
- .radar { width: 200px; height: 200px; max-width: 100%; }
48
-
49
- .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
50
- .tag { background: #1e3a5f; color: #38bdf8; padding: 4px 12px; border-radius: 12px; font-size: 0.75em; min-height: 28px; display: inline-flex; align-items: center; }
51
-
52
- .expertise { margin: 10px 0; }
53
- .expertise-title { font-size: 0.75em; color: #64748b; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
54
- .expertise-item { font-size: 0.8em; color: #cbd5e1; padding: 2px 0; }
55
- .superpower { color: #fbbf24; font-weight: 600; font-size: 0.85em; margin: 6px 0; }
56
-
57
- .tips { margin-top: 12px; border-top: 1px solid #475569; padding-top: 12px; }
58
- .tip-row { display: flex; gap: 4px; font-size: 0.8em; margin: 4px 0; color: #cbd5e1; min-height: 44px; align-items: center; }
59
- .tip-icon { width: 20px; text-align: center; }
60
- .tip-label { color: #64748b; min-width: 55px; }
61
-
62
- /* Modal */
63
- .modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 100; justify-content: center; align-items: center; padding: 20px; }
64
- .modal-overlay.active { display: flex; }
65
- .modal {
66
- background: #1e293b; border-radius: 16px; max-width: 600px; width: 100%; max-height: 90vh; overflow-y: auto; padding: 32px;
67
- border: 1px solid #475569;
68
- -webkit-overflow-scrolling: touch;
69
- }
70
- .modal-close { float: right; background: none; border: none; color: #94a3b8; font-size: 1.5em; cursor: pointer; min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
71
- .modal h2 { color: #38bdf8; margin: 16px 0 8px; font-size: 1.1em; }
72
- .modal p, .modal li { color: #cbd5e1; font-size: 0.9em; line-height: 1.6; }
73
- .modal ul { padding-left: 20px; }
74
-
75
- /* Mobile: screens < 480px */
76
- @media (max-width: 480px) {
77
- body { padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)); }
78
- h1 { font-size: 1.5em; margin: 12px 0 20px; }
79
- .grid { grid-template-columns: 1fr; gap: 16px; }
80
- .card { padding: 18px; }
81
- .card-name { font-size: 1.25em; }
82
- .card-role { font-size: 0.9em; }
83
- .card-quote { font-size: 0.85em; }
84
- .tip-row { font-size: 0.85em; }
85
- .radar { width: 180px; height: 180px; }
86
- .badge { font-size: 0.8em; padding: 5px 12px; }
87
- .tag { font-size: 0.8em; padding: 5px 14px; }
88
-
89
- .modal-overlay { padding: 0; align-items: stretch; }
90
- .modal { max-width: 100%; max-height: 100vh; height: 100%; border-radius: 0; padding: 20px; padding-top: max(20px, env(safe-area-inset-top)); padding-bottom: max(20px, env(safe-area-inset-bottom)); }
91
- .modal h2 { font-size: 1.15em; }
92
- .modal p, .modal li { font-size: 0.95em; line-height: 1.7; }
93
- }
94
- </style>
95
- </head>
96
- <body>
97
-
98
- <h1>VIPCare</h1>
99
- <div class="grid" id="grid"></div>
100
-
101
- <div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
102
- <div class="modal" id="modal-content"></div>
103
- </div>
104
-
105
- <script>
106
- const cards = [{"name":"Compare JSON","title":"VP","company":"ACME","location":"","disc":"?","mbti":"?","scores":{},"tags":[],"icebreakers":[],"dos":[],"donts":[],"gifts":[],"expertise":[],"superpower":"","quote":"Test"},{"name":"Sam Altman","title":"","company":"","location":"","disc":"?","mbti":"?","scores":{},"tags":[],"icebreakers":[],"dos":[],"donts":[],"gifts":[],"expertise":[],"superpower":"","quote":""}];
107
-
108
- const SCORE_LABELS = {
109
- openness: 'Openness',
110
- conscientiousness: 'Conscientiousness',
111
- extraversion: 'Extraversion',
112
- agreeableness: 'Agreeableness',
113
- resilience: 'Resilience',
114
- decision_style: 'Decision',
115
- risk_appetite: 'Risk',
116
- communication: 'Communication',
117
- influence: 'Influence',
118
- leadership: 'Leadership'
119
- };
120
-
121
- function radarSvg(scores, size = 200) {
122
- const keys = Object.keys(SCORE_LABELS);
123
- const cx = size / 2, cy = size / 2, r = size * 0.38;
124
- const n = keys.length;
125
-
126
- let gridLines = '';
127
- for (let level = 1; level <= 5; level++) {
128
- const lr = r * level / 5;
129
- let pts = [];
130
- for (let i = 0; i < n; i++) {
131
- const angle = (Math.PI * 2 * i / n) - Math.PI / 2;
132
- pts.push(`${cx + lr * Math.cos(angle)},${cy + lr * Math.sin(angle)}`);
133
- }
134
- gridLines += `<polygon points="${pts.join(' ')}" fill="none" stroke="#334155" stroke-width="0.5"/>`;
135
- }
136
-
137
- let axes = '', labels = '', dataPoints = [];
138
- for (let i = 0; i < n; i++) {
139
- const angle = (Math.PI * 2 * i / n) - Math.PI / 2;
140
- const x = cx + r * Math.cos(angle);
141
- const y = cy + r * Math.sin(angle);
142
- axes += `<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}" stroke="#334155" stroke-width="0.5"/>`;
143
-
144
- const lx = cx + (r + 22) * Math.cos(angle);
145
- const ly = cy + (r + 22) * Math.sin(angle);
146
- const label = SCORE_LABELS[keys[i]] || keys[i];
147
- labels += `<text x="${lx}" y="${ly}" text-anchor="middle" dominant-baseline="middle" fill="#64748b" font-size="9">${label}</text>`;
148
-
149
- const val = (scores[keys[i]] || 0) / 5;
150
- const dx = cx + r * val * Math.cos(angle);
151
- const dy = cy + r * val * Math.sin(angle);
152
- dataPoints.push(`${dx},${dy}`);
153
- }
154
-
155
- const dataPolygon = `<polygon points="${dataPoints.join(' ')}" fill="rgba(56,189,248,0.2)" stroke="#38bdf8" stroke-width="1.5"/>`;
156
-
157
- return `<svg viewBox="0 0 ${size} ${size}" class="radar">${gridLines}${axes}${dataPolygon}${labels}</svg>`;
158
- }
159
-
160
- function renderCard(card, index) {
161
- const scores = card.scores || {};
162
- const radar = radarSvg(scores);
163
-
164
- return `
165
- <div class="card" onclick="openModal(${index})">
166
- <div class="card-header">
167
- <div>
168
- <div class="card-name">${card.name || 'Unknown'}</div>
169
- <div class="card-role">${card.title || ''}${card.company ? ' @ ' + card.company : ''}</div>
170
- </div>
171
- <div class="card-badges">
172
- <span class="badge badge-disc">${card.disc || '?'}</span>
173
- <span class="badge badge-mbti">${card.mbti || '?'}</span>
174
- </div>
175
- </div>
176
- ${card.quote ? `<div class="card-quote">"${card.quote.slice(0, 120)}${card.quote.length > 120 ? '...' : ''}"</div>` : ''}
177
- <div class="radar-container">${radar}</div>
178
- ${card.superpower ? `<div class="superpower">⚡ ${card.superpower}</div>` : ''}
179
- ${card.tags?.length ? `<div class="tags">${card.tags.map(t => `<span class="tag">${t}</span>`).join('')}</div>` : ''}
180
- <div class="tips">
181
- ${card.icebreakers?.length ? `<div class="tip-row"><span class="tip-icon">💡</span><span class="tip-label">Icebreaker</span>${card.icebreakers[0]}</div>` : ''}
182
- ${card.dos?.length ? `<div class="tip-row"><span class="tip-icon">✅</span><span class="tip-label">Do</span>${card.dos[0]}</div>` : ''}
183
- ${card.donts?.length ? `<div class="tip-row"><span class="tip-icon">❌</span><span class="tip-label">Don't</span>${card.donts[0]}</div>` : ''}
184
- </div>
185
- </div>`;
186
- }
187
-
188
- function openModal(index) {
189
- const card = cards[index];
190
- const s = card.scores || {};
191
- const modal = document.getElementById('modal-content');
192
-
193
- modal.innerHTML = `
194
- <button class="modal-close" onclick="closeModal()">&times;</button>
195
- <h1 style="color:#38bdf8;margin-bottom:4px">${card.name}</h1>
196
- <p style="color:#94a3b8">${card.title || ''}${card.company ? ' @ ' + card.company : ''}${card.location ? ' · ' + card.location : ''}</p>
197
- ${card.quote ? `<div class="card-quote" style="margin:16px 0">"${card.quote}"</div>` : ''}
198
-
199
- <h2>Personality</h2>
200
- <p><strong>DISC:</strong> ${card.disc || '?'} &nbsp; <strong>MBTI:</strong> ${card.mbti || '?'}</p>
201
- <div style="display:flex;justify-content:center;margin:16px 0">${radarSvg(s, 260)}</div>
202
-
203
- ${card.expertise?.length ? `<h2>Expertise</h2><ul>${card.expertise.map(e => `<li>${e}</li>`).join('')}</ul>` : ''}
204
- ${card.superpower ? `<p><strong>⚡ Superpower:</strong> ${card.superpower}</p>` : ''}
205
-
206
- <h2>How to Work With Them</h2>
207
- ${card.icebreakers?.length ? `<p><strong>💡 Icebreakers:</strong> ${card.icebreakers.join(', ')}</p>` : ''}
208
- ${card.dos?.length ? `<p><strong>✅ Do:</strong> ${card.dos.join(' · ')}</p>` : ''}
209
- ${card.donts?.length ? `<p><strong>❌ Don't:</strong> ${card.donts.join(' · ')}</p>` : ''}
210
- ${card.gifts?.length ? `<p><strong>🎁 Gifts:</strong> ${card.gifts.join(', ')}</p>` : ''}
211
-
212
- ${card.tags?.length ? `<h2>Tags</h2><div class="tags">${card.tags.map(t => `<span class="tag">${t}</span>`).join('')}</div>` : ''}
213
- `;
214
-
215
- document.getElementById('modal').classList.add('active');
216
- }
217
-
218
- function closeModal() {
219
- document.getElementById('modal').classList.remove('active');
220
- }
221
-
222
- document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
223
-
224
- // Render
225
- const grid = document.getElementById('grid');
226
- grid.innerHTML = cards.map((card, i) => renderCard(card, i)).join('');
227
- </script>
228
- </body>
229
- </html>