careermate 0.1.0 → 0.1.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.
Files changed (93) hide show
  1. package/README.md +10 -7
  2. package/apps/mcp/src/index.ts +3 -1
  3. package/apps/web/DESIGN_GUIDE.md +2 -2
  4. package/apps/web/public/icon-180.png +0 -0
  5. package/apps/web/public/index.html +4 -1
  6. package/apps/web/public/lib.js +1 -1
  7. package/apps/web/public/pages/applications.js +17 -1
  8. package/apps/web/public/pages/settings.js +6 -4
  9. package/apps/web/public/styles.css +49 -36
  10. package/apps/web/src/info.ts +9 -22
  11. package/apps/web/src/routes.ts +17 -13
  12. package/apps/web/src/server.ts +33 -25
  13. package/apps/web/src/settings.ts +35 -17
  14. package/bin/careermate.mjs +8 -5
  15. package/dist/{mcpb-stage/dist/web.mjs → doctor.mjs} +1122 -1450
  16. package/dist/install-page/favicon.svg +1 -0
  17. package/dist/install-page/icon-180.png +0 -0
  18. package/dist/install-page/icon-512.png +0 -0
  19. package/dist/install-page/index.html +309 -167
  20. package/dist/install-page/llms-install.txt +202 -0
  21. package/dist/install-page/og.png +0 -0
  22. package/dist/install-page/shots/applications-dark.png +0 -0
  23. package/dist/install-page/shots/applications-light.png +0 -0
  24. package/dist/install-page/shots/documents-dark.png +0 -0
  25. package/dist/install-page/shots/documents-light.png +0 -0
  26. package/dist/install-page/shots/home-dark.png +0 -0
  27. package/dist/install-page/shots/home-light.png +0 -0
  28. package/dist/install-page/shots/interview-dark.png +0 -0
  29. package/dist/install-page/shots/interview-light.png +0 -0
  30. package/dist/install-page/shots/jobs-dark.png +0 -0
  31. package/dist/install-page/shots/jobs-light.png +0 -0
  32. package/dist/install-page/shots/settings-dark.png +0 -0
  33. package/dist/install-page/shots/settings-light.png +0 -0
  34. package/dist/install-page/start.html +346 -0
  35. package/dist/install-page/style.css +94 -3
  36. package/dist/mcp.mjs +326 -68
  37. package/dist/migrate.mjs +4715 -0
  38. package/dist/public/icon-180.png +0 -0
  39. package/dist/public/index.html +4 -1
  40. package/dist/public/lib.js +1 -1
  41. package/dist/public/pages/applications.js +17 -1
  42. package/dist/public/pages/settings.js +6 -4
  43. package/dist/public/styles.css +49 -36
  44. package/dist/web.mjs +323 -129
  45. package/docs/DEMO.md +3 -3
  46. package/docs/MCP_TOOLS.md +21 -2
  47. package/docs/ROADMAP.md +3 -3
  48. package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
  49. package/docs/START_WORKFLOW.md +6 -0
  50. package/docs/SUPPORTED_AI_APPS.md +85 -33
  51. package/docs/WORKFLOWS.md +1 -1
  52. package/package.json +3 -2
  53. package/packages/core/src/services.ts +42 -1
  54. package/packages/db/src/connection.ts +30 -0
  55. package/packages/db/src/index.ts +1 -0
  56. package/packages/db/src/repositories.ts +109 -63
  57. package/packages/db/src/schema.ts +118 -0
  58. package/packages/exporters/src/html.ts +9 -12
  59. package/packages/mcp-tools/src/tools.ts +81 -0
  60. package/packages/shared/src/enums.ts +39 -0
  61. package/scripts/build-dist.mjs +27 -8
  62. package/scripts/build-mcpb.mjs +60 -4
  63. package/scripts/gen-og.mjs +86 -0
  64. package/scripts/init.ts +32 -3
  65. package/scripts/seed.ts +5 -5
  66. package/scripts/shots-seed.ts +192 -0
  67. package/scripts/shots.ts +89 -0
  68. package/tsconfig.json +3 -1
  69. package/dist/careermate.mcpb +0 -0
  70. package/dist/mcp-smoke.err +0 -3
  71. package/dist/mcpb-stage/README.md +0 -219
  72. package/dist/mcpb-stage/dist/install-page/index.html +0 -434
  73. package/dist/mcpb-stage/dist/install-page/style.css +0 -407
  74. package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
  75. package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
  76. package/dist/mcpb-stage/dist/public/app.js +0 -118
  77. package/dist/mcpb-stage/dist/public/index.html +0 -41
  78. package/dist/mcpb-stage/dist/public/lib.js +0 -282
  79. package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
  80. package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
  81. package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
  82. package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
  83. package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
  84. package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
  85. package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
  86. package/dist/mcpb-stage/dist/public/styles.css +0 -420
  87. package/dist/mcpb-stage/manifest.json +0 -40
  88. package/docs/INSTALL.md +0 -148
  89. package/docs/INSTALL_AND_USAGE.md +0 -99
  90. package/install-page/index.html +0 -474
  91. package/install-page/style.css +0 -391
  92. package/install-page/vercel.json +0 -20
  93. /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
@@ -1,233 +0,0 @@
1
- // Settings — data location, connection, theme, backup/export, and danger zone.
2
- // Everything stays local; this page surfaces where data lives and how to manage it.
3
- import {
4
- el, get, post, icon, navigate, Card, Stat, Btn, IconBtn, Input,
5
- openModal, copyText, downloadUrl, fmtRelative, toastOk, toastError, mount,
6
- } from '/lib.js';
7
-
8
- const COUNT_LABELS = {
9
- profile: '프로필', experiences: '경력', projects: '프로젝트', skills: '스킬',
10
- documents: '문서', cover_letters: '자기소개서', jobs: '채용공고',
11
- applications: '지원', interview_preps: '면접 준비',
12
- };
13
-
14
- export async function render(ctx) {
15
- ctx.setActions([]);
16
- const { info, backups } = await get('/api/settings');
17
-
18
- const wrap = el('div', { class: 'stack-4' },
19
- DataLocation(info),
20
- Connection(info),
21
- Theme(),
22
- MyData(info.counts),
23
- Backup(backups, () => render(ctx)),
24
- DangerZone(ctx),
25
- );
26
-
27
- mount(ctx.view, wrap);
28
- }
29
-
30
- /* ----------------------------------------------------- 데이터 저장 위치 */
31
- function kvRow(label, value, copyable) {
32
- return [
33
- el('dt', {}, label),
34
- el('dd', {},
35
- el('div', { class: 'flex between gap-3' },
36
- el('span', { class: 'truncate tnum', style: { fontFamily: 'var(--mono, ui-monospace, monospace)' }, title: value }, value || '—'),
37
- copyable && value
38
- ? IconBtn('copy', { title: '복사', onClick: () => copyText(value) })
39
- : null)),
40
- ];
41
- }
42
-
43
- function DataLocation(info) {
44
- const kv = el('dl', { class: 'kv' },
45
- ...kvRow('데이터 폴더', info.data_dir, true),
46
- ...kvRow('데이터베이스', info.db_path, true),
47
- ...kvRow('앱 버전', info.version, false),
48
- ...kvRow('Node 버전', info.node_version, false),
49
- );
50
-
51
- const privacy = el('div', { class: 'callout callout--privacy' },
52
- icon('lock'),
53
- el('div', {},
54
- el('div', { class: 'callout__title' }, '내 컴퓨터에만 저장됩니다'),
55
- el('div', { class: 'callout__body' },
56
- '모든 데이터는 이 컴퓨터에만 저장됩니다. 외부로 전송되지 않습니다. AI에는 사용자가 직접 입력하거나 AI가 MCP로 조회한 정보만 전달됩니다.')));
57
-
58
- return Card({
59
- title: '데이터 저장 위치',
60
- body: el('div', { class: 'stack-3' }, kv, privacy),
61
- });
62
- }
63
-
64
- /* ---------------------------------------------------------- 연결 상태 */
65
- function Connection(info) {
66
- const dashRow = el('div', { class: 'flex between gap-3', style: { padding: '4px 0' } },
67
- el('div', { class: 'flex gap-3', style: { alignItems: 'center' } },
68
- el('span', { class: 'badge badge--final_passed' }, el('span', { class: 'dot' }), '실행 중'),
69
- el('div', {},
70
- el('div', { class: 'strong' }, '대시보드'),
71
- el('div', { class: 'muted text-sm tnum' }, info.url || '—'))),
72
- info.url
73
- ? Btn('새 탭에서 열기', { icon: 'external', sm: true, variant: 'ghost', onClick: () => window.open(info.url, '_blank') })
74
- : null);
75
-
76
- const mcpRow = el('div', { class: 'flex between gap-3', style: { padding: '4px 0', alignItems: 'flex-start' } },
77
- el('div', { class: 'flex gap-3', style: { alignItems: 'flex-start' } },
78
- el('div', { class: 'empty__icon', style: { width: '30px', height: '30px', margin: 0, borderRadius: '8px' } }, icon('link')),
79
- el('div', {},
80
- el('div', { class: 'strong' }, 'MCP 서버'),
81
- el('div', { class: 'muted text-sm', style: { maxWidth: '440px', lineHeight: '1.6' } },
82
- 'MCP 서버는 사용하시는 AI 클라이언트(Claude·ChatGPT·Cursor)가 직접 실행하며, 이 대시보드와 같은 데이터베이스를 공유합니다.'))),
83
- Btn('설치·연결 안내 열기', { icon: 'external', sm: true, variant: 'ghost', onClick: () => window.open('/install', '_blank') }));
84
-
85
- return Card({
86
- title: '연결 상태',
87
- body: el('div', { class: 'stack-2' }, dashRow, el('div', { class: 'divider' }), mcpRow),
88
- });
89
- }
90
-
91
- /* ------------------------------------------------------------- 테마 */
92
- function currentTheme() {
93
- const saved = localStorage.getItem('cf-theme');
94
- return saved === 'light' || saved === 'dark' ? saved : 'system';
95
- }
96
-
97
- function Theme() {
98
- const options = [
99
- { value: 'light', label: '라이트' },
100
- { value: 'dark', label: '다크' },
101
- { value: 'system', label: '시스템' },
102
- ];
103
-
104
- const row = el('div', { class: 'flex gap-2 wrap' });
105
-
106
- function applyTheme(value) {
107
- if (value === 'system') {
108
- delete document.documentElement.dataset.theme;
109
- localStorage.removeItem('cf-theme');
110
- } else {
111
- document.documentElement.dataset.theme = value;
112
- localStorage.setItem('cf-theme', value);
113
- }
114
- paint();
115
- }
116
-
117
- function paint() {
118
- const active = currentTheme();
119
- mount(row, ...options.map((o) => {
120
- const b = Btn(o.label, { variant: 'ghost', onClick: () => applyTheme(o.value) });
121
- b.setAttribute('aria-pressed', String(o.value === active));
122
- if (o.value === active) b.classList.add('is-selected');
123
- return b;
124
- }));
125
- }
126
- paint();
127
-
128
- return Card({
129
- title: '테마',
130
- sub: '시스템 설정을 따르거나 직접 고를 수 있어요',
131
- body: row,
132
- });
133
- }
134
-
135
- /* ---------------------------------------------------------- 내 데이터 */
136
- function MyData(counts = {}) {
137
- const entries = Object.entries(COUNT_LABELS)
138
- .map(([key, label]) => Stat({ label, value: counts[key] ?? 0 }));
139
- return Card({
140
- title: '내 데이터',
141
- sub: '이 컴퓨터에 저장된 항목 수',
142
- body: el('div', { class: 'grid grid--3' }, ...entries),
143
- });
144
- }
145
-
146
- /* ------------------------------------------------- 백업 / 내보내기 */
147
- function Backup(backups = [], rerender) {
148
- async function createBackup() {
149
- try {
150
- const res = await post('/api/settings/backup');
151
- toastOk('백업을 생성했습니다.');
152
- await rerender();
153
- } catch (e) {
154
- toastError(e);
155
- }
156
- }
157
-
158
- const list = backups.length
159
- ? el('table', { class: 'table' },
160
- el('thead', {}, el('tr', {},
161
- el('th', {}, '파일'), el('th', {}, '생성'), el('th', { style: { textAlign: 'right' } }, '크기'))),
162
- el('tbody', {}, ...backups.map((b) => el('tr', {},
163
- el('td', {}, el('span', { class: 'truncate', title: b.path || b.filename }, b.filename)),
164
- el('td', { class: 'muted text-sm' }, fmtRelative(b.created_at)),
165
- el('td', { class: 'muted text-sm tnum', style: { textAlign: 'right' } }, `${Math.round((b.size || 0) / 1024)} KB`)))))
166
- : el('p', { class: 'muted', style: { margin: 0 } }, '아직 백업이 없습니다. "백업 생성"으로 현재 데이터를 안전하게 보관하세요.');
167
-
168
- return Card({
169
- title: '백업 / 내보내기',
170
- actions: [
171
- Btn('백업 생성', { icon: 'copy', sm: true, variant: 'primary', onClick: createBackup }),
172
- Btn('전체 데이터 내보내기(JSON)', { icon: 'download', sm: true, variant: 'ghost', onClick: () => downloadUrl('/api/settings/export-all') }),
173
- ],
174
- body: list,
175
- });
176
- }
177
-
178
- /* -------------------------------------------------- 위험 구역 (Danger zone) */
179
- function DangerZone(ctx) {
180
- function openResetModal() {
181
- let confirmBtn;
182
- const input = Input({
183
- placeholder: 'DELETE',
184
- onInput: (e) => { if (confirmBtn) confirmBtn.disabled = e.target.value.trim() !== 'DELETE'; },
185
- onKeydown: (e) => { if (e.key === 'Enter' && e.target.value.trim() === 'DELETE' && confirmBtn && !confirmBtn.disabled) confirmBtn.click(); },
186
- });
187
-
188
- openModal({
189
- title: '모든 데이터 초기화',
190
- body: el('div', { class: 'stack-3' },
191
- el('div', { class: 'callout' },
192
- icon('info'),
193
- el('div', {},
194
- el('div', { class: 'callout__title' }, '되돌릴 수 없습니다'),
195
- el('div', { class: 'callout__body' },
196
- '프로필·공고·지원·문서·면접 준비를 포함한 모든 데이터가 영구적으로 삭제됩니다. 안전을 위해 초기화 직전에 자동으로 백업이 한 번 생성됩니다.'))),
197
- el('p', { class: 'text-secondary', style: { margin: 0, lineHeight: '1.6' } },
198
- '계속하려면 아래 입력란에 ', el('span', { class: 'strong' }, 'DELETE'), ' 를 입력하세요.'),
199
- input),
200
- footer: (close) => {
201
- confirmBtn = Btn('모든 데이터 초기화', {
202
- variant: 'danger',
203
- disabled: true,
204
- onClick: async () => {
205
- confirmBtn.disabled = true;
206
- try {
207
- await post('/api/settings/reset', { confirm: 'DELETE' });
208
- close();
209
- toastOk('모든 데이터가 초기화되었습니다.');
210
- navigate('/');
211
- await ctx.refreshNav();
212
- } catch (e) {
213
- toastError(e);
214
- confirmBtn.disabled = input.value.trim() !== 'DELETE';
215
- }
216
- },
217
- });
218
- return [Btn('취소', { onClick: close }), confirmBtn];
219
- },
220
- });
221
- }
222
-
223
- return Card({
224
- title: '위험 구역',
225
- sub: '이 작업은 되돌릴 수 없습니다',
226
- body: el('div', { class: 'flex between gap-3 wrap' },
227
- el('div', { style: { maxWidth: '440px' } },
228
- el('div', { class: 'strong' }, '모든 데이터 초기화'),
229
- el('div', { class: 'muted text-sm', style: { lineHeight: '1.6' } },
230
- '이 컴퓨터에 저장된 모든 CareerMate 데이터를 삭제합니다. 초기화 전에 자동으로 백업이 생성됩니다.')),
231
- Btn('모든 데이터 초기화', { icon: 'trash', variant: 'danger', onClick: openResetModal })),
232
- });
233
- }
@@ -1,420 +0,0 @@
1
- /* =============================================================================
2
- CareerMate design system — a calm, dense, real-productivity-tool aesthetic.
3
- No framework, no CDN (works fully offline; nothing leaves the machine).
4
- Inspired by the restraint of Linear / Notion / Raycast — not AI-flashy.
5
- ========================================================================== */
6
-
7
- /* Self-hosted brand font (offline; served from /fonts by the local server).
8
- A real loaded face — not a system fallback — so the type is intentional and
9
- identical on every machine. One variable file covers all weights. */
10
- @font-face {
11
- font-family: 'Pretendard Variable';
12
- font-weight: 45 920;
13
- font-style: normal;
14
- /* block (not swap): show no fallback-then-swap flash; the font is local so the
15
- brief invisibility is imperceptible, and it's preloaded in index.html. */
16
- font-display: block;
17
- src: local('Pretendard Variable'),
18
- url('/fonts/PretendardVariable.woff2') format('woff2');
19
- }
20
-
21
- :root {
22
- /* Neutral palette (light) */
23
- --bg: #ffffff;
24
- --bg-subtle: #f7f7f8;
25
- --bg-sunken: #f1f1f3;
26
- --surface: #ffffff;
27
- --surface-hover: #f7f7f8;
28
- --border: #e6e6e9;
29
- --border-strong: #d4d4d8;
30
- --text: #1c1c1f;
31
- --text-secondary: #5c5c66;
32
- --text-tertiary: #8a8a94;
33
- --text-on-accent: #ffffff;
34
-
35
- /* Accent — calm indigo */
36
- --accent: #4f46e5;
37
- --accent-hover: #4338ca;
38
- --accent-soft: #eef2ff;
39
- --accent-border: #c7d2fe;
40
-
41
- /* Semantic */
42
- --green: #15803d; --green-soft: #ecfdf3; --green-border: #bbf7d0;
43
- --teal: #0f766e; --teal-soft: #effcf9; --teal-border: #99f6e4;
44
- --blue: #1d4ed8; --blue-soft: #eff4ff; --blue-border: #bfdbfe;
45
- --violet: #6d28d9; --violet-soft: #f5f0ff; --violet-border: #ddd6fe;
46
- --amber: #b45309; --amber-soft: #fffbeb; --amber-border: #fde68a;
47
- --red: #b91c1c; --red-soft: #fef2f2; --red-border: #fecaca;
48
- --slate: #475569; --slate-soft: #f5f6f8; --slate-border: #dbe0e6;
49
-
50
- /* Elevation */
51
- --shadow-sm: 0 1px 2px rgba(16,16,20,.05);
52
- --shadow-md: 0 4px 16px rgba(16,16,20,.08);
53
- --shadow-lg: 0 12px 40px rgba(16,16,20,.16);
54
-
55
- /* Geometry */
56
- --radius-sm: 6px;
57
- --radius: 9px;
58
- --radius-lg: 14px;
59
- --sidebar-w: 248px;
60
- --topbar-h: 56px;
61
- --maxw: 1080px;
62
-
63
- --font: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont,
64
- 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
65
- --mono: 'SFMono-Regular', 'JetBrains Mono', Consolas, 'D2Coding', monospace;
66
- }
67
-
68
- @media (prefers-color-scheme: dark) {
69
- :root:not([data-theme='light']) {
70
- --bg: #161618;
71
- --bg-subtle: #1d1d20;
72
- --bg-sunken: #0f0f11;
73
- --surface: #1d1d20;
74
- --surface-hover: #26262a;
75
- --border: #2c2c31;
76
- --border-strong: #3a3a41;
77
- --text: #ededf0;
78
- --text-secondary: #a5a5b0;
79
- --text-tertiary: #74747e;
80
- --accent: #818cf8;
81
- --accent-hover: #a5b4fc;
82
- --accent-soft: #1e1b3a;
83
- --accent-border: #3730a3;
84
- --green-soft:#0e2a1a; --teal-soft:#0c2826; --blue-soft:#11203f; --violet-soft:#1e1638;
85
- --amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b;
86
- --shadow-lg: 0 12px 40px rgba(0,0,0,.5);
87
- }
88
- }
89
- :root[data-theme='dark'] {
90
- --bg:#161618; --bg-subtle:#1d1d20; --bg-sunken:#0f0f11; --surface:#1d1d20; --surface-hover:#26262a;
91
- --border:#2c2c31; --border-strong:#3a3a41; --text:#ededf0; --text-secondary:#a5a5b0; --text-tertiary:#74747e;
92
- --accent:#818cf8; --accent-hover:#a5b4fc; --accent-soft:#1e1b3a; --accent-border:#3730a3;
93
- --green-soft:#0e2a1a; --teal-soft:#0c2826; --blue-soft:#11203f; --violet-soft:#1e1638;
94
- --amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b; --shadow-lg:0 12px 40px rgba(0,0,0,.5);
95
- }
96
-
97
- * { box-sizing: border-box; }
98
- /* Icons must never stretch inside flex containers. */
99
- svg { flex: none; }
100
- html, body { height: 100%; }
101
- body {
102
- margin: 0; font-family: var(--font); color: var(--text); background: var(--bg-subtle);
103
- font-size: 15px; line-height: 1.55; -webkit-font-smoothing: antialiased;
104
- text-rendering: optimizeLegibility;
105
- }
106
- a { color: var(--accent); text-decoration: none; }
107
- a:hover { text-decoration: underline; }
108
- h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
109
- ::selection { background: var(--accent-soft); }
110
- :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
111
-
112
- /* ----------------------------------------------------------------- layout */
113
- .app { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }
114
-
115
- .sidebar {
116
- background: var(--bg); border-right: 1px solid var(--border);
117
- display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh;
118
- }
119
- .sidebar__brand { display: flex; align-items: center; gap: 10px; padding: 18px 18px 14px; }
120
- .sidebar__logo {
121
- width: 30px; height: 30px; border-radius: 8px; flex: none;
122
- background: var(--accent);
123
- display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 15px;
124
- }
125
- .sidebar__title { font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
126
- .sidebar__subtitle { font-size: 12px; color: var(--text-tertiary); margin-top: 1px; }
127
- .nav { padding: 6px 10px; display: flex; flex-direction: column; gap: 1px; flex: 1; overflow-y: auto; }
128
- .nav__label { font-size: 11.5px; font-weight: 600; color: var(--text-tertiary); padding: 12px 10px 4px; text-transform: uppercase; letter-spacing: .04em; }
129
- .nav__item {
130
- display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--radius-sm);
131
- color: var(--text-secondary); font-weight: 500; cursor: pointer; user-select: none; position: relative;
132
- }
133
- .nav__item:hover { background: var(--surface-hover); color: var(--text); text-decoration: none; }
134
- .nav__item.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
135
- .nav__item svg { width: 17px; height: 17px; flex: none; }
136
- .nav__badge { margin-left: auto; font-size: 11.5px; background: var(--bg-sunken); color: var(--text-secondary);
137
- padding: 1px 7px; border-radius: 999px; font-weight: 600; }
138
- .nav__item.is-active .nav__badge { background: var(--accent); color: #fff; }
139
- .sidebar__foot { padding: 12px 16px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-tertiary); }
140
- .sidebar__foot .dot { display:inline-block; width:7px; height:7px; border-radius:999px; background: var(--green); margin-right:6px; }
141
-
142
- .main { display: flex; flex-direction: column; min-width: 0; }
143
- .topbar {
144
- height: var(--topbar-h); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--bg) 88%, transparent);
145
- backdrop-filter: blur(8px); position: sticky; top: 0; z-index: 20;
146
- display: flex; align-items: center; gap: 12px; padding: 0 24px;
147
- }
148
- .topbar__title { font-size: 15px; font-weight: 650; }
149
- .topbar__crumb { color: var(--text-tertiary); }
150
- .topbar__spacer { flex: 1; }
151
- .view { padding: 24px; max-width: var(--maxw); width: 100%; margin: 0 auto; }
152
- .view--wide { max-width: 1280px; }
153
-
154
- .page-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
155
- .page-head__text h1 { font-size: 22px; }
156
- .page-head__text p { margin: 4px 0 0; color: var(--text-secondary); }
157
- .page-head__actions { margin-left: auto; display: flex; gap: 8px; flex: none; }
158
-
159
- /* ----------------------------------------------------------------- grid */
160
- .grid { display: grid; gap: 16px; }
161
- .grid--2 { grid-template-columns: repeat(2, 1fr); }
162
- .grid--3 { grid-template-columns: repeat(3, 1fr); }
163
- .grid--4 { grid-template-columns: repeat(4, 1fr); }
164
- @media (max-width: 900px){ .grid--2,.grid--3,.grid--4{ grid-template-columns: 1fr; } .app{ grid-template-columns: 1fr; } .sidebar{ display:none; } }
165
-
166
- /* ----------------------------------------------------------------- card */
167
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
168
- .card__head { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
169
- .card__head h3 { font-size: 15px; }
170
- .card__head .sub { color: var(--text-tertiary); font-size: 13px; }
171
- .card__head .right { margin-left: auto; display: flex; gap: 6px; }
172
- .card__body { padding: 18px; }
173
- .card__body--tight { padding: 0; }
174
- /* a light bordered block for grouping inside a card/modal — never nest a full .card */
175
- .subcard { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; }
176
- .card.is-clickable { cursor: pointer; transition: border-color .12s, box-shadow .12s, transform .12s; }
177
- .card.is-clickable:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
178
-
179
- /* stat tiles */
180
- .stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; }
181
- .stat.is-clickable { cursor: pointer; transition: border-color .12s, box-shadow .12s; }
182
- .stat.is-clickable:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
183
- .stat__label { font-size: 13px; color: var(--text-secondary); display:flex; align-items:center; gap:7px; }
184
- .stat__label svg { width: 15px; height: 15px; color: var(--text-tertiary); }
185
- .stat__value { font-size: 26px; font-weight: 700; letter-spacing: -.02em; margin-top: 6px; }
186
- .stat__hint { font-size: 12.5px; color: var(--text-tertiary); margin-top: 2px; }
187
-
188
- /* ----------------------------------------------------------------- buttons */
189
- .btn {
190
- display: inline-flex; align-items: center; gap: 7px; height: 34px; padding: 0 13px;
191
- border-radius: var(--radius-sm); border: 1px solid var(--border-strong); background: var(--surface);
192
- color: var(--text); font-weight: 550; font-size: 14px; cursor: pointer; font-family: inherit;
193
- transition: background .12s, border-color .12s, opacity .12s; white-space: nowrap; user-select: none;
194
- }
195
- .btn:hover { background: var(--surface-hover); }
196
- .btn:disabled { opacity: .5; cursor: not-allowed; }
197
- .btn svg { width: 15px; height: 15px; }
198
- .btn--primary { background: var(--accent); border-color: var(--accent); color: #fff; }
199
- .btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
200
- .btn--ghost { border-color: transparent; background: transparent; }
201
- .btn--ghost:hover { background: var(--surface-hover); }
202
- .btn--danger { color: var(--red); border-color: var(--red-border); background: var(--red-soft); }
203
- .btn--danger:hover { background: var(--red); color:#fff; border-color: var(--red); }
204
- .btn--sm { height: 28px; padding: 0 10px; font-size: 13px; }
205
- .btn--block { width: 100%; justify-content: center; }
206
- .icon-btn { width: 30px; height: 30px; padding: 0; justify-content: center; }
207
- /* a ghost button marked as the selected option (segmented toggle), e.g. theme picker */
208
- .btn.is-selected { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
209
-
210
- /* ----------------------------------------------------------------- forms */
211
- .field { margin-bottom: 14px; }
212
- .field > label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
213
- .field .hint { font-size: 12px; color: var(--text-tertiary); margin-top: 5px; }
214
- .input, .textarea, .select {
215
- width: 100%; font-family: inherit; font-size: 14px; color: var(--text);
216
- background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
217
- padding: 8px 11px; transition: border-color .12s, box-shadow .12s;
218
- }
219
- .input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
220
- .textarea { resize: vertical; min-height: 90px; line-height: 1.6; }
221
- .textarea--lg { min-height: 280px; font-size: 15px; }
222
- .input::placeholder, .textarea::placeholder { color: var(--text-tertiary); }
223
- .row { display: flex; gap: 10px; }
224
- .row > * { flex: 1; }
225
- /* compact inline input for the topbar/toolbar (search boxes) */
226
- .input--inline { width: 220px; max-width: 46vw; }
227
-
228
- /* ----------------------------------------------------------------- badges */
229
- .badge { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600;
230
- padding: 2px 9px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg-sunken); color: var(--text-secondary); white-space: nowrap; }
231
- .badge .dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
232
- .badge--draft { color: var(--slate); background: var(--slate-soft); border-color: var(--slate-border); }
233
- .badge--planned { color: var(--blue); background: var(--blue-soft); border-color: var(--blue-border); }
234
- .badge--applied { color: var(--violet); background: var(--violet-soft); border-color: var(--violet-border); }
235
- .badge--document_passed { color: var(--teal); background: var(--teal-soft); border-color: var(--teal-border); }
236
- .badge--interview { color: var(--amber); background: var(--amber-soft); border-color: var(--amber-border); }
237
- .badge--final_passed { color: var(--green); background: var(--green-soft); border-color: var(--green-border); }
238
- .badge--rejected { color: var(--red); background: var(--red-soft); border-color: var(--red-border); }
239
- .badge--on_hold { color: var(--slate); background: var(--slate-soft); border-color: var(--slate-border); }
240
- .badge--accent { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-border); }
241
- .badge--score { font-variant-numeric: tabular-nums; }
242
-
243
- .chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; padding:3px 9px; border-radius:7px;
244
- background: var(--bg-sunken); color: var(--text-secondary); border:1px solid var(--border); }
245
- .chip--accent { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
246
- .chips { display:flex; flex-wrap:wrap; gap:6px; }
247
-
248
- /* ----------------------------------------------------------------- table */
249
- .table { width: 100%; border-collapse: collapse; }
250
- .table th { text-align: left; font-size: 12px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase;
251
- letter-spacing: .03em; padding: 10px 16px; border-bottom: 1px solid var(--border); }
252
- .table td { padding: 12px 16px; border-bottom: 1px solid var(--border); vertical-align: middle; }
253
- .table tr:last-child td { border-bottom: none; }
254
- .table tbody tr.is-clickable { cursor: pointer; }
255
- .table tbody tr.is-clickable:hover { background: var(--surface-hover); }
256
- .table .muted { color: var(--text-tertiary); }
257
- .table .strong { font-weight: 600; }
258
-
259
- /* ----------------------------------------------------------------- progress */
260
- .progress { height: 7px; background: var(--bg-sunken); border-radius: 999px; overflow: hidden; }
261
- .progress__bar { height: 100%; background: var(--accent); border-radius: 999px; transition: width .4s ease; }
262
-
263
- /* ----------------------------------------------------------------- empty state */
264
- .empty { text-align: center; padding: 44px 24px; color: var(--text-secondary); }
265
- .empty__icon { width: 46px; height: 46px; border-radius: 12px; background: var(--bg-sunken); display: grid; place-items: center; margin: 0 auto 14px; color: var(--text-tertiary); }
266
- .empty__icon svg { width: 22px; height: 22px; }
267
- .empty h3 { font-size: 15px; color: var(--text); margin-bottom: 6px; }
268
- .empty p { max-width: 420px; margin: 0 auto 16px; font-size: 13.5px; }
269
-
270
- /* ----------------------------------------------------------------- timeline / versions */
271
- .timeline { display: flex; flex-direction: column; }
272
- .tl-item { display: grid; grid-template-columns: 22px 1fr; gap: 12px; padding-bottom: 4px; }
273
- .tl-item__rail { display: flex; flex-direction: column; align-items: center; }
274
- .tl-item__dot { width: 11px; height: 11px; border-radius: 999px; background: var(--surface); border: 2px solid var(--border-strong); margin-top: 4px; flex: none; }
275
- .tl-item.is-current .tl-item__dot { background: var(--accent); border-color: var(--accent); }
276
- .tl-item__line { width: 2px; flex: 1; background: var(--border); margin: 4px 0; }
277
- .tl-item:last-child .tl-item__line { display: none; }
278
- .tl-item__body { padding-bottom: 16px; }
279
-
280
- /* ----------------------------------------------------------------- kanban */
281
- .board { display: grid; grid-auto-flow: column; grid-auto-columns: 272px; gap: 14px; overflow-x: auto; padding-bottom: 8px; }
282
- .board__col { background: var(--bg-subtle); border: 1px solid var(--border); border-radius: var(--radius-lg); display: flex; flex-direction: column; max-height: calc(100vh - 200px); }
283
- .board__col-head { padding: 11px 14px; display: flex; align-items: center; gap: 8px; position: sticky; top: 0; }
284
- .board__col-head .count { margin-left: auto; font-size: 12.5px; color: var(--text-tertiary); font-weight: 600; }
285
- .board__cards { padding: 0 10px 12px; display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
286
- .board-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 11px 12px; cursor: pointer; box-shadow: var(--shadow-sm); }
287
- .board-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
288
- .board-card__company { font-weight: 600; font-size: 14px; }
289
- .board-card__role { font-size: 13px; color: var(--text-secondary); margin-top: 1px; }
290
- .board-card__meta { display: flex; align-items: center; gap: 8px; margin-top: 9px; }
291
-
292
- /* ----------------------------------------------------------------- list row (shared)
293
- One primitive behind the home action lane, recent-job list, and any dense list.
294
- Leading slot → title + muted sub → right-aligned trailing (badges/score). */
295
- .list-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
296
- .list-row:last-child { border-bottom: none; }
297
- .list-row.is-clickable { cursor: pointer; transition: background .12s; }
298
- .list-row.is-clickable:hover { background: var(--surface-hover); }
299
- .list-row__lead { flex: none; display: grid; place-items: center; }
300
- .list-row__lead--chip { width: 30px; height: 30px; border-radius: 8px; background: var(--bg-sunken); color: var(--text-tertiary); }
301
- .list-row__lead--chip svg { width: 16px; height: 16px; }
302
- .list-row__dot { width: 9px; height: 9px; border-radius: 999px; flex: none; }
303
- .list-row__main { flex: 1; min-width: 0; }
304
- .list-row__title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
305
- .list-row__sub { font-size: 13px; color: var(--text-tertiary); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
306
- .list-row__trail { flex: none; display: flex; align-items: center; gap: 8px; margin-left: auto; white-space: nowrap; }
307
-
308
- /* applications: status-group section header (reads clearly as a section title) */
309
- .app-group { }
310
- .app-group__head { display: flex; align-items: center; gap: 9px; padding-bottom: 8px; margin-bottom: 2px; border-bottom: 1px solid var(--border); }
311
- .app-group__dot { width: 9px; height: 9px; border-radius: 999px; flex: none; }
312
- .app-group__title { font-weight: 650; font-size: 15px; letter-spacing: -.01em; }
313
- .app-group__count { margin-left: auto; color: var(--text-tertiary); font-size: 13px; }
314
-
315
- /* ----------------------------------------------------------------- pipeline bar + funnel
316
- Visualizes the application status_breakdown. Segment color == badge--{status} hue,
317
- so a colour means the same stage everywhere (board, badges, here). */
318
- .pipebar { display: flex; height: 14px; border-radius: 999px; overflow: hidden; background: var(--bg-sunken); gap: 2px; }
319
- .pipebar__seg { min-width: 8px; cursor: pointer; transition: flex-grow .4s ease, opacity .12s; }
320
- .pipebar__seg:hover { opacity: .8; }
321
- .pipefunnel { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-top: 18px; }
322
- .pipefunnel__tile { cursor: pointer; }
323
- .pipefunnel__label { font-size: 13px; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; }
324
- .pipefunnel__dot { width: 8px; height: 8px; border-radius: 999px; flex: none; }
325
- .pipefunnel__value { font-size: 24px; font-weight: 700; letter-spacing: -.02em; margin-top: 5px; line-height: 1.1; }
326
- .pipefunnel__conv { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
327
- @media (max-width: 560px){ .pipefunnel { grid-template-columns: repeat(2, 1fr); } }
328
-
329
- /* ----------------------------------------------------------------- check row (onboarding)
330
- The ONE place setup guidance lives. Outcome rows auto-detect completion. */
331
- .check-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; }
332
- .check-row__dot { width: 16px; height: 16px; border: 2px solid var(--border-strong); border-radius: 999px; flex: none; box-sizing: border-box; }
333
- .check-row__icon { width: 16px; height: 16px; flex: none; color: var(--green); }
334
- .check-row__label { font-size: 14px; }
335
- .check-row.is-done .check-row__label { color: var(--text-tertiary); text-decoration: line-through; }
336
- .check-row__label.is-link { color: var(--accent); cursor: pointer; }
337
- .check-row__label.is-link:hover { text-decoration: underline; }
338
-
339
- /* ----------------------------------------------------------------- activity feed (day-bucketed) */
340
- .feed-group { font-size: 11.5px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .04em; padding: 14px 0 6px; }
341
- .feed-group:first-child { padding-top: 0; }
342
- .feed-item { display: flex; gap: 12px; align-items: flex-start; padding: 6px 0; }
343
- .feed-item__icon { width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; flex: none; background: var(--bg-sunken); color: var(--text-tertiary); }
344
- .feed-item__icon svg { width: 16px; height: 16px; }
345
- .feed-item__body { flex: 1; min-width: 0; }
346
- .feed-item__text { font-size: 13.5px; line-height: 1.5; }
347
- .feed-item__time { display: block; font-size: 12.5px; color: var(--text-tertiary); margin-top: 1px; }
348
-
349
- /* ----------------------------------------------------------------- first-run hero
350
- Day one: a single getting-started card centered on a quiet dot canvas, instead
351
- of a wall of empty cards. */
352
- .firstrun-hero { display: grid; place-items: start center; padding: 28px 0;
353
- background-image: radial-gradient(var(--border) 1px, transparent 1px); background-size: 24px 24px; border-radius: var(--radius-lg); }
354
- .firstrun-hero > .card { width: 100%; max-width: 540px; }
355
-
356
- /* ----------------------------------------------------------------- doc preview */
357
- .doc-preview { white-space: pre-wrap; word-break: break-word; font-size: 15px; line-height: 1.75; color: var(--text);
358
- background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 26px; max-height: 60vh; overflow-y: auto; }
359
- .kv { display: grid; grid-template-columns: 132px 1fr; gap: 8px 16px; }
360
- .kv dt { color: var(--text-tertiary); font-size: 13px; }
361
- .kv dd { margin: 0; }
362
-
363
- /* ----------------------------------------------------------------- modal */
364
- .modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
365
- .modal-root.is-open { display: block; }
366
- .modal__scrim { position: absolute; inset: 0; background: rgba(16,16,20,.42); backdrop-filter: blur(2px); animation: fade .15s; }
367
- .modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: min(640px, calc(100vw - 32px));
368
- max-height: calc(100vh - 48px); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
369
- box-shadow: var(--shadow-lg); display: flex; flex-direction: column; animation: pop .16s ease; }
370
- .modal--lg { width: min(880px, calc(100vw - 32px)); }
371
- .modal__head { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; }
372
- .modal__head h3 { font-size: 15px; }
373
- .modal__body { padding: 20px; overflow-y: auto; }
374
- .modal__foot { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
375
- @keyframes pop { from { transform: translate(-50%,-46%); opacity: 0; } to { transform: translate(-50%,-50%); opacity: 1; } }
376
- @keyframes fade { from { opacity: 0; } to { opacity: 1; } }
377
-
378
- /* ----------------------------------------------------------------- toast */
379
- .toasts { position: fixed; bottom: 20px; right: 20px; z-index: 80; display: flex; flex-direction: column; gap: 10px; }
380
- .toast { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius);
381
- box-shadow: var(--shadow-md); padding: 12px 16px; min-width: 260px; max-width: 380px; animation: slidein .2s ease; font-size: 14px; }
382
- .toast--success { border-left-color: var(--green); }
383
- .toast--error { border-left-color: var(--red); }
384
- .toast__title { font-weight: 600; margin-bottom: 2px; }
385
- .toast__body { color: var(--text-secondary); font-size: 13px; }
386
- @keyframes slidein { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
387
-
388
- /* ----------------------------------------------------------------- tabs */
389
- .tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 18px; }
390
- .tab { padding: 9px 14px; font-size: 14px; font-weight: 550; color: var(--text-secondary); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
391
- .tab:hover { color: var(--text); }
392
- .tab.is-active { color: var(--accent); border-bottom-color: var(--accent); }
393
-
394
- /* ----------------------------------------------------------------- utils */
395
- .muted { color: var(--text-tertiary); }
396
- .text-sm { font-size: 13px; }
397
- .text-secondary { color: var(--text-secondary); }
398
- .stack { display: flex; flex-direction: column; }
399
- .stack-2 > * + * { margin-top: 8px; } .stack-3 > * + * { margin-top: 12px; } .stack-4 > * + * { margin-top: 16px; }
400
- .flex { display: flex; align-items: center; } .flex-col{ display:flex; flex-direction:column; }
401
- .gap-2 { gap: 8px; } .gap-3 { gap: 12px; } .gap-4 { gap: 16px; }
402
- .between { justify-content: space-between; } .center { align-items: center; } .wrap { flex-wrap: wrap; }
403
- .ml-auto { margin-left: auto; } .mt-2{margin-top:8px;} .mt-3{margin-top:12px;} .mt-4{margin-top:16px;} .mb-2{margin-bottom:8px;} .mb-3{margin-bottom:12px;} .mb-4{margin-bottom:16px;}
404
- .tnum { font-variant-numeric: tabular-nums; }
405
- .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
406
- .divider { height: 1px; background: var(--border); margin: 16px 0; }
407
- .skeleton { background: linear-gradient(90deg, var(--bg-sunken) 25%, var(--surface-hover) 37%, var(--bg-sunken) 63%); background-size: 400% 100%; animation: shimmer 1.4s infinite; border-radius: 6px; }
408
- @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
409
- .hide { display: none !important; }
410
-
411
- /* score color helper */
412
- .score-strong { color: var(--green); } .score-mid { color: var(--amber); } .score-weak { color: var(--red); }
413
-
414
- /* banner / callout */
415
- .callout { display: flex; gap: 12px; padding: 14px 16px; border-radius: var(--radius); border: 1px solid var(--accent-border); background: var(--accent-soft); }
416
- .callout svg { flex: none; width: 18px; height: 18px; color: var(--accent); margin-top: 1px; }
417
- .callout--privacy { border-color: var(--border); background: var(--bg-subtle); }
418
- .callout--privacy svg { color: var(--text-tertiary); }
419
- .callout__title { font-weight: 600; margin-bottom: 2px; }
420
- .callout__body { color: var(--text-secondary); font-size: 13px; }