vipcare 0.5.1 โ†’ 0.6.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/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,86 @@ 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
+ <span class="count" id="count"></span>
238
+ </header>
239
+ <div class="grid" id="grid"></div>
240
+ </div>
241
+ <div class="panel" id="panel">
242
+ <button class="panel-close" onclick="closePanel()">&times;</button>
243
+ <div id="panel-content"></div>
244
+ </div>
260
245
  </div>
261
246
 
262
247
  <script>
@@ -314,124 +299,126 @@ function radarSvg(scores, size = 200) {
314
299
  return \`<svg viewBox="0 0 \${size} \${size}" class="radar">\${gridLines}\${axes}\${dataPolygon}\${labels}</svg>\`;
315
300
  }
316
301
 
302
+ function mbtiClass(mbti) {
303
+ if (!mbti || mbti === '?') return 'badge-unknown';
304
+ const t = mbti.toUpperCase();
305
+ if (t.includes('NT')) return 'badge-nt';
306
+ if (t.includes('NF')) return 'badge-nf';
307
+ if (t[1] === 'S' && t[3] === 'J') return 'badge-sj';
308
+ if (t[1] === 'S' && t[3] === 'P') return 'badge-sp';
309
+ return 'badge-nt';
310
+ }
311
+
312
+ let activeCard = null;
313
+
317
314
  function renderCard(card, index) {
318
315
  const avatarUrl = \`https://unavatar.io/twitter/\${card.twitter_handle || card.slug || 'unknown'}\`;
319
-
320
316
  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>
317
+ <div class="card" id="card-\${index}" onclick="openPanel(\${index})">
318
+ <div style="display:flex;gap:10px;align-items:center;margin-bottom:8px">
319
+ <img src="\${avatarUrl}" alt="" style="width:40px;height:40px;border-radius:50%;object-fit:cover;border:2px solid #e2e8f0" onerror="this.style.display='none'">
320
+ <div style="flex:1;min-width:0">
321
+ <div class="card-name">\${card.name || 'Unknown'}</div>
322
+ <div class="card-role">\${card.one_liner || card.title || ''}</div>
329
323
  </div>
330
- <span class="badge badge-mbti" title="\${card.mbti_reason || ''}">\${card.mbti || '?'}</span>
324
+ <span class="badge \${mbtiClass(card.mbti)}" title="\${card.mbti_reason || ''}">\${card.mbti || '?'}</span>
331
325
  </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>
326
+ \${card.latest_news ? \`<div class="news-chip">๐Ÿ“ฐ \${card.latest_news.slice(0, 100)}\${card.latest_news.length > 100 ? '...' : ''}</div>\` : ''}
327
+ \${card.current_focus ? \`<div style="font-size:0.8em;color:#475569;margin:4px 0">๐ŸŽฏ \${card.current_focus}</div>\` : ''}
328
+ \${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>\` : '')}
329
+ \${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('')}\` : ''}
330
+ \${card.tags?.length ? \`<div class="tags">\${card.tags.slice(0,3).map(t => \`<span class="tag">\${t}</span>\`).join('')}</div>\` : ''}
339
331
  </div>\`;
340
332
  }
341
333
 
342
- function openModal(index) {
334
+ function openPanel(index) {
335
+ // Highlight active card
336
+ if (activeCard !== null) document.getElementById('card-' + activeCard)?.classList.remove('active');
337
+ activeCard = index;
338
+ document.getElementById('card-' + index)?.classList.add('active');
339
+
343
340
  const card = cards[index];
344
341
  const s = card.scores || {};
345
- const modal = document.getElementById('modal-content');
346
-
342
+ const panel = document.getElementById('panel-content');
347
343
  const avatarUrl = \`https://unavatar.io/twitter/\${card.twitter_handle || card.slug || 'unknown'}\`;
348
344
 
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'">
345
+ panel.innerHTML = \`
346
+ <div style="display:flex;gap:14px;align-items:center;margin-bottom:16px">
347
+ <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
348
  <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>\` : ''}
349
+ <div style="font-size:1.3em;font-weight:700;color:#0f172a">\${card.name}</div>
350
+ <div style="font-size:0.85em;color:#64748b">\${card.title || ''}\${card.company ? ' @ ' + card.company : ''}</div>
351
+ \${card.previous_role ? \`<div style="font-size:0.75em;color:#94a3b8">Previously: \${card.previous_role}</div>\` : ''}
352
+ <div style="margin-top:4px">
353
+ \${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>\` : ''}
354
+ <span class="badge \${mbtiClass(card.mbti)}" title="\${card.mbti_reason || ''}">\${card.mbti || '?'}</span>
355
+ </div>
358
356
  </div>
359
357
  </div>
360
358
 
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>\` : ''}
359
+ \${card.latest_news ? \`<div class="news-chip" style="margin-bottom:12px">๐Ÿ“ฐ \${card.latest_news}</div>\` : ''}
362
360
 
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>\` : ''}
361
+ <h2>Focus & Goals</h2>
362
+ \${card.current_focus ? \`<p>๐ŸŽฏ \${card.current_focus}</p>\` : ''}
363
+ \${card.wants ? \`<p>๐Ÿ’Ž \${card.wants}</p>\` : ''}
366
364
 
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>\` : ''}
365
+ \${card.philosophy?.length ? \`<h2>Philosophy</h2>\${card.philosophy.map(p => \`<blockquote>"\${p}"</blockquote>\`).join('')}\` : ''}
370
366
 
371
367
  \${card.talking_points?.length ? \`<h2>Talking Points</h2><ul>\${card.talking_points.map(t => \`<li>\${t}</li>\`).join('')}</ul>\` : ''}
372
368
 
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>\` : ''}
369
+ <h2>Approach</h2>
370
+ \${card.last_connection ? \`<p>๐Ÿค <strong>Last:</strong> \${card.last_connection}</p>\` : '<p style="color:#94a3b8">No interactions yet</p>'}
371
+ \${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>\` : ''}
372
+ \${card.gifts?.length ? \`<p style="font-size:0.85em">๐ŸŽ \${card.gifts.join(', ')}</p>\` : ''}
378
373
 
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('')}\` : ''}
374
+ \${card.competition?.length ? \`<h2>Competition</h2><p style="font-size:0.85em">\${card.competition.join(', ')}</p>\` : ''}
380
375
 
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>
376
+ \${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
377
 
386
- \${card.annotations?.length ? \`<h2>Your Notes</h2>\${card.annotations.map(a => \`<p style="font-size:0.85em;color:#64748b">๐Ÿ“ \${a}</p>\`).join('')}\` : ''}
378
+ <h2>Personality</h2>
379
+ <p style="font-size:0.85em"><strong>MBTI:</strong> \${card.mbti || '?'}\${card.mbti_reason ? \` โ€” \${card.mbti_reason}\` : ''}</p>
380
+ \${card.superpower ? \`<p style="font-size:0.85em">โšก \${card.superpower}</p>\` : ''}
381
+ <div style="display:flex;justify-content:center;margin:12px 0">\${radarSvg(s, 220)}</div>
387
382
 
388
- \${card.tags?.length ? \`<div class="tags" style="margin-top:12px">\${card.tags.map(t => \`<span class="tag">\${t}</span>\`).join('')}</div>\` : ''}
383
+ \${card.annotations?.length ? \`<h2>Your Notes</h2>\${card.annotations.map(a => \`<p style="font-size:0.8em;color:#64748b">๐Ÿ“ \${a}</p>\`).join('')}\` : ''}
389
384
 
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>\` : ''}
385
+ <div style="display:flex;gap:8px;margin-top:20px">
386
+ \${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>\` : ''}
387
+ \${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
388
  </div>
394
389
  \`;
395
390
 
396
- document.getElementById('modal').classList.add('active');
391
+ document.getElementById('panel').classList.add('open');
392
+ document.getElementById('main').classList.add('panel-open');
397
393
  }
398
394
 
399
- function closeModal() {
400
- document.getElementById('modal').classList.remove('active');
395
+ function closePanel() {
396
+ document.getElementById('panel').classList.remove('open');
397
+ document.getElementById('main').classList.remove('panel-open');
398
+ if (activeCard !== null) document.getElementById('card-' + activeCard)?.classList.remove('active');
399
+ activeCard = null;
401
400
  }
402
401
 
403
- document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
402
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closePanel(); });
404
403
 
405
404
  async function regenerateProfile(slug, btn) {
406
- const originalText = btn.textContent;
405
+ const orig = btn.textContent;
407
406
  btn.textContent = 'โณ Updating...';
408
407
  btn.disabled = true;
409
- btn.style.opacity = '0.6';
410
-
411
408
  try {
412
409
  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
- }
410
+ if (resp.ok) { btn.textContent = 'โœ… Done'; setTimeout(() => location.reload(), 1000); }
411
+ else { const d = await resp.json(); btn.textContent = 'โŒ ' + (d.error||'Failed'); setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 3000); }
412
+ } catch { btn.textContent = 'โŒ Error'; setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 3000); }
430
413
  }
431
414
 
415
+ // Sort by most recently updated
416
+ cards.sort((a, b) => (b.updated || '').localeCompare(a.updated || ''));
417
+
432
418
  // Render
433
419
  const grid = document.getElementById('grid');
434
420
  grid.innerHTML = cards.map((card, i) => renderCard(card, i)).join('');
421
+ document.getElementById('count').textContent = cards.length + ' contacts';
435
422
  </script>
436
423
  </body>
437
424
  </html>`;
package/lib/templates.js CHANGED
@@ -4,13 +4,22 @@ synthesize an actionable profile focused on HOW to work with this person.
4
4
 
5
5
  ## Rules
6
6
  - Only include information supported by the provided data
7
- - For personality/MBTI inferences, note these are estimates based on public behavior
8
7
  - Focus on ACTIONABLE intelligence: what they care about NOW, what they want, how to approach them
9
8
  - Include their own words whenever possible (direct quotes)
10
9
  - If user annotations exist, incorporate them into interaction history
11
10
  - If a section has no data, write "No data available."
12
11
  - Output ONLY the Markdown profile followed by the JSON metadata block
13
12
 
13
+ ## MBTI/Personality Rules (CRITICAL)
14
+ - Do NOT default to INTJ. Most people are NOT INTJ.
15
+ - Analyze ACTUAL behavior from the data: How do they communicate? Are they verbose or terse? Do they use humor? Do they share personal feelings? Do they focus on big picture or details? Do they seek consensus or decide alone?
16
+ - E vs I: Frequent public engagement, many tweets, social energy = E. Rare posts, private, reflective = I.
17
+ - S vs N: Focus on concrete details, practical matters = S. Focus on vision, future possibilities = N.
18
+ - T vs F: Decisions based on logic/data = T. Decisions mentioning people/values/impact = F.
19
+ - J vs P: Structured, plans, deadlines = J. Flexible, spontaneous, exploratory = P.
20
+ - Each letter MUST have a specific evidence citation from the data. If insufficient data, write "Insufficient data to estimate" instead of guessing.
21
+ - DISC type must also cite specific behavioral evidence.
22
+
14
23
  ## Output Format
15
24
 
16
25
  # {Full Name} โ€” Profile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vipcare",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
5
  "type": "module",
6
6
  "bin": {