careermate 0.0.9 → 0.0.12

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 (52) hide show
  1. package/AGENTS.md +3 -2
  2. package/INSTALL.md +6 -6
  3. package/LICENSE +1 -1
  4. package/README.md +18 -16
  5. package/dist/career-os/README.md +2 -2
  6. package/dist/doctor.mjs +17857 -6595
  7. package/dist/mcp.mjs +14545 -3839
  8. package/dist/migrate.mjs +14258 -3709
  9. package/dist/pdf.worker.min.mjs +4 -4
  10. package/dist/public/app.js +3 -18
  11. package/dist/public/i18n/en/documents.js +1 -0
  12. package/dist/public/i18n/en/home.js +8 -1
  13. package/dist/public/i18n/en/jobs.js +2 -0
  14. package/dist/public/i18n/en/nav.js +1 -0
  15. package/dist/public/i18n/en/settings.js +1 -6
  16. package/dist/public/i18n/ko/documents.js +1 -0
  17. package/dist/public/i18n/ko/home.js +8 -1
  18. package/dist/public/i18n/ko/jobs.js +2 -0
  19. package/dist/public/i18n/ko/nav.js +1 -0
  20. package/dist/public/i18n/ko/settings.js +1 -6
  21. package/dist/public/index.html +0 -1
  22. package/dist/public/lib.js +13 -4
  23. package/dist/public/pages/documents.js +27 -10
  24. package/dist/public/pages/home.js +15 -5
  25. package/dist/public/pages/insights.js +156 -0
  26. package/dist/public/pages/jobs.js +21 -3
  27. package/dist/public/pages/settings.js +13 -12
  28. package/dist/public/styles.css +33 -0
  29. package/dist/web.mjs +16890 -5492
  30. package/docs/FAQ.md +3 -4
  31. package/docs/LOCAL_FIRST.md +32 -0
  32. package/docs/MCP_TOOLS.md +6 -3
  33. package/docs/REVIEW_IMPLEMENTATION_PLAN.md +22 -0
  34. package/docs/career-os/README.md +2 -2
  35. package/package.json +7 -12
  36. package/packages/core/src/data-health.ts +161 -0
  37. package/packages/core/src/fit-explain.ts +63 -0
  38. package/packages/core/src/index.ts +4 -0
  39. package/packages/core/src/job-categories.data.ts +140 -0
  40. package/packages/core/src/job-categories.ts +106 -0
  41. package/packages/core/src/jobsearch.ts +374 -66
  42. package/packages/core/src/reuse-insights.ts +126 -0
  43. package/packages/db/src/connection.ts +46 -26
  44. package/packages/db/src/index.ts +3 -0
  45. package/packages/db/src/interview-retrospectives.ts +145 -0
  46. package/packages/db/src/job-search.ts +100 -0
  47. package/packages/db/src/provenance.ts +125 -0
  48. package/packages/db/src/repositories.ts +1 -0
  49. package/packages/mcp-tools/src/tools.ts +115 -11
  50. package/packages/prompts/src/system.ts +21 -64
  51. package/scripts/init.ts +2 -0
  52. package/tsconfig.json +9 -10
@@ -314,11 +314,11 @@ async function renderDetail(ctx, jobId) {
314
314
  ]);
315
315
 
316
316
  // --- Page head ------------------------------------------------------------
317
- // 수정은 상단바(ctx.setActions)에만 둔다 — 여기엔 상태 변경·원문·삭제만.
317
+ // 수정은 상단바(ctx.setActions)에만 둔다 — 여기엔 상태 변경·원문만.
318
+ // 삭제는 위험 액션이라 페이지 최하단 '위험 구역'으로 강등한다(아래 DangerZone).
318
319
  const headActions = el('div', { class: 'flex gap-2 center wrap' },
319
320
  statusSelect,
320
321
  job.url ? el('a', { class: 'btn btn--ghost btn--sm', href: job.url, attrs: { target: '_blank', rel: 'noopener noreferrer' } }, icon('external'), el('span', {}, t('jobs.detail.openPosting'))) : null,
321
- delBtn,
322
322
  );
323
323
 
324
324
  const head = el('div', { class: 'page-head' },
@@ -344,10 +344,23 @@ async function renderDetail(ctx, jobId) {
344
344
  FitCard(job.fit),
345
345
  TimelineCard(job.timeline),
346
346
  RelatedCard(job.related),
347
+ DangerZone(delBtn),
347
348
  );
348
349
  mount(ctx.view, wrap);
349
350
  }
350
351
 
352
+ // Delete is destructive, so it sits at the very bottom in a quiet 위험 구역 row
353
+ // (not a card, per the design guide) instead of competing in the top-right header.
354
+ function DangerZone(delBtn) {
355
+ return el('div', { class: 'stack-2', style: { marginTop: '8px' } },
356
+ el('div', { class: 'divider' }),
357
+ el('div', { class: 'flex between center wrap gap-3' },
358
+ el('div', { style: { maxWidth: '440px' } },
359
+ el('div', { class: 'strong text-sm' }, t('jobs.detail.dangerZone')),
360
+ el('div', { class: 'muted text-sm', style: { lineHeight: '1.6' } }, t('jobs.detail.dangerZoneHint'))),
361
+ delBtn));
362
+ }
363
+
351
364
  /* ----------------------------------------------------------- detail cards */
352
365
 
353
366
  function PostingCard(job) {
@@ -534,7 +547,12 @@ function CoverLettersCard(coverLetters) {
534
547
  icon('file', 'muted'),
535
548
  el('div', {},
536
549
  el('div', { class: 'strong' }, cl.title),
537
- el('div', { class: 'muted text-sm' }, t('jobs.cover.versions', { count: cl.version_count ?? 1 })))),
550
+ // Preview line: version count + last-updated date so the row isn't a bare
551
+ // title. (The latest version's note isn't in this response — listByJob maps
552
+ // without versions — so we fall back to the date per the design.)
553
+ el('div', { class: 'muted text-sm' },
554
+ t('jobs.cover.versions', { count: cl.version_count ?? 1 }),
555
+ cl.updated_at ? ` · ${fmtDate(cl.updated_at)}` : ''))),
538
556
  icon('chevronRight', 'muted')))),
539
557
  });
540
558
  }
@@ -30,12 +30,16 @@ export async function render(ctx) {
30
30
  }
31
31
 
32
32
  /* ----------------------------------------------------- 데이터 저장 위치 */
33
- function kvRow(label, value, copyable) {
33
+ function kvRow(label, value, copyable, { wrapValue = false } = {}) {
34
34
  return [
35
35
  el('dt', {}, label),
36
36
  el('dd', {},
37
37
  el('div', { class: 'flex between gap-3' },
38
- el('span', { class: 'truncate tnum', style: { fontFamily: 'var(--mono, ui-monospace, monospace)' }, title: value }, value || '—'),
38
+ el('span', {
39
+ class: `${wrapValue ? 'kv__value-wrap' : 'truncate'} tnum`,
40
+ style: { fontFamily: 'var(--mono, ui-monospace, monospace)' },
41
+ title: value,
42
+ }, value || '—'),
39
43
  copyable && value
40
44
  ? IconBtn('copy', { title: t('settings.copy'), onClick: () => copyText(value) })
41
45
  : null)),
@@ -70,9 +74,9 @@ function Connection(info) {
70
74
  async function createShortcut() {
71
75
  try {
72
76
  const { shortcut } = await post('/api/settings/dashboard-shortcut', { open: true });
73
- toastOk(isDemo ? t('settings.connection.shortcutDemoToast') : t('settings.connection.shortcutToast'));
74
77
  openModal({
75
78
  title: isDemo ? t('settings.connection.shortcutDemoTitle') : t('settings.connection.shortcutTitle'),
79
+ intent: 'contextual',
76
80
  body: el('div', { class: 'stack-3' },
77
81
  el('div', { class: 'callout' },
78
82
  icon('info'),
@@ -83,11 +87,11 @@ function Connection(info) {
83
87
  ? t('settings.connection.shortcutDemoCalloutBody')
84
88
  : t('settings.connection.shortcutCalloutBody')))),
85
89
  el('dl', { class: 'kv' },
86
- ...kvRow(t('settings.connection.shortcutFolder'), shortcut.shortcut_dir, true),
87
- ...kvRow(t('settings.connection.shortcutLauncher'), shortcut.launcher_path, true))),
90
+ ...kvRow(t('settings.connection.shortcutFolder'), shortcut.shortcut_dir, true, { wrapValue: true }),
91
+ ...kvRow(t('settings.connection.shortcutLauncher'), shortcut.launcher_path, true, { wrapValue: true }))),
88
92
  footer: (close) => [
89
- Btn(t('settings.copyPath'), { icon: 'copy', onClick: () => copyText(shortcut.launcher_path) }),
90
- Btn(t('settings.confirm'), { variant: 'primary', onClick: close }),
93
+ Btn(t('settings.copyPath'), { icon: 'copy', variant: 'primary', onClick: () => copyText(shortcut.launcher_path) }),
94
+ Btn(t('common.close'), { variant: 'ghost', onClick: close }),
91
95
  ],
92
96
  });
93
97
  } catch (e) {
@@ -360,6 +364,7 @@ function openImportModal({ fileName, backup, preview, rerender }) {
360
364
  openModal({
361
365
  title: t('settings.import.title'),
362
366
  size: 'lg',
367
+ intent: 'alert',
363
368
  body: el('div', { class: 'stack-3' },
364
369
  el('div', { class: 'callout' },
365
370
  icon('info'),
@@ -409,15 +414,10 @@ function openImportModal({ fileName, backup, preview, rerender }) {
409
414
  /* -------------------------------------------------- 위험 구역 (Danger zone) */
410
415
  /* ----------------------------------------------------- 도움말 · 문제 신고 */
411
416
  function Support() {
412
- const ext = (href, iconName, label) =>
413
- el('a', { class: 'btn btn--ghost btn--sm', href, attrs: { target: '_blank', rel: 'noopener noreferrer' } },
414
- icon(iconName), el('span', {}, label));
415
417
  return Card({
416
418
  title: t('settings.support.title'),
417
419
  sub: t('settings.support.sub'),
418
420
  body: el('div', { class: 'flex gap-2 wrap' },
419
- ext('https://github.com/osntak/careermate/issues/new?template=bug_report.yml', 'external', t('settings.support.report')),
420
- ext('https://github.com/osntak/careermate/discussions', 'external', t('settings.support.ask')),
421
421
  el('a', { class: 'btn btn--ghost btn--sm', href: 'mailto:osntak@gmail.com' }, icon('mail'), el('span', {}, t('settings.support.email'))),
422
422
  ),
423
423
  });
@@ -437,6 +437,7 @@ function DangerZone(ctx) {
437
437
 
438
438
  openModal({
439
439
  title: t('settings.reset.title'),
440
+ intent: 'alert',
440
441
  body: el('div', { class: 'stack-3' },
441
442
  el('div', { class: 'callout' },
442
443
  icon('info'),
@@ -406,6 +406,10 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
406
406
  .list-row:focus-within .row-action { opacity: 1; pointer-events: auto; }
407
407
  @media (hover: none) { .list-row .row-action { opacity: 1; pointer-events: auto; } }
408
408
 
409
+ /* Home's "next action" lane is scanned repeatedly on mobile, so keep it tighter
410
+ than generic cards while preserving the shared ListRow structure. */
411
+ .home-action-card .list-row__trail .badge { max-width: 116px; overflow: hidden; text-overflow: ellipsis; }
412
+
409
413
  /* ----------------------------------------------------------------- pipeline bar + funnel
410
414
  Visualizes the application status_breakdown. Segment color == badge--{status} hue,
411
415
  so a colour means the same stage everywhere (board, badges, here). */
@@ -419,6 +423,9 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
419
423
  .pipefunnel__value { font-size: 24px; font-weight: 700; letter-spacing: -.02em; margin-top: 5px; line-height: 1.1; }
420
424
  .pipefunnel__conv { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
421
425
  @media (max-width: 560px){ .pipefunnel { grid-template-columns: repeat(2, 1fr); } }
426
+ /* Home KPI tiles (the only .grid--4) read better as a 2×2 on phones than a single
427
+ full-width column; overrides the ≤900px collapse for this narrow range. */
428
+ @media (max-width: 560px){ .grid--4 { grid-template-columns: repeat(2, 1fr); } }
422
429
 
423
430
  /* ----------------------------------------------------------------- check row (onboarding)
424
431
  The ONE place setup guidance lives. Outcome rows auto-detect completion. */
@@ -464,6 +471,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
464
471
  /* min-width:0 overrides a grid item's default min-width:auto so the 1fr column
465
472
  can shrink below its content — otherwise a long path/value overflows the grid. */
466
473
  .kv dd { margin: 0; min-width: 0; }
474
+ .kv__value-wrap { min-width: 0; white-space: normal; word-break: keep-all; overflow-wrap: anywhere; line-height: 1.5; }
467
475
  .settings-action-row { align-items: flex-start; min-width: 0; }
468
476
  .settings-action-row__main { flex: 1 1 auto; min-width: 0; }
469
477
  .settings-action-row__main > div:last-child { min-width: 0; }
@@ -473,10 +481,13 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
473
481
  .modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
474
482
  .modal-root.is-open { display: block; }
475
483
  .modal__scrim { position: absolute; inset: 0; background: rgba(16,16,20,.42); backdrop-filter: blur(2px); animation: fade .15s; }
484
+ .modal-root[data-intent="contextual"] .modal__scrim { background: rgba(16,16,20,.22); backdrop-filter: blur(1px); }
485
+ .modal-root[data-intent="alert"] .modal__scrim { background: rgba(16,16,20,.52); }
476
486
  .modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: min(640px, calc(100vw - 32px));
477
487
  max-height: calc(100vh - 48px); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
478
488
  box-shadow: var(--shadow-lg); display: flex; flex-direction: column; animation: pop .16s ease; }
479
489
  .modal--lg { width: min(880px, calc(100vw - 32px)); }
490
+ .modal--alert { width: min(520px, calc(100vw - 32px)); }
480
491
  .modal__head { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; }
481
492
  .modal__head h3 { font-size: 15px; }
482
493
  .modal__body { padding: 20px; overflow-y: auto; }
@@ -485,6 +496,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
485
496
  .modal__error[hidden] { display: none; }
486
497
  .modal__foot { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
487
498
  @keyframes pop { from { transform: translate(-50%,-46%); opacity: 0; } to { transform: translate(-50%,-50%); opacity: 1; } }
499
+ @keyframes sheetup { from { transform: translateY(18px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
488
500
  @keyframes fade { from { opacity: 0; } to { opacity: 1; } }
489
501
 
490
502
  /* Dropdown menu (Menu/ExportMenu) — fixed popover anchored to its trigger so it
@@ -504,6 +516,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
504
516
 
505
517
  /* ----------------------------------------------------------------- toast */
506
518
  .toasts { position: fixed; bottom: 20px; right: 20px; z-index: 80; display: flex; flex-direction: column; gap: 10px; }
519
+ body[data-modal-intent] .toasts { top: 20px; bottom: auto; }
507
520
  .toast { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius);
508
521
  box-shadow: var(--shadow-md); padding: 12px 16px; min-width: 260px; max-width: 380px; animation: slidein .2s ease; font-size: 14px; }
509
522
  .toast--success { border-left-color: var(--green); }
@@ -575,7 +588,27 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
575
588
  .settings-action-row > .btn{ width: 100%; justify-content: center; }
576
589
  .card__head{ align-items: flex-start; flex-wrap: wrap; }
577
590
  .card__head .right{ margin-left: 0; width: 100%; justify-content: flex-start; }
591
+ .home-action-card .card__head{ align-items: center; flex-wrap: nowrap; padding: 12px 14px; }
592
+ .home-action-card .card__head .right{ margin-left: auto; width: auto; justify-content: flex-end; }
593
+ .home-action-card .card__body{ padding: 8px 14px; }
594
+ .home-action-card .list-row{ gap: 10px; padding: 8px 0; }
595
+ .home-action-card .list-row__trail{ gap: 6px; }
578
596
  .page-head{ flex-wrap: wrap; }
597
+ .modal-root[data-intent="contextual"] .modal{
598
+ top: auto; left: 0; right: 0; bottom: 0; transform: none; width: 100%;
599
+ max-height: min(86vh, calc(100vh - 24px)); border-radius: var(--radius-lg) var(--radius-lg) 0 0;
600
+ border-bottom: 0; animation: sheetup .16s ease;
601
+ }
602
+ .modal-root[data-intent="contextual"] .modal__foot{ justify-content: stretch; }
603
+ .modal-root[data-intent="contextual"] .modal__foot .btn{ flex: 1 1 auto; justify-content: center; }
604
+ .modal__foot{
605
+ justify-content: stretch; flex-wrap: wrap;
606
+ padding-bottom: max(14px, env(safe-area-inset-bottom));
607
+ }
608
+ .modal__foot .btn{ flex: 1 1 0; min-width: 0; min-height: 44px; justify-content: center; }
609
+ .toasts{ left: 16px; right: 16px; bottom: max(16px, env(safe-area-inset-bottom)); }
610
+ body[data-modal-intent] .toasts{ top: calc(var(--topbar-h) + 16px); bottom: auto; }
611
+ .toast{ width: 100%; min-width: 0; max-width: none; }
579
612
  }
580
613
 
581
614
  /* ---------- reduced motion ----------