heyiam 0.3.6 → 0.3.9

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.
Files changed (92) hide show
  1. package/dist/archive.js +28 -0
  2. package/dist/db.js +9 -0
  3. package/dist/export.js +3 -0
  4. package/dist/llm/project-enhance.js +11 -5
  5. package/dist/mount.js +43 -18
  6. package/dist/public/assets/index-ByoBtx7P.css +1 -0
  7. package/dist/public/assets/index-LQHTU1Wz.js +37 -0
  8. package/dist/public/index.html +2 -2
  9. package/dist/render/build-render-data.js +1 -0
  10. package/dist/render/liquid.js +14 -1
  11. package/dist/render/mock-data.js +6 -0
  12. package/dist/render/select-profile-skills.js +54 -0
  13. package/dist/render/templates/aurora/portfolio.liquid +4 -4
  14. package/dist/render/templates/aurora/styles.css +6 -6
  15. package/dist/render/templates/bauhaus/portfolio.liquid +4 -4
  16. package/dist/render/templates/bauhaus/project.liquid +2 -2
  17. package/dist/render/templates/bauhaus/styles.css +1 -1
  18. package/dist/render/templates/blueprint/portfolio.liquid +4 -4
  19. package/dist/render/templates/blueprint/styles.css +1 -1
  20. package/dist/render/templates/canvas/portfolio.liquid +4 -4
  21. package/dist/render/templates/canvas/styles.css +2 -2
  22. package/dist/render/templates/carbon/portfolio.liquid +4 -4
  23. package/dist/render/templates/carbon/styles.css +7 -7
  24. package/dist/render/templates/chalk/portfolio.liquid +4 -4
  25. package/dist/render/templates/chalk/styles.css +44 -2
  26. package/dist/render/templates/circuit/portfolio.liquid +4 -4
  27. package/dist/render/templates/cosmos/portfolio.liquid +4 -4
  28. package/dist/render/templates/cosmos/styles.css +4 -4
  29. package/dist/render/templates/daylight/portfolio.liquid +4 -4
  30. package/dist/render/templates/editorial/portfolio.liquid +4 -4
  31. package/dist/render/templates/editorial/project.liquid +1 -1
  32. package/dist/render/templates/editorial/styles.css +6 -1
  33. package/dist/render/templates/ember/portfolio.liquid +4 -4
  34. package/dist/render/templates/ember/styles.css +2 -2
  35. package/dist/render/templates/glacier/portfolio.liquid +4 -4
  36. package/dist/render/templates/glacier/project.liquid +3 -3
  37. package/dist/render/templates/glacier/styles.css +2 -2
  38. package/dist/render/templates/grid/portfolio.liquid +4 -4
  39. package/dist/render/templates/grid/styles.css +2 -2
  40. package/dist/render/templates/kinetic/portfolio.liquid +4 -4
  41. package/dist/render/templates/kinetic/project.liquid +1 -1
  42. package/dist/render/templates/kinetic/styles.css +5 -5
  43. package/dist/render/templates/meridian/portfolio.liquid +4 -4
  44. package/dist/render/templates/meridian/styles.css +1 -1
  45. package/dist/render/templates/minimal/portfolio.liquid +3 -3
  46. package/dist/render/templates/minimal/project.liquid +1 -1
  47. package/dist/render/templates/mono/portfolio.liquid +4 -4
  48. package/dist/render/templates/mono/styles.css +15 -1
  49. package/dist/render/templates/neon/portfolio.liquid +4 -4
  50. package/dist/render/templates/neon/styles.css +11 -20
  51. package/dist/render/templates/noir/portfolio.liquid +4 -4
  52. package/dist/render/templates/noir/styles.css +5 -5
  53. package/dist/render/templates/obsidian/portfolio.liquid +4 -4
  54. package/dist/render/templates/obsidian/styles.css +2 -2
  55. package/dist/render/templates/paper/portfolio.liquid +4 -4
  56. package/dist/render/templates/paper/project.liquid +2 -2
  57. package/dist/render/templates/paper/styles.css +60 -1
  58. package/dist/render/templates/parallax/portfolio.liquid +4 -4
  59. package/dist/render/templates/parallax/styles.css +1 -1
  60. package/dist/render/templates/parchment/portfolio.liquid +4 -4
  61. package/dist/render/templates/parchment/styles.css +3 -3
  62. package/dist/render/templates/partials/_work-timeline.liquid +1 -1
  63. package/dist/render/templates/project.liquid +1 -1
  64. package/dist/render/templates/radar/portfolio.liquid +4 -4
  65. package/dist/render/templates/radar/project.liquid +1 -1
  66. package/dist/render/templates/radar/styles.css +4 -4
  67. package/dist/render/templates/showcase/portfolio.liquid +4 -4
  68. package/dist/render/templates/showcase/project.liquid +1 -1
  69. package/dist/render/templates/showcase/styles.css +7 -7
  70. package/dist/render/templates/signal/portfolio.liquid +4 -4
  71. package/dist/render/templates/signal/styles.css +5 -5
  72. package/dist/render/templates/strata/portfolio.liquid +4 -4
  73. package/dist/render/templates/strata/styles.css +3 -3
  74. package/dist/render/templates/styles.css +39 -28
  75. package/dist/render/templates/terminal/portfolio.liquid +2 -2
  76. package/dist/render/templates/terminal/project.liquid +1 -1
  77. package/dist/render/templates/verdant/portfolio.liquid +4 -4
  78. package/dist/render/templates/verdant/styles.css +17 -2
  79. package/dist/render/templates/zen/portfolio.liquid +4 -4
  80. package/dist/render/templates/zen/styles.css +8 -8
  81. package/dist/routes/context.js +26 -3
  82. package/dist/routes/enhance.js +10 -3
  83. package/dist/routes/export.js +1 -0
  84. package/dist/routes/github.js +1 -1
  85. package/dist/routes/portfolio-render-data.js +15 -1
  86. package/dist/routes/preview.js +31 -1
  87. package/dist/routes/projects.js +8 -1
  88. package/dist/routes/publish.js +9 -2
  89. package/dist/settings.js +2 -0
  90. package/package.json +1 -1
  91. package/dist/public/assets/index-7mUuxgqY.js +0 -37
  92. package/dist/public/assets/index-CMyamplX.css +0 -1
@@ -30,6 +30,17 @@
30
30
  --font-body: 'Inter', sans-serif;
31
31
  --font-mono: 'IBM Plex Mono', monospace;
32
32
 
33
+ /* Type scale — xs is the WCAG-driven readable floor for labels/meta.
34
+ Templates may override individual sizes for identity, but should
35
+ reference these tokens before introducing new literals. */
36
+ --font-size-xs: 0.75rem; /* 12px — labels, meta, caption */
37
+ --font-size-sm: 0.875rem; /* 14px — small body, dense rows */
38
+ --font-size-base: 1rem; /* 16px — body */
39
+ --font-size-lg: 1.125rem; /* 18px — subhead */
40
+ --font-size-xl: 1.25rem; /* 20px — heading */
41
+ --font-size-2xl: 1.5rem; /* 24px — section heading */
42
+ --font-size-3xl: 2rem; /* 32px — page title */
43
+
33
44
  --radius-sm: 0.25rem;
34
45
  --radius-md: 0.375rem;
35
46
  }
@@ -107,13 +118,13 @@ a:hover { text-decoration: underline; }
107
118
  }
108
119
  .section-header__title {
109
120
  font-family: var(--font-display);
110
- font-size: 1rem;
121
+ font-size: var(--font-size-base);
111
122
  font-weight: 600;
112
123
  color: var(--on-surface);
113
124
  }
114
125
  .section-header__meta {
115
126
  font-family: var(--font-mono);
116
- font-size: 9px;
127
+ font-size: var(--font-size-xs);
117
128
  text-transform: uppercase;
118
129
  letter-spacing: 0.05em;
119
130
  color: var(--on-surface-variant);
@@ -145,7 +156,7 @@ a:hover { text-decoration: underline; }
145
156
  }
146
157
 
147
158
  .stat-card__value--sm {
148
- font-size: 1.25rem;
159
+ font-size: var(--font-size-xl);
149
160
  }
150
161
 
151
162
  .stat-grid {
@@ -162,7 +173,7 @@ a:hover { text-decoration: underline; }
162
173
  }
163
174
  .stat-card__label {
164
175
  font-family: var(--font-mono);
165
- font-size: 9px;
176
+ font-size: var(--font-size-xs);
166
177
  text-transform: uppercase;
167
178
  letter-spacing: 0.05em;
168
179
  color: var(--on-surface-variant);
@@ -170,7 +181,7 @@ a:hover { text-decoration: underline; }
170
181
  }
171
182
  .stat-card__value {
172
183
  font-family: var(--font-display);
173
- font-size: 1.5rem;
184
+ font-size: var(--font-size-2xl);
174
185
  font-weight: 700;
175
186
  color: var(--on-surface);
176
187
  }
@@ -184,7 +195,7 @@ a:hover { text-decoration: underline; }
184
195
  font-family: var(--font-mono);
185
196
  font-size: 11px;
186
197
  line-height: 1.2;
187
- padding: 0.125rem 0.5rem;
198
+ padding: 0.25rem 0.5rem;
188
199
  border-radius: var(--radius-sm);
189
200
  background: var(--surface-low);
190
201
  color: var(--on-surface-variant);
@@ -210,7 +221,7 @@ a:hover { text-decoration: underline; }
210
221
 
211
222
  .project-title {
212
223
  font-family: var(--font-display);
213
- font-size: 1.25rem;
224
+ font-size: var(--font-size-xl);
214
225
  font-weight: 700;
215
226
  color: var(--on-surface);
216
227
  margin-bottom: 0.25rem;
@@ -224,7 +235,7 @@ a:hover { text-decoration: underline; }
224
235
  }
225
236
  .project-link {
226
237
  font-family: var(--font-mono);
227
- font-size: 0.75rem;
238
+ font-size: var(--font-size-xs);
228
239
  color: var(--primary);
229
240
  display: flex;
230
241
  align-items: center;
@@ -284,7 +295,7 @@ a:hover { text-decoration: underline; }
284
295
  text-align: left;
285
296
  padding: 0.5rem 0;
286
297
  font-family: var(--font-mono);
287
- font-size: 9px;
298
+ font-size: var(--font-size-xs);
288
299
  text-transform: uppercase;
289
300
  letter-spacing: 0.05em;
290
301
  color: var(--outline);
@@ -335,7 +346,7 @@ a:hover { text-decoration: underline; }
335
346
  }
336
347
  .session-card__meta {
337
348
  color: var(--on-surface-variant);
338
- font-size: 0.75rem;
349
+ font-size: var(--font-size-xs);
339
350
  }
340
351
  .session-card__skills { margin-top: 0.5rem; }
341
352
 
@@ -348,13 +359,13 @@ a:hover { text-decoration: underline; }
348
359
  }
349
360
  .note__title {
350
361
  font-family: var(--font-body);
351
- font-size: 0.875rem;
362
+ font-size: var(--font-size-sm);
352
363
  font-weight: 600;
353
364
  color: var(--on-surface);
354
365
  margin-bottom: 0.25rem;
355
366
  }
356
367
  .note__body {
357
- font-size: 0.875rem;
368
+ font-size: var(--font-size-sm);
358
369
  color: var(--on-surface-variant);
359
370
  line-height: 1.5;
360
371
  }
@@ -395,7 +406,7 @@ a:hover { text-decoration: underline; }
395
406
  }
396
407
  .phase-timeline__label {
397
408
  font-family: var(--font-mono);
398
- font-size: 0.6875rem;
409
+ font-size: var(--font-size-xs);
399
410
  text-transform: uppercase;
400
411
  letter-spacing: 0.05em;
401
412
  color: var(--on-surface-variant);
@@ -410,7 +421,7 @@ a:hover { text-decoration: underline; }
410
421
  }
411
422
  .export-footer__text {
412
423
  font-family: var(--font-mono);
413
- font-size: 10px;
424
+ font-size: var(--font-size-xs);
414
425
  color: var(--on-surface-variant);
415
426
  text-transform: uppercase;
416
427
  letter-spacing: 0.05em;
@@ -438,7 +449,7 @@ a:hover { text-decoration: underline; }
438
449
  .sidebar-section { margin-bottom: 1.5rem; }
439
450
  .sidebar-section__heading {
440
451
  font-family: var(--font-mono);
441
- font-size: 9px;
452
+ font-size: var(--font-size-xs);
442
453
  text-transform: uppercase;
443
454
  letter-spacing: 0.05em;
444
455
  color: var(--outline);
@@ -457,7 +468,7 @@ a:hover { text-decoration: underline; }
457
468
  }
458
469
  .tool-list__name, .file-list__path {
459
470
  font-family: var(--font-mono);
460
- font-size: 0.75rem;
471
+ font-size: var(--font-size-xs);
461
472
  color: var(--on-surface);
462
473
  overflow: hidden;
463
474
  text-overflow: ellipsis;
@@ -465,7 +476,7 @@ a:hover { text-decoration: underline; }
465
476
  }
466
477
  .tool-list__count {
467
478
  font-family: var(--font-mono);
468
- font-size: 0.75rem;
479
+ font-size: var(--font-size-xs);
469
480
  color: var(--on-surface-variant);
470
481
  }
471
482
  .file-list__adds { color: var(--green); font-family: var(--font-mono); font-size: 0.6875rem; }
@@ -476,7 +487,7 @@ a:hover { text-decoration: underline; }
476
487
 
477
488
  .breadcrumb {
478
489
  font-family: var(--font-mono);
479
- font-size: 0.75rem;
490
+ font-size: var(--font-size-xs);
480
491
  color: var(--on-surface-variant);
481
492
  margin-bottom: 1rem;
482
493
  }
@@ -486,7 +497,7 @@ a:hover { text-decoration: underline; }
486
497
  .session-header { margin-bottom: 1.5rem; }
487
498
  .session-header__title {
488
499
  font-family: var(--font-display);
489
- font-size: 1.5rem;
500
+ font-size: var(--font-size-2xl);
490
501
  font-weight: 700;
491
502
  color: var(--on-surface);
492
503
  margin-bottom: 0.5rem;
@@ -500,11 +511,11 @@ a:hover { text-decoration: underline; }
500
511
  }
501
512
  .session-header__date {
502
513
  font-family: var(--font-mono);
503
- font-size: 0.75rem;
514
+ font-size: var(--font-size-xs);
504
515
  }
505
516
  .session-header__source {
506
517
  font-family: var(--font-mono);
507
- font-size: 0.75rem;
518
+ font-size: var(--font-size-xs);
508
519
  background: var(--surface-low);
509
520
  padding: 0.125rem 0.5rem;
510
521
  border-radius: var(--radius-sm);
@@ -521,7 +532,7 @@ a:hover { text-decoration: underline; }
521
532
  .session-header__devtake p { margin: 0; }
522
533
  .session-header__context {
523
534
  color: var(--on-surface-variant);
524
- font-size: 0.875rem;
535
+ font-size: var(--font-size-sm);
525
536
  margin: 0.75rem 0;
526
537
  }
527
538
 
@@ -537,13 +548,13 @@ a:hover { text-decoration: underline; }
537
548
  }
538
549
  .stat__value {
539
550
  font-family: var(--font-display);
540
- font-size: 1.125rem;
551
+ font-size: var(--font-size-lg);
541
552
  font-weight: 700;
542
553
  color: var(--on-surface);
543
554
  }
544
555
  .stat__label {
545
556
  font-family: var(--font-mono);
546
- font-size: 9px;
557
+ font-size: var(--font-size-xs);
547
558
  text-transform: uppercase;
548
559
  letter-spacing: 0.05em;
549
560
  color: var(--on-surface-variant);
@@ -560,7 +571,7 @@ a:hover { text-decoration: underline; }
560
571
  }
561
572
  .content-section__heading {
562
573
  font-family: var(--font-display);
563
- font-size: 1rem;
574
+ font-size: var(--font-size-base);
564
575
  font-weight: 600;
565
576
  color: var(--on-surface);
566
577
  margin-bottom: 0.75rem;
@@ -591,7 +602,7 @@ a:hover { text-decoration: underline; }
591
602
  .beat:last-child { border-bottom: none; }
592
603
  .beat__number {
593
604
  font-family: var(--font-mono);
594
- font-size: 0.75rem;
605
+ font-size: var(--font-size-xs);
595
606
  color: var(--primary);
596
607
  font-weight: 600;
597
608
  min-width: 1.5rem;
@@ -601,7 +612,7 @@ a:hover { text-decoration: underline; }
601
612
  .beat__content { flex: 1; min-width: 0; }
602
613
  .beat__title {
603
614
  font-family: var(--font-display);
604
- font-size: 0.875rem;
615
+ font-size: var(--font-size-sm);
605
616
  font-weight: 600;
606
617
  color: var(--on-surface);
607
618
  }
@@ -627,7 +638,7 @@ a:hover { text-decoration: underline; }
627
638
  }
628
639
  .qa-pair__answer {
629
640
  color: var(--on-surface-variant);
630
- font-size: 0.875rem;
641
+ font-size: var(--font-size-sm);
631
642
  line-height: 1.5;
632
643
  }
633
644
 
@@ -82,9 +82,9 @@
82
82
  <span class="term-sep">&middot;</span>
83
83
  <span>{{ project.totalFilesChanged }} files</span>
84
84
  </div>
85
- {% if project.skills.size > 0 %}
85
+ {% if project.profileSkills.size > 0 %}
86
86
  <div class="term-project-entry__skills">
87
- {% for skill in project.skills %}
87
+ {% for skill in project.profileSkills %}
88
88
  <span class="term-skill-tag">{{ skill }}</span>
89
89
  {% endfor %}
90
90
  </div>
@@ -69,7 +69,7 @@
69
69
  {% if sessionsJson %}
70
70
  <div class="term-section">
71
71
  <div class="term-comment"># work timeline</div>
72
- <div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
72
+ <div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
73
73
  </div>
74
74
  {% endif %}
75
75
 
@@ -142,8 +142,8 @@
142
142
  <h3>{{ p.title }}</h3>
143
143
  <span class="vd-project-card__arrow" aria-hidden="true">&rarr;</span>
144
144
  </div>
145
- {% if p.narrative != blank %}
146
- <p class="vd-project-card__narrative">{{ p.narrative }}</p>
145
+ {% if p.tagline != blank %}
146
+ <p class="vd-project-card__narrative">{{ p.tagline }}</p>
147
147
  {% endif %}
148
148
  <div class="vd-project-card__meta">
149
149
  <span>{{ p.totalSessions }} sessions</span>
@@ -152,9 +152,9 @@
152
152
  <span>&middot;</span>
153
153
  <span>{{ p.totalLoc | formatLoc }} LOC</span>
154
154
  </div>
155
- {% if p.skills.size > 0 %}
155
+ {% if p.profileSkills.size > 0 %}
156
156
  <div class="vd-skills" aria-label="Skills used">
157
- {% for skill in p.skills %}
157
+ {% for skill in p.profileSkills %}
158
158
  <span class="vd-skill-chip">{{ skill }}</span>
159
159
  {% endfor %}
160
160
  </div>
@@ -294,7 +294,7 @@
294
294
  /* Project page uses smaller stat cards */
295
295
  .verdant.heyiam-project .vd-stat-card { padding: 1rem; }
296
296
  .verdant.heyiam-project .vd-stat-value { font-size: 1.5rem; }
297
- .verdant.heyiam-project .vd-stat-label { font-size: 0.6875rem; }
297
+ .verdant.heyiam-project .vd-stat-label { font-size: 0.75rem; }
298
298
 
299
299
  /* ── Section Headers ── */
300
300
  .verdant .vd-section-header {
@@ -482,7 +482,7 @@
482
482
 
483
483
  /* ── Narrative ── */
484
484
  .verdant .vd-narrative {
485
- font-size: 0.9375rem;
485
+ font-size: 1rem;
486
486
  line-height: 1.8;
487
487
  color: var(--vd-on-surface-2);
488
488
  }
@@ -1256,6 +1256,21 @@
1256
1256
  .verdant .vd-footer { flex-direction: column; gap: 0.5rem; text-align: center; }
1257
1257
  }
1258
1258
 
1259
+ @media (max-width: 480px) {
1260
+ .verdant .vd-page { padding: 1rem 0.75rem 2rem; }
1261
+ .verdant .vd-hero h1 { font-size: 1.375rem; }
1262
+ .verdant .vd-hero__avatar { width: 64px; height: 64px; font-size: 1.375rem; }
1263
+ .verdant .vd-hero__photo { width: 80px; height: 104px; }
1264
+ .verdant .vd-stats,
1265
+ .verdant .vd-stat-grid { grid-template-columns: 1fr; }
1266
+ .verdant .vd-sidebar { grid-template-columns: 1fr; }
1267
+ .verdant .vd-project-header h1 { font-size: 1.375rem; }
1268
+ .verdant .vd-session-header h1 { font-size: 1.25rem; }
1269
+ .verdant .vd-vine-timeline,
1270
+ .verdant .vd-beats { padding-left: 1.25rem; }
1271
+ .verdant .vd-nav__links { flex-wrap: wrap; gap: 0.75rem; }
1272
+ }
1273
+
1259
1274
 
1260
1275
  /* Live-edit empty field hiding */
1261
1276
  [data-portfolio-empty="true"] { display: none; }
@@ -71,15 +71,15 @@
71
71
  <h3 class="zen-project-title">
72
72
  <a href="/{{ user.username }}/{{ p.slug }}">{{ p.title }}</a>
73
73
  </h3>
74
- {% if p.narrative != blank %}
75
- <p class="zen-project-narrative">{{ p.narrative }}</p>
74
+ {% if p.tagline != blank %}
75
+ <p class="zen-project-narrative">{{ p.tagline }}</p>
76
76
  {% endif %}
77
77
  <p class="zen-project-meta">
78
78
  {{ p.totalSessions }} sessions &middot; {{ p.totalDurationMinutes | formatDuration }} &middot; {{ p.totalLoc | localeNumber }} lines changed
79
79
  </p>
80
- {% if p.skills.size > 0 %}
80
+ {% if p.profileSkills.size > 0 %}
81
81
  <p class="zen-project-skills">
82
- {{ p.skills | join: ", " }}
82
+ {{ p.profileSkills | join: ", " }}
83
83
  </p>
84
84
  {% endif %}
85
85
  {% if p.sourceCounts.size > 0 %}
@@ -276,7 +276,7 @@ a:active {
276
276
 
277
277
  .zen-body {
278
278
  font-family: var(--zen-font-body);
279
- font-size: 0.9375rem;
279
+ font-size: 1rem;
280
280
  line-height: 1.8;
281
281
  color: var(--zen-text-secondary);
282
282
  }
@@ -342,7 +342,7 @@ a:active {
342
342
 
343
343
  .zen-header-bio {
344
344
  font-family: var(--zen-font-body);
345
- font-size: 0.9375rem;
345
+ font-size: 1rem;
346
346
  line-height: 1.8;
347
347
  color: var(--zen-text-secondary);
348
348
  margin-block-end: 1rem;
@@ -500,7 +500,7 @@ a:active {
500
500
 
501
501
  .zen-project-narrative {
502
502
  font-family: var(--zen-font-body);
503
- font-size: 0.9375rem;
503
+ font-size: 1rem;
504
504
  line-height: 1.8;
505
505
  color: var(--zen-text-secondary);
506
506
  margin-block-end: 1.25rem;
@@ -591,7 +591,7 @@ a:active {
591
591
  /* ── Narrative ── */
592
592
  .zen-narrative p {
593
593
  font-family: var(--zen-font-body);
594
- font-size: 0.9375rem;
594
+ font-size: 1rem;
595
595
  line-height: 1.9;
596
596
  color: var(--zen-text-secondary);
597
597
  margin-block-end: 1.5rem;
@@ -703,7 +703,7 @@ a:active {
703
703
 
704
704
  .zen-decision-text {
705
705
  font-family: var(--zen-font-body);
706
- font-size: 0.9375rem;
706
+ font-size: 1rem;
707
707
  line-height: 1.7;
708
708
  color: var(--zen-text-secondary);
709
709
  }
@@ -837,7 +837,7 @@ a:active {
837
837
 
838
838
  .zen-beat-desc {
839
839
  font-family: var(--zen-font-body);
840
- font-size: 0.9375rem;
840
+ font-size: 1rem;
841
841
  line-height: 1.85;
842
842
  color: var(--zen-text-secondary);
843
843
  }
@@ -861,7 +861,7 @@ a:active {
861
861
 
862
862
  .zen-question {
863
863
  font-family: var(--zen-font-body);
864
- font-size: 0.9375rem;
864
+ font-size: 1rem;
865
865
  font-weight: 500;
866
866
  line-height: 1.7;
867
867
  color: var(--zen-text);
@@ -870,7 +870,7 @@ a:active {
870
870
 
871
871
  .zen-answer {
872
872
  font-family: var(--zen-font-body);
873
- font-size: 0.9375rem;
873
+ font-size: 1rem;
874
874
  line-height: 1.85;
875
875
  color: var(--zen-text-secondary);
876
876
  }
@@ -9,8 +9,8 @@ import { bridgeToAnalyzer, mergeActiveIntervals, sumIntervalMs } from '../bridge
9
9
  import { analyzeSession } from '../analyzer.js';
10
10
  import { loadEnhancedData, loadProjectEnhanceResult, getUploadedState, } from '../settings.js';
11
11
  import { getTemplateCss } from '../render/templates.js';
12
- import { archiveSessionFiles } from '../archive.js';
13
- import { getDatabase, openDatabase, getSessionStats as dbGetSessionStats, getSessionCount, getAllSessionMetas, getAllProjectStats, getSessionsByProject, getProjectUuid, } from '../db.js';
12
+ import { archiveSessionFiles, findReadableSessionPath } from '../archive.js';
13
+ import { getDatabase, openDatabase, getSessionStats as dbGetSessionStats, getSessionCount, getAllSessionMetas, getAllProjectStats, getSessionsByProject, getProjectUuid, getSessionRow, updateSessionPath, } from '../db.js';
14
14
  import { ensureSessionIndexed, displayNameFromDir } from '../sync.js';
15
15
  export { displayNameFromDir };
16
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -298,11 +298,34 @@ export function createRouteContext(sessionsBasePath, dbPath) {
298
298
  };
299
299
  }
300
300
  async function loadSession(sessionPath, projectName, sessionId) {
301
- const parsed = await parseSession(sessionPath);
301
+ const readablePath = await resolveSessionReadPath(sessionPath, sessionId);
302
+ const parsed = await parseSession(readablePath);
302
303
  const analyzerInput = bridgeToAnalyzer(parsed, { sessionId, projectName });
303
304
  const session = analyzeSession(analyzerInput);
304
305
  return mergeEnhancedData(session);
305
306
  }
307
+ /**
308
+ * Returns a path to a readable session file. If the originally-indexed
309
+ * path still exists, returns it unchanged. Otherwise falls back to the
310
+ * archive copy and heals the DB cache so future reads skip the lookup.
311
+ *
312
+ * This compensates for source tools (notably Claude Code's 30-day
313
+ * cleanup) deleting session files after we've indexed them.
314
+ */
315
+ async function resolveSessionReadPath(originalPath, sessionId) {
316
+ const row = getSessionRow(db, sessionId);
317
+ const projectDir = row?.project_dir;
318
+ if (!projectDir)
319
+ return originalPath;
320
+ const readable = await findReadableSessionPath(originalPath, projectDir);
321
+ if (!readable)
322
+ return originalPath;
323
+ if (readable !== originalPath) {
324
+ updateSessionPath(db, sessionId, readable);
325
+ console.log(`[loadSession] Healed stale path for ${sessionId} → archive copy`);
326
+ }
327
+ return readable;
328
+ }
306
329
  // ── getSessionStats ──────────────────────────────────────
307
330
  async function getSessionStats(meta, projectName) {
308
331
  try {
@@ -5,7 +5,7 @@ import { enhanceProject, refineNarrative } from '../llm/project-enhance.js';
5
5
  import { getAnthropicApiKey, saveEnhancedData, loadEnhancedData, deleteEnhancedData, loadFreshProjectEnhanceResult, saveProjectEnhanceResult, loadProjectEnhanceResult, buildProjectFingerprint, getUploadedState, } from '../settings.js';
6
6
  import { requireProject } from './context.js';
7
7
  import { startSSE } from './sse.js';
8
- import { invalidatePortfolioPreviewCache } from './preview.js';
8
+ import { invalidatePortfolioPreviewCache, invalidateProjectPreviewCache } from './preview.js';
9
9
  export function createEnhanceRouter(ctx) {
10
10
  const router = Router();
11
11
  // Triage endpoint -- AI selects which sessions are worth showcasing (SSE stream)
@@ -259,7 +259,11 @@ export function createEnhanceRouter(ctx) {
259
259
  // Save project enhance result explicitly
260
260
  router.post('/api/projects/:project/enhance-save', async (req, res) => {
261
261
  const project = String(req.params.project);
262
- const { selectedSessionIds, result, title, repoUrl, projectUrl, screenshotBase64 } = req.body;
262
+ // FIXME(security): screenshotBase64 is accepted with no size cap and no
263
+ // `data:image/(png|jpeg|jpg|webp);base64,...` shape check. Local-only CLI
264
+ // so not a privilege issue today, but worth a regex + ~4 MB cap for
265
+ // defense-in-depth and to keep the cache JSON from ballooning.
266
+ const { selectedSessionIds, result, title, repoUrl, projectUrl, screenshotBase64, hideSessionDates } = req.body;
263
267
  if (!Array.isArray(selectedSessionIds) || !result?.narrative) {
264
268
  res.status(400).json({ error: { code: 'INVALID_INPUT', message: 'selectedSessionIds and result are required' } });
265
269
  return;
@@ -272,9 +276,12 @@ export function createEnhanceRouter(ctx) {
272
276
  const proj = await requireProject(ctx, project, res);
273
277
  if (!proj)
274
278
  return;
275
- saveProjectEnhanceResult(proj.dirName, selectedSessionIds, result, undefined, { title, repoUrl, projectUrl, screenshotBase64 });
279
+ saveProjectEnhanceResult(proj.dirName, selectedSessionIds, result, undefined, { title, repoUrl, projectUrl, screenshotBase64, hideSessionDates });
276
280
  // Project title/narrative/skills appear in portfolio listing — bust cache.
277
281
  invalidatePortfolioPreviewCache();
282
+ // Per-project render cache is keyed by the URL param the client passed.
283
+ invalidateProjectPreviewCache(project);
284
+ invalidateProjectPreviewCache(proj.dirName);
278
285
  res.json({ saved: true, enhancedAt: new Date().toISOString() });
279
286
  }
280
287
  catch (err) {
@@ -52,6 +52,7 @@ function buildFallbackCache(sessions) {
52
52
  enhancedAt: new Date().toISOString(),
53
53
  selectedSessionIds: allIds,
54
54
  result: {
55
+ tagline: '',
55
56
  narrative: '',
56
57
  arc: [],
57
58
  skills: allSkills,
@@ -188,7 +188,7 @@ export function createGithubRouter(ctx) {
188
188
  fingerprint: 'gh-publish',
189
189
  enhancedAt: new Date().toISOString(),
190
190
  selectedSessionIds: detail.sessions.map((s) => s.id),
191
- result: { narrative: '', arc: [], skills: [], timeline: [], questions: [] },
191
+ result: { tagline: '', narrative: '', arc: [], skills: [], timeline: [], questions: [] },
192
192
  };
193
193
  const proj = detail.project;
194
194
  projectInputs.push({
@@ -2,6 +2,7 @@ import { getPortfolioProfile, loadProjectEnhanceResult } from '../settings.js';
2
2
  import { getSessionsByProject, getAllProjectStats } from '../db.js';
3
3
  import { displayNameFromDir } from '../sync.js';
4
4
  import { toSlug } from '../format-utils.js';
5
+ import { selectProfileSkills } from '../render/select-profile-skills.js';
5
6
  /**
6
7
  * Assemble the `PortfolioRenderData` payload from local project data.
7
8
  *
@@ -54,16 +55,29 @@ export async function buildPortfolioRenderData(ctx, auth, opts = {}) {
54
55
  loc: (s.loc_added || 0) + (s.loc_removed || 0),
55
56
  durationMinutes: s.duration_minutes || 0,
56
57
  }));
58
+ const projectSkills = cached?.result?.skills || proj.skills || [];
59
+ const sessionSkills = dbSessions
60
+ .filter((s) => !s.is_subagent && s.skills)
61
+ .map((s) => {
62
+ try {
63
+ return JSON.parse(s.skills);
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ });
57
69
  portfolioProjects.push({
58
70
  slug: toSlug(title),
59
71
  title,
72
+ tagline: cached?.result?.tagline || '',
60
73
  narrative: cached?.result?.narrative || proj.description || '',
61
74
  totalSessions: projSessions,
62
75
  totalLoc: projLoc,
63
76
  totalDurationMinutes: projDuration,
64
77
  totalAgentDurationMinutes: projAgentDuration,
65
78
  totalFilesChanged: proj.totalFiles || 0,
66
- skills: cached?.result?.skills || proj.skills || [],
79
+ skills: projectSkills,
80
+ profileSkills: selectProfileSkills({ projectSkills, sessionSkills }),
67
81
  publishedCount: 0,
68
82
  sessions: sessionActivity,
69
83
  });
@@ -14,6 +14,7 @@ import { displayNameFromDir } from '../sync.js';
14
14
  import { toSlug } from '../format-utils.js';
15
15
  import { getSessionsByProject, getAllProjectStats } from '../db.js';
16
16
  import { applyPortfolioProjectFilter } from './portfolio-render-data.js';
17
+ import { selectProfileSkills } from '../render/select-profile-skills.js';
17
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
19
  /**
19
20
  * In-memory cache for expensive buildProjectPreviewData calls.
@@ -53,6 +54,15 @@ function portfolioCacheKey(templateName) {
53
54
  export function invalidatePortfolioPreviewCache() {
54
55
  portfolioPreviewCache.clear();
55
56
  }
57
+ /**
58
+ * Drop the per-project render-data cache for `projectParam`. Call after any
59
+ * mutation that affects the rendered project page (title, URLs, narrative,
60
+ * screenshot, skills, timeline). Without this, the 30s TTL on
61
+ * previewDataCache masks the change in the React UI.
62
+ */
63
+ export function invalidateProjectPreviewCache(projectParam) {
64
+ previewDataCache.delete(projectParam);
65
+ }
56
66
  /** Test helper: read current cache entry without mutating. */
57
67
  export function _getPortfolioPreviewCacheEntry(templateName = 'editorial') {
58
68
  return portfolioPreviewCache.get(portfolioCacheKey(templateName));
@@ -176,6 +186,12 @@ async function buildProjectPreviewData(ctx, projectParam, queryOverrides) {
176
186
  repoUrl: metaRepoUrl,
177
187
  projectUrl: metaProjectUrl,
178
188
  screenshotUrl: (() => {
189
+ // Manual uploads land only in the enhance-cache JSON (no disk write),
190
+ // so check the cache first — mirrors export.ts:resolveScreenshotDataUri.
191
+ const b64 = cachedAny?.screenshotBase64;
192
+ if (b64) {
193
+ return b64.startsWith('data:') ? b64 : `data:image/png;base64,${b64}`;
194
+ }
179
195
  return existsSync(path.join(SCREENSHOTS_DIR, `${slug}.png`))
180
196
  ? `/screenshots/${slug}.png`
181
197
  : undefined;
@@ -192,6 +208,7 @@ async function buildProjectPreviewData(ctx, projectParam, queryOverrides) {
192
208
  allSessionCards,
193
209
  sessionBaseUrl: `/preview/project/${encodeURIComponent(projectParam)}/session`,
194
210
  sessionSuffix: '.html',
211
+ hideSessionDates: cachedAny?.hideSessionDates,
195
212
  });
196
213
  const result = { renderData, enhanceResult, projName: projAny.name };
197
214
  // Cache the result (template-agnostic data, re-rendered cheaply per template)
@@ -587,16 +604,29 @@ body { overflow: auto !important; min-height: auto !important; }
587
604
  loc: (s.loc_added || 0) + (s.loc_removed || 0),
588
605
  durationMinutes: s.duration_minutes || 0,
589
606
  }));
607
+ const projectSkills = cached?.result?.skills || proj.skills || [];
608
+ const sessionSkills = dbSessions
609
+ .filter(s => !s.is_subagent && s.skills)
610
+ .map(s => {
611
+ try {
612
+ return JSON.parse(s.skills);
613
+ }
614
+ catch {
615
+ return [];
616
+ }
617
+ });
590
618
  portfolioProjects.push({
591
619
  slug: toSlug(title),
592
620
  title,
621
+ tagline: cached?.result?.tagline || '',
593
622
  narrative: cached?.result?.narrative || proj.description || '',
594
623
  totalSessions: projSessions,
595
624
  totalLoc: projLoc,
596
625
  totalDurationMinutes: projDuration,
597
626
  totalAgentDurationMinutes: projAgentDuration,
598
627
  totalFilesChanged: proj.totalFiles || 0,
599
- skills: cached?.result?.skills || proj.skills || [],
628
+ skills: projectSkills,
629
+ profileSkills: selectProfileSkills({ projectSkills, sessionSkills }),
600
630
  publishedCount: 0,
601
631
  sessions: sessionActivity,
602
632
  });
@@ -217,7 +217,14 @@ export function createProjectsRouter(ctx) {
217
217
  res.status(400).json({ error: { code: 'NO_CACHE', message: 'Project must be enhanced before managing sessions' } });
218
218
  return;
219
219
  }
220
- saveProjectEnhanceResult(proj.dirName, selectedSessionIds, cache.result, undefined, { title: cache.title, repoUrl: cache.repoUrl, projectUrl: cache.projectUrl, screenshotBase64: cache.screenshotBase64 });
220
+ saveProjectEnhanceResult(proj.dirName, selectedSessionIds, cache.result, undefined, {
221
+ title: cache.title,
222
+ repoUrl: cache.repoUrl,
223
+ projectUrl: cache.projectUrl,
224
+ screenshotBase64: cache.screenshotBase64,
225
+ template: cache.template,
226
+ hideSessionDates: cache.hideSessionDates,
227
+ });
221
228
  invalidatePortfolioPreviewCache();
222
229
  res.json({ ok: true, selectedSessionIds });
223
230
  }