careermate 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -17
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/apps/mcp/src/index.ts +4 -3
- package/apps/web/DESIGN_GUIDE.md +2 -2
- package/apps/web/demo/demo-shim.js +536 -0
- package/apps/web/demo/demo.css +75 -0
- package/apps/web/demo/seed.js +269 -0
- package/apps/web/public/app.js +34 -2
- package/apps/web/public/icon-180.png +0 -0
- package/apps/web/public/index.html +14 -3
- package/apps/web/public/lib.js +135 -23
- package/apps/web/public/pages/applications.js +18 -9
- package/apps/web/public/pages/documents.js +66 -50
- package/apps/web/public/pages/home.js +12 -11
- package/apps/web/public/pages/interview.js +9 -12
- package/apps/web/public/pages/jobs.js +39 -36
- package/apps/web/public/pages/profile.js +66 -86
- package/apps/web/public/pages/settings.js +21 -14
- package/apps/web/public/styles.css +105 -38
- package/apps/web/src/http.ts +31 -4
- package/apps/web/src/info.ts +9 -22
- package/apps/web/src/routes.ts +19 -14
- package/apps/web/src/server.ts +34 -29
- package/apps/web/src/settings.ts +35 -17
- package/bin/careermate.mjs +8 -5
- package/dist/doctor.mjs +78908 -0
- package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +12 -0
- package/dist/install-page/.omc/state/idle-notif-cooldown.json +3 -0
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +147 -0
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +22 -0
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +44 -0
- package/dist/install-page/favicon.svg +1 -0
- package/dist/install-page/icon-180.png +0 -0
- package/dist/install-page/icon-512.png +0 -0
- package/dist/install-page/index.html +328 -225
- package/dist/install-page/landing.js +54 -0
- package/dist/install-page/llms-install.txt +202 -0
- package/dist/install-page/og.png +0 -0
- package/dist/install-page/shots/applications-dark.png +0 -0
- package/dist/install-page/shots/applications-light.png +0 -0
- package/dist/install-page/shots/documents-dark.png +0 -0
- package/dist/install-page/shots/documents-light.png +0 -0
- package/dist/install-page/shots/home-dark.png +0 -0
- package/dist/install-page/shots/home-light.png +0 -0
- package/dist/install-page/shots/interview-dark.png +0 -0
- package/dist/install-page/shots/interview-light.png +0 -0
- package/dist/install-page/shots/jobs-dark.png +0 -0
- package/dist/install-page/shots/jobs-light.png +0 -0
- package/dist/install-page/shots/settings-dark.png +0 -0
- package/dist/install-page/shots/settings-light.png +0 -0
- package/dist/install-page/start.html +323 -0
- package/dist/install-page/style.css +128 -4
- package/dist/mcp.mjs +74225 -2007
- package/dist/{mcpb-stage/dist/web.mjs → migrate.mjs} +142 -2660
- package/dist/pdf.worker.min.mjs +28 -0
- package/dist/public/app.js +34 -2
- package/dist/public/icon-180.png +0 -0
- package/dist/public/index.html +14 -3
- package/dist/public/lib.js +135 -23
- package/dist/public/pages/applications.js +18 -9
- package/dist/public/pages/documents.js +66 -50
- package/dist/public/pages/home.js +12 -11
- package/dist/public/pages/interview.js +9 -12
- package/dist/public/pages/jobs.js +39 -36
- package/dist/public/pages/profile.js +66 -86
- package/dist/public/pages/settings.js +21 -14
- package/dist/public/styles.css +105 -38
- package/dist/web.mjs +39950 -369
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/DECISIONS.md +1 -1
- package/docs/DEMO.md +6 -6
- package/docs/MCP_TOOLS.md +68 -9
- package/docs/ROADMAP.md +11 -10
- package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
- package/docs/START_WORKFLOW.md +19 -7
- package/docs/SUPPORTED_AI_APPS.md +85 -33
- package/docs/TODO.md +1 -1
- package/docs/WORKFLOWS.md +8 -6
- package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +64 -0
- package/package.json +8 -3
- package/packages/core/src/services.ts +42 -1
- package/packages/db/src/connection.ts +30 -0
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/paths.ts +11 -0
- package/packages/db/src/repositories.ts +109 -63
- package/packages/db/src/schema.ts +118 -0
- package/packages/exporters/src/html.ts +9 -12
- package/packages/mcp-tools/src/bridge.ts +55 -32
- package/packages/mcp-tools/src/index.ts +9 -0
- package/packages/mcp-tools/src/tools.ts +269 -65
- package/packages/mcp-tools/src/update.ts +330 -0
- package/packages/parsers/src/documents.ts +521 -0
- package/packages/parsers/src/index.ts +8 -5
- package/packages/prompts/src/onboarding.ts +2 -2
- package/packages/shared/src/build-info.ts +22 -0
- package/packages/shared/src/enums.ts +39 -0
- package/packages/shared/src/index.ts +1 -0
- package/packages/shared/src/schemas.ts +24 -3
- package/packages/workflows/src/definitions.ts +4 -2
- package/scripts/build-demo.mjs +130 -0
- package/scripts/build-dist.mjs +46 -8
- package/scripts/build-mcpb.mjs +60 -4
- package/scripts/doctor.ts +16 -3
- package/scripts/gen-og.mjs +86 -0
- package/scripts/init.ts +32 -3
- package/scripts/seed.ts +5 -5
- package/scripts/shots-seed.ts +192 -0
- package/scripts/shots.ts +89 -0
- package/tsconfig.json +3 -1
- package/dist/careermate.mcpb +0 -0
- package/dist/mcp-smoke.err +0 -3
- package/dist/mcpb-stage/README.md +0 -219
- package/dist/mcpb-stage/dist/install-page/index.html +0 -434
- package/dist/mcpb-stage/dist/install-page/style.css +0 -407
- package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
- package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
- package/dist/mcpb-stage/dist/public/app.js +0 -118
- package/dist/mcpb-stage/dist/public/index.html +0 -41
- package/dist/mcpb-stage/dist/public/lib.js +0 -282
- package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
- package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
- package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
- package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
- package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
- package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
- package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
- package/dist/mcpb-stage/dist/public/styles.css +0 -420
- package/dist/mcpb-stage/manifest.json +0 -40
- package/docs/INSTALL.md +0 -148
- package/docs/INSTALL_AND_USAGE.md +0 -99
- package/install-page/index.html +0 -474
- package/install-page/style.css +0 -391
- package/install-page/vercel.json +0 -20
- /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
// Profile — the user's career identity, fully editable. Feeds AI writing/analysis.
|
|
2
2
|
import {
|
|
3
|
-
el, get, post, put, del, icon, Card, Badge, Btn, IconBtn, EmptyState, Chips,
|
|
3
|
+
el, get, post, put, del, icon, Card, Badge, Btn, IconBtn, SubmitBtn, EmptyState, Chips,
|
|
4
4
|
Field, Input, Textarea, openModal, closeModal, confirmDialog,
|
|
5
|
-
toastOk, toastError, fmtDate, mount,
|
|
5
|
+
toastOk, toastError, fmtDate, mount, linesToArray, csvToArray,
|
|
6
6
|
} from '/lib.js';
|
|
7
7
|
|
|
8
8
|
/* ----------------------------------------------------------- small helpers */
|
|
9
9
|
|
|
10
|
-
// textarea (newline-separated) → trimmed array, empties dropped.
|
|
11
|
-
function lines(str) {
|
|
12
|
-
return String(str || '').split('\n').map((s) => s.trim()).filter(Boolean);
|
|
13
|
-
}
|
|
14
|
-
// "a, b, c" → trimmed array, empties dropped.
|
|
15
|
-
function commas(str) {
|
|
16
|
-
return String(str || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
17
|
-
}
|
|
18
10
|
function val(node) { return node ? node.value : ''; }
|
|
19
11
|
|
|
20
12
|
// Render a "YYYY-MM ~ YYYY-MM / 재직중" period label from a record.
|
|
@@ -146,24 +138,22 @@ function editBasic(p, reload) {
|
|
|
146
138
|
),
|
|
147
139
|
footer: (close) => [
|
|
148
140
|
Btn('취소', { onClick: close }),
|
|
149
|
-
|
|
150
|
-
const links =
|
|
141
|
+
SubmitBtn('기본 정보 저장', async () => {
|
|
142
|
+
const links = linesToArray(linksTa.value).map((line) => {
|
|
151
143
|
const i = line.indexOf('|');
|
|
152
144
|
const label = (i >= 0 ? line.slice(0, i) : line).trim();
|
|
153
145
|
const url = (i >= 0 ? line.slice(i + 1) : line).trim();
|
|
154
146
|
return { label: label || url, url };
|
|
155
147
|
}).filter((l) => l.url);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} catch (e) { toastError(e); }
|
|
166
|
-
} }),
|
|
148
|
+
await put('/api/profile', {
|
|
149
|
+
name: val(name).trim(), email: val(email).trim(), phone: val(phone).trim(),
|
|
150
|
+
location: val(location).trim(), headline: val(headline).trim(),
|
|
151
|
+
summary: val(summary).trim(), links,
|
|
152
|
+
});
|
|
153
|
+
toastOk('기본 정보를 저장했어요.');
|
|
154
|
+
close();
|
|
155
|
+
await reload();
|
|
156
|
+
}),
|
|
167
157
|
],
|
|
168
158
|
});
|
|
169
159
|
}
|
|
@@ -206,17 +196,15 @@ function editDesired(p, reload) {
|
|
|
206
196
|
),
|
|
207
197
|
footer: (close) => [
|
|
208
198
|
Btn('취소', { onClick: close }),
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
} catch (e) { toastError(e); }
|
|
219
|
-
} }),
|
|
199
|
+
SubmitBtn('희망 조건 저장', async () => {
|
|
200
|
+
await put('/api/profile', {
|
|
201
|
+
desired_roles: csvToArray(roles.value),
|
|
202
|
+
desired_conditions: val(conditions).trim(),
|
|
203
|
+
});
|
|
204
|
+
toastOk('희망 조건을 저장했어요.');
|
|
205
|
+
close();
|
|
206
|
+
await reload();
|
|
207
|
+
}),
|
|
220
208
|
],
|
|
221
209
|
});
|
|
222
210
|
}
|
|
@@ -260,17 +248,15 @@ function editWriting(p, reload) {
|
|
|
260
248
|
),
|
|
261
249
|
footer: (close) => [
|
|
262
250
|
Btn('취소', { onClick: close }),
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
} catch (e) { toastError(e); }
|
|
273
|
-
} }),
|
|
251
|
+
SubmitBtn('설정 저장', async () => {
|
|
252
|
+
await put('/api/profile', {
|
|
253
|
+
preferred_tone: val(tone).trim(),
|
|
254
|
+
emphasis_points: linesToArray(emphasis.value),
|
|
255
|
+
});
|
|
256
|
+
toastOk('자기소개서 설정을 저장했어요.');
|
|
257
|
+
close();
|
|
258
|
+
await reload();
|
|
259
|
+
}),
|
|
274
260
|
],
|
|
275
261
|
});
|
|
276
262
|
}
|
|
@@ -323,7 +309,7 @@ function ExperiencesCard(items, reload) {
|
|
|
323
309
|
async function removeExperience(x, reload) {
|
|
324
310
|
const ok = await confirmDialog({ title: '경력 삭제', message: `"${x.company}" 경력을 삭제할까요?`, confirmLabel: '삭제', danger: true });
|
|
325
311
|
if (!ok) return;
|
|
326
|
-
try { await del(`/api/experiences/${x.id}`); toastOk('삭제했어요.'); await reload(); }
|
|
312
|
+
try { await del(`/api/experiences/${x.id}`); toastOk('경력을 삭제했어요.'); await reload(); }
|
|
327
313
|
catch (e) { toastError(e); }
|
|
328
314
|
}
|
|
329
315
|
|
|
@@ -356,8 +342,8 @@ function editExperience(x, reload) {
|
|
|
356
342
|
),
|
|
357
343
|
footer: (close) => [
|
|
358
344
|
Btn('취소', { onClick: close }),
|
|
359
|
-
|
|
360
|
-
if (!val(company).trim())
|
|
345
|
+
SubmitBtn(x ? '경력 저장' : '경력 추가', async () => {
|
|
346
|
+
if (!val(company).trim()) throw new Error('회사명을 입력해 주세요.');
|
|
361
347
|
const payload = {
|
|
362
348
|
company: val(company).trim(),
|
|
363
349
|
role: val(role).trim(),
|
|
@@ -366,17 +352,15 @@ function editExperience(x, reload) {
|
|
|
366
352
|
end_date: isCurrent.checked ? '' : val(end).trim(),
|
|
367
353
|
is_current: isCurrent.checked,
|
|
368
354
|
description: val(desc).trim(),
|
|
369
|
-
achievements:
|
|
370
|
-
tech:
|
|
355
|
+
achievements: linesToArray(achievements.value),
|
|
356
|
+
tech: csvToArray(tech.value),
|
|
371
357
|
};
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
} catch (e) { toastError(e); }
|
|
379
|
-
} }),
|
|
358
|
+
if (x) await put(`/api/experiences/${x.id}`, payload);
|
|
359
|
+
else await post('/api/experiences', payload);
|
|
360
|
+
toastOk(x ? '경력을 수정했어요.' : '경력을 추가했어요.');
|
|
361
|
+
close();
|
|
362
|
+
await reload();
|
|
363
|
+
}),
|
|
380
364
|
],
|
|
381
365
|
});
|
|
382
366
|
}
|
|
@@ -432,7 +416,7 @@ function periodProject(x) {
|
|
|
432
416
|
async function removeProject(x, reload) {
|
|
433
417
|
const ok = await confirmDialog({ title: '프로젝트 삭제', message: `"${x.name}" 프로젝트를 삭제할까요?`, confirmLabel: '삭제', danger: true });
|
|
434
418
|
if (!ok) return;
|
|
435
|
-
try { await del(`/api/projects/${x.id}`); toastOk('삭제했어요.'); await reload(); }
|
|
419
|
+
try { await del(`/api/projects/${x.id}`); toastOk('프로젝트를 삭제했어요.'); await reload(); }
|
|
436
420
|
catch (e) { toastError(e); }
|
|
437
421
|
}
|
|
438
422
|
|
|
@@ -460,43 +444,41 @@ function editProject(x, reload) {
|
|
|
460
444
|
),
|
|
461
445
|
footer: (close) => [
|
|
462
446
|
Btn('취소', { onClick: close }),
|
|
463
|
-
|
|
464
|
-
if (!val(name).trim())
|
|
447
|
+
SubmitBtn(x ? '프로젝트 저장' : '프로젝트 추가', async () => {
|
|
448
|
+
if (!val(name).trim()) throw new Error('프로젝트명을 입력해 주세요.');
|
|
465
449
|
const payload = {
|
|
466
450
|
name: val(name).trim(),
|
|
467
451
|
role: val(role).trim(),
|
|
468
452
|
description: val(desc).trim(),
|
|
469
|
-
highlights:
|
|
470
|
-
tech:
|
|
453
|
+
highlights: linesToArray(highlights.value),
|
|
454
|
+
tech: csvToArray(tech.value),
|
|
471
455
|
url: val(url).trim(),
|
|
472
456
|
start_date: val(start).trim(),
|
|
473
457
|
end_date: val(end).trim(),
|
|
474
458
|
};
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
} catch (e) { toastError(e); }
|
|
482
|
-
} }),
|
|
459
|
+
if (x) await put(`/api/projects/${x.id}`, payload);
|
|
460
|
+
else await post('/api/projects', payload);
|
|
461
|
+
toastOk(x ? '프로젝트를 수정했어요.' : '프로젝트를 추가했어요.');
|
|
462
|
+
close();
|
|
463
|
+
await reload();
|
|
464
|
+
}),
|
|
483
465
|
],
|
|
484
466
|
});
|
|
485
467
|
}
|
|
486
468
|
|
|
487
|
-
/* -------------------------------------------------------- 6.
|
|
469
|
+
/* -------------------------------------------------------- 6. 기술 (Skills) */
|
|
488
470
|
|
|
489
471
|
function SkillsCard(items, reload) {
|
|
490
472
|
const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editSkill(null, reload) });
|
|
491
473
|
|
|
492
474
|
if (!items.length) {
|
|
493
475
|
return Card({
|
|
494
|
-
title: '
|
|
476
|
+
title: '기술',
|
|
495
477
|
actions: add,
|
|
496
478
|
body: EmptyState({
|
|
497
479
|
iconName: 'sparkle',
|
|
498
480
|
title: '등록된 기술이 없어요',
|
|
499
|
-
body: '보유
|
|
481
|
+
body: '보유 기술을 정리해 두면 공고 키워드 매칭이 정확해집니다.',
|
|
500
482
|
action: Btn('기술 추가', { icon: 'plus', variant: 'primary', onClick: () => editSkill(null, reload) }),
|
|
501
483
|
}),
|
|
502
484
|
});
|
|
@@ -524,7 +506,7 @@ function SkillsCard(items, reload) {
|
|
|
524
506
|
IconBtn('trash', { title: '삭제', variant: 'danger', onClick: () => removeSkill(s, reload) })))))));
|
|
525
507
|
|
|
526
508
|
return Card({
|
|
527
|
-
title: '
|
|
509
|
+
title: '기술',
|
|
528
510
|
sub: `${items.length}개`,
|
|
529
511
|
actions: add,
|
|
530
512
|
body: el('div', { class: 'stack-3' }, ...blocks),
|
|
@@ -534,7 +516,7 @@ function SkillsCard(items, reload) {
|
|
|
534
516
|
async function removeSkill(s, reload) {
|
|
535
517
|
const ok = await confirmDialog({ title: '기술 삭제', message: `"${s.name}"을(를) 삭제할까요?`, confirmLabel: '삭제', danger: true });
|
|
536
518
|
if (!ok) return;
|
|
537
|
-
try { await del(`/api/skills/${s.id}`); toastOk('삭제했어요.'); await reload(); }
|
|
519
|
+
try { await del(`/api/skills/${s.id}`); toastOk('기술을 삭제했어요.'); await reload(); }
|
|
538
520
|
catch (e) { toastError(e); }
|
|
539
521
|
}
|
|
540
522
|
|
|
@@ -554,8 +536,8 @@ function editSkill(s, reload) {
|
|
|
554
536
|
),
|
|
555
537
|
footer: (close) => [
|
|
556
538
|
Btn('취소', { onClick: close }),
|
|
557
|
-
|
|
558
|
-
if (!val(name).trim())
|
|
539
|
+
SubmitBtn(s ? '기술 저장' : '기술 추가', async () => {
|
|
540
|
+
if (!val(name).trim()) throw new Error('기술명을 입력해 주세요.');
|
|
559
541
|
const yrs = val(years).trim();
|
|
560
542
|
const payload = {
|
|
561
543
|
name: val(name).trim(),
|
|
@@ -563,14 +545,12 @@ function editSkill(s, reload) {
|
|
|
563
545
|
level: val(level).trim(),
|
|
564
546
|
};
|
|
565
547
|
if (yrs !== '' && !Number.isNaN(Number(yrs))) payload.years = Number(yrs);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
} catch (e) { toastError(e); }
|
|
573
|
-
} }),
|
|
548
|
+
if (s) await put(`/api/skills/${s.id}`, payload);
|
|
549
|
+
else await post('/api/skills', payload);
|
|
550
|
+
toastOk(s ? '기술을 수정했어요.' : '기술을 추가했어요.');
|
|
551
|
+
close();
|
|
552
|
+
await reload();
|
|
553
|
+
}),
|
|
574
554
|
],
|
|
575
555
|
});
|
|
576
556
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Settings — data location, connection, theme, backup/export, and danger zone.
|
|
2
2
|
// Everything stays local; this page surfaces where data lives and how to manage it.
|
|
3
3
|
import {
|
|
4
|
-
el, get, post, icon, navigate, Card,
|
|
4
|
+
el, get, post, icon, navigate, Card, Btn, IconBtn, Input,
|
|
5
5
|
openModal, copyText, downloadUrl, fmtRelative, toastOk, toastError, mount,
|
|
6
6
|
} from '/lib.js';
|
|
7
7
|
|
|
8
8
|
const COUNT_LABELS = {
|
|
9
|
-
profile: '프로필', experiences: '경력', projects: '프로젝트', skills: '
|
|
9
|
+
profile: '프로필', experiences: '경력', projects: '프로젝트', skills: '기술',
|
|
10
10
|
documents: '문서', cover_letters: '자기소개서', jobs: '채용공고',
|
|
11
11
|
applications: '지원', interview_preps: '면접 준비',
|
|
12
12
|
};
|
|
@@ -90,7 +90,8 @@ function Connection(info) {
|
|
|
90
90
|
|
|
91
91
|
/* ------------------------------------------------------------- 테마 */
|
|
92
92
|
function currentTheme() {
|
|
93
|
-
|
|
93
|
+
// New key with legacy 'cf-theme' fallback so a saved theme survives the rename.
|
|
94
|
+
const saved = localStorage.getItem('careermate-theme') || localStorage.getItem('cf-theme');
|
|
94
95
|
return saved === 'light' || saved === 'dark' ? saved : 'system';
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -106,10 +107,12 @@ function Theme() {
|
|
|
106
107
|
function applyTheme(value) {
|
|
107
108
|
if (value === 'system') {
|
|
108
109
|
delete document.documentElement.dataset.theme;
|
|
109
|
-
localStorage.removeItem('
|
|
110
|
+
localStorage.removeItem('careermate-theme');
|
|
111
|
+
localStorage.removeItem('cf-theme'); // also clear legacy key
|
|
110
112
|
} else {
|
|
111
113
|
document.documentElement.dataset.theme = value;
|
|
112
|
-
localStorage.setItem('
|
|
114
|
+
localStorage.setItem('careermate-theme', value);
|
|
115
|
+
localStorage.removeItem('cf-theme'); // migrate off legacy key
|
|
113
116
|
}
|
|
114
117
|
paint();
|
|
115
118
|
}
|
|
@@ -134,12 +137,14 @@ function Theme() {
|
|
|
134
137
|
|
|
135
138
|
/* ---------------------------------------------------------- 내 데이터 */
|
|
136
139
|
function MyData(counts = {}) {
|
|
137
|
-
const
|
|
138
|
-
|
|
140
|
+
const items = Object.entries(COUNT_LABELS).map(([key, label]) =>
|
|
141
|
+
el('div', { class: 'countstrip__item' },
|
|
142
|
+
el('div', { class: 'countstrip__num tnum' }, String(counts[key] ?? 0)),
|
|
143
|
+
el('div', { class: 'countstrip__label' }, label)));
|
|
139
144
|
return Card({
|
|
140
145
|
title: '내 데이터',
|
|
141
146
|
sub: '이 컴퓨터에 저장된 항목 수',
|
|
142
|
-
body: el('div', { class: '
|
|
147
|
+
body: el('div', { class: 'countstrip' }, ...items),
|
|
143
148
|
});
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -148,7 +153,7 @@ function Backup(backups = [], rerender) {
|
|
|
148
153
|
async function createBackup() {
|
|
149
154
|
try {
|
|
150
155
|
const res = await post('/api/settings/backup');
|
|
151
|
-
toastOk('백업을
|
|
156
|
+
toastOk('백업을 생성했어요.');
|
|
152
157
|
await rerender();
|
|
153
158
|
} catch (e) {
|
|
154
159
|
toastError(e);
|
|
@@ -179,10 +184,12 @@ function Backup(backups = [], rerender) {
|
|
|
179
184
|
function DangerZone(ctx) {
|
|
180
185
|
function openResetModal() {
|
|
181
186
|
let confirmBtn;
|
|
187
|
+
// Gate on the Korean word so there's no IME switch and it matches the '초기화' button.
|
|
188
|
+
// (The API still expects the fixed 'DELETE' token; we send that on the wire below.)
|
|
182
189
|
const input = Input({
|
|
183
|
-
placeholder: '
|
|
184
|
-
onInput: (e) => { if (confirmBtn) confirmBtn.disabled = e.target.value.trim() !== '
|
|
185
|
-
onKeydown: (e) => { if (e.key === 'Enter' && e.target.value.trim() === '
|
|
190
|
+
placeholder: '초기화',
|
|
191
|
+
onInput: (e) => { if (confirmBtn) confirmBtn.disabled = e.target.value.trim() !== '초기화'; },
|
|
192
|
+
onKeydown: (e) => { if (e.key === 'Enter' && e.target.value.trim() === '초기화' && confirmBtn && !confirmBtn.disabled) confirmBtn.click(); },
|
|
186
193
|
});
|
|
187
194
|
|
|
188
195
|
openModal({
|
|
@@ -195,7 +202,7 @@ function DangerZone(ctx) {
|
|
|
195
202
|
el('div', { class: 'callout__body' },
|
|
196
203
|
'프로필·공고·지원·문서·면접 준비를 포함한 모든 데이터가 영구적으로 삭제됩니다. 안전을 위해 초기화 직전에 자동으로 백업이 한 번 생성됩니다.'))),
|
|
197
204
|
el('p', { class: 'text-secondary', style: { margin: 0, lineHeight: '1.6' } },
|
|
198
|
-
'계속하려면 아래 입력란에 ', el('span', { class: 'strong' }, '
|
|
205
|
+
'계속하려면 아래 입력란에 ', el('span', { class: 'strong' }, '초기화'), ' 를 입력하세요.'),
|
|
199
206
|
input),
|
|
200
207
|
footer: (close) => {
|
|
201
208
|
confirmBtn = Btn('모든 데이터 초기화', {
|
|
@@ -206,7 +213,7 @@ function DangerZone(ctx) {
|
|
|
206
213
|
try {
|
|
207
214
|
await post('/api/settings/reset', { confirm: 'DELETE' });
|
|
208
215
|
close();
|
|
209
|
-
toastOk('모든
|
|
216
|
+
toastOk('모든 데이터를 초기화했어요.');
|
|
210
217
|
navigate('/');
|
|
211
218
|
await ctx.refreshNav();
|
|
212
219
|
} catch (e) {
|
package/dist/public/styles.css
CHANGED
|
@@ -19,24 +19,24 @@
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
:root {
|
|
22
|
-
/*
|
|
23
|
-
--bg: #
|
|
24
|
-
--bg-subtle: #
|
|
25
|
-
--bg-sunken: #
|
|
22
|
+
/* Warm paper neutrals (light) — unified with the public landing page */
|
|
23
|
+
--bg: #faf8f4;
|
|
24
|
+
--bg-subtle: #f2efe8;
|
|
25
|
+
--bg-sunken: #ece8df;
|
|
26
26
|
--surface: #ffffff;
|
|
27
|
-
--surface-hover: #
|
|
28
|
-
--border: #
|
|
29
|
-
--border-strong: #
|
|
30
|
-
--text: #
|
|
31
|
-
--text-secondary: #
|
|
32
|
-
--text-tertiary: #
|
|
27
|
+
--surface-hover: #f5f2ec;
|
|
28
|
+
--border: #e6e1d6;
|
|
29
|
+
--border-strong: #d6d0c2;
|
|
30
|
+
--text: #1c1a16;
|
|
31
|
+
--text-secondary: #595449;
|
|
32
|
+
--text-tertiary: #8b867a;
|
|
33
33
|
--text-on-accent: #ffffff;
|
|
34
34
|
|
|
35
|
-
/* Accent —
|
|
36
|
-
--accent: #
|
|
37
|
-
--accent-hover: #
|
|
38
|
-
--accent-soft: #
|
|
39
|
-
--accent-border: #
|
|
35
|
+
/* Accent — deep emerald (single flat accent, no gradient) */
|
|
36
|
+
--accent: #0f7256;
|
|
37
|
+
--accent-hover: #0b5b44;
|
|
38
|
+
--accent-soft: #e7f1ec;
|
|
39
|
+
--accent-border: #bcdccd;
|
|
40
40
|
|
|
41
41
|
/* Semantic */
|
|
42
42
|
--green: #15803d; --green-soft: #ecfdf3; --green-border: #bbf7d0;
|
|
@@ -67,31 +67,38 @@
|
|
|
67
67
|
|
|
68
68
|
@media (prefers-color-scheme: dark) {
|
|
69
69
|
:root:not([data-theme='light']) {
|
|
70
|
-
--bg: #
|
|
71
|
-
--bg-subtle: #
|
|
72
|
-
--bg-sunken: #
|
|
73
|
-
--surface: #
|
|
74
|
-
--surface-hover: #
|
|
75
|
-
--border: #
|
|
76
|
-
--border-strong: #
|
|
77
|
-
--text: #
|
|
78
|
-
--text-secondary: #
|
|
79
|
-
--text-tertiary: #
|
|
80
|
-
--accent: #
|
|
81
|
-
--accent-hover: #
|
|
82
|
-
--accent-soft: #
|
|
83
|
-
--accent-border: #
|
|
70
|
+
--bg: #14130f;
|
|
71
|
+
--bg-subtle: #201d17;
|
|
72
|
+
--bg-sunken: #100f0b;
|
|
73
|
+
--surface: #1b1915;
|
|
74
|
+
--surface-hover: #262219;
|
|
75
|
+
--border: #2e2a22;
|
|
76
|
+
--border-strong: #3e3930;
|
|
77
|
+
--text: #f2eee5;
|
|
78
|
+
--text-secondary: #aaa599;
|
|
79
|
+
--text-tertiary: #79746a;
|
|
80
|
+
--accent: #34d399;
|
|
81
|
+
--accent-hover: #6ee7b7;
|
|
82
|
+
--accent-soft: #11281d;
|
|
83
|
+
--accent-border: #1f4f3c;
|
|
84
|
+
--text-on-accent: #05241a;
|
|
84
85
|
--green-soft:#0e2a1a; --teal-soft:#0c2826; --blue-soft:#11203f; --violet-soft:#1e1638;
|
|
85
86
|
--amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b;
|
|
87
|
+
/* lighter semantic foregrounds so status badges stay legible on dark soft bg */
|
|
88
|
+
--green:#4ade80; --teal:#2dd4bf; --blue:#60a5fa; --violet:#a78bfa; --amber:#fbbf24; --red:#ef4444; --slate:#94a3b8;
|
|
89
|
+
--green-border:#1f4f3c; --teal-border:#155e57; --blue-border:#1e3a6b; --violet-border:#3b2f6b; --amber-border:#5c4410; --red-border:#5c2020; --slate-border:#3a4250;
|
|
86
90
|
--shadow-lg: 0 12px 40px rgba(0,0,0,.5);
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
:root[data-theme='dark'] {
|
|
90
|
-
--bg:#
|
|
91
|
-
--border:#
|
|
92
|
-
--accent:#
|
|
94
|
+
--bg:#14130f; --bg-subtle:#201d17; --bg-sunken:#100f0b; --surface:#1b1915; --surface-hover:#262219;
|
|
95
|
+
--border:#2e2a22; --border-strong:#3e3930; --text:#f2eee5; --text-secondary:#aaa599; --text-tertiary:#79746a;
|
|
96
|
+
--accent:#34d399; --accent-hover:#6ee7b7; --accent-soft:#11281d; --accent-border:#1f4f3c; --text-on-accent:#05241a;
|
|
93
97
|
--green-soft:#0e2a1a; --teal-soft:#0c2826; --blue-soft:#11203f; --violet-soft:#1e1638;
|
|
94
|
-
--amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b;
|
|
98
|
+
--amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b;
|
|
99
|
+
--green:#4ade80; --teal:#2dd4bf; --blue:#60a5fa; --violet:#a78bfa; --amber:#fbbf24; --red:#ef4444; --slate:#94a3b8;
|
|
100
|
+
--green-border:#1f4f3c; --teal-border:#155e57; --blue-border:#1e3a6b; --violet-border:#3b2f6b; --amber-border:#5c4410; --red-border:#5c2020; --slate-border:#3a4250;
|
|
101
|
+
--shadow-lg:0 12px 40px rgba(0,0,0,.5);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
* { box-sizing: border-box; }
|
|
@@ -120,7 +127,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
120
127
|
.sidebar__logo {
|
|
121
128
|
width: 30px; height: 30px; border-radius: 8px; flex: none;
|
|
122
129
|
background: var(--accent);
|
|
123
|
-
display: grid; place-items: center; color:
|
|
130
|
+
display: grid; place-items: center; color: var(--text-on-accent); font-weight: 800; font-size: 15px;
|
|
124
131
|
}
|
|
125
132
|
.sidebar__title { font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
|
|
126
133
|
.sidebar__subtitle { font-size: 12px; color: var(--text-tertiary); margin-top: 1px; }
|
|
@@ -135,7 +142,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
135
142
|
.nav__item svg { width: 17px; height: 17px; flex: none; }
|
|
136
143
|
.nav__badge { margin-left: auto; font-size: 11.5px; background: var(--bg-sunken); color: var(--text-secondary);
|
|
137
144
|
padding: 1px 7px; border-radius: 999px; font-weight: 600; }
|
|
138
|
-
.nav__item.is-active .nav__badge { background: var(--accent); color:
|
|
145
|
+
.nav__item.is-active .nav__badge { background: var(--accent); color: var(--text-on-accent); }
|
|
139
146
|
.sidebar__foot { padding: 12px 16px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-tertiary); }
|
|
140
147
|
.sidebar__foot .dot { display:inline-block; width:7px; height:7px; border-radius:999px; background: var(--green); margin-right:6px; }
|
|
141
148
|
|
|
@@ -148,6 +155,13 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
148
155
|
.topbar__title { font-size: 15px; font-weight: 650; }
|
|
149
156
|
.topbar__crumb { color: var(--text-tertiary); }
|
|
150
157
|
.topbar__spacer { flex: 1; }
|
|
158
|
+
/* mobile nav: hamburger (hidden ≥900px) + off-canvas drawer scrim */
|
|
159
|
+
.nav-toggle { display: none; width: 34px; height: 34px; flex: none; place-items: center;
|
|
160
|
+
border: 1px solid var(--border); background: var(--surface); border-radius: var(--radius-sm);
|
|
161
|
+
color: var(--text-secondary); cursor: pointer; margin-right: 4px; }
|
|
162
|
+
.nav-toggle:hover { background: var(--surface-hover); color: var(--text); }
|
|
163
|
+
.nav-toggle svg { width: 20px; height: 20px; }
|
|
164
|
+
.nav-scrim { display: none; position: fixed; inset: 0; z-index: 40; background: rgba(0,0,0,.4); }
|
|
151
165
|
.view { padding: 24px; max-width: var(--maxw); width: 100%; margin: 0 auto; }
|
|
152
166
|
.view--wide { max-width: 1280px; }
|
|
153
167
|
|
|
@@ -161,7 +175,16 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
161
175
|
.grid--2 { grid-template-columns: repeat(2, 1fr); }
|
|
162
176
|
.grid--3 { grid-template-columns: repeat(3, 1fr); }
|
|
163
177
|
.grid--4 { grid-template-columns: repeat(4, 1fr); }
|
|
164
|
-
@media (max-width: 900px){
|
|
178
|
+
@media (max-width: 900px){
|
|
179
|
+
.grid--2,.grid--3,.grid--4{ grid-template-columns: 1fr; }
|
|
180
|
+
.app{ grid-template-columns: 1fr; }
|
|
181
|
+
/* sidebar slides in as an off-canvas drawer; the topbar hamburger toggles it */
|
|
182
|
+
.sidebar{ position: fixed; top: 0; left: 0; bottom: 0; height: 100%; z-index: 50;
|
|
183
|
+
transform: translateX(-100%); transition: transform .22s ease; box-shadow: var(--shadow-lg); }
|
|
184
|
+
.app.nav-open .sidebar{ transform: none; }
|
|
185
|
+
.app.nav-open .nav-scrim{ display: block; }
|
|
186
|
+
.nav-toggle{ display: inline-grid; }
|
|
187
|
+
}
|
|
165
188
|
|
|
166
189
|
/* ----------------------------------------------------------------- card */
|
|
167
190
|
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
|
|
@@ -184,6 +207,12 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
184
207
|
.stat__label svg { width: 15px; height: 15px; color: var(--text-tertiary); }
|
|
185
208
|
.stat__value { font-size: 26px; font-weight: 700; letter-spacing: -.02em; margin-top: 6px; }
|
|
186
209
|
.stat__hint { font-size: 12.5px; color: var(--text-tertiary); margin-top: 2px; }
|
|
210
|
+
/* borderless count strip (settings 내 데이터) — flat counts, never a card-in-card */
|
|
211
|
+
.countstrip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px 24px; }
|
|
212
|
+
.countstrip__item { display: flex; flex-direction: column; gap: 2px; }
|
|
213
|
+
.countstrip__num { font-size: 24px; font-weight: 700; letter-spacing: -.02em; }
|
|
214
|
+
.countstrip__label { font-size: 13px; color: var(--text-secondary); }
|
|
215
|
+
@media (max-width: 560px){ .countstrip { grid-template-columns: repeat(2, 1fr); } }
|
|
187
216
|
|
|
188
217
|
/* ----------------------------------------------------------------- buttons */
|
|
189
218
|
.btn {
|
|
@@ -195,7 +224,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
195
224
|
.btn:hover { background: var(--surface-hover); }
|
|
196
225
|
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
197
226
|
.btn svg { width: 15px; height: 15px; }
|
|
198
|
-
.btn--primary { background: var(--accent); border-color: var(--accent); color:
|
|
227
|
+
.btn--primary { background: var(--accent); border-color: var(--accent); color: var(--text-on-accent); }
|
|
199
228
|
.btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
200
229
|
.btn--ghost { border-color: transparent; background: transparent; }
|
|
201
230
|
.btn--ghost:hover { background: var(--surface-hover); }
|
|
@@ -216,7 +245,14 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
216
245
|
background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
|
|
217
246
|
padding: 8px 11px; transition: border-color .12s, box-shadow .12s;
|
|
218
247
|
}
|
|
219
|
-
.input:focus, .textarea:focus, .select:focus {
|
|
248
|
+
.input:focus, .textarea:focus, .select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
249
|
+
/* keyboard focus must be clearly visible (the soft ring alone fails WCAG 1.4.11/2.4.11) */
|
|
250
|
+
.input:focus-visible, .textarea:focus-visible, .select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; box-shadow: none; }
|
|
251
|
+
/* skip link — hidden until focused, then drops into the top-left */
|
|
252
|
+
.skip-link { position: fixed; top: 8px; left: 8px; z-index: 200; transform: translateY(-200%);
|
|
253
|
+
background: var(--accent); color: var(--text-on-accent); padding: 8px 14px; border: 0;
|
|
254
|
+
border-radius: var(--radius-sm); font: inherit; font-weight: 600; cursor: pointer; transition: transform .15s; }
|
|
255
|
+
.skip-link:focus { transform: none; outline: 2px solid var(--text-on-accent); outline-offset: 2px; }
|
|
220
256
|
.textarea { resize: vertical; min-height: 90px; line-height: 1.6; }
|
|
221
257
|
.textarea--lg { min-height: 280px; font-size: 15px; }
|
|
222
258
|
.input::placeholder, .textarea::placeholder { color: var(--text-tertiary); }
|
|
@@ -255,6 +291,9 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
255
291
|
.table tbody tr.is-clickable:hover { background: var(--surface-hover); }
|
|
256
292
|
.table .muted { color: var(--text-tertiary); }
|
|
257
293
|
.table .strong { font-weight: 600; }
|
|
294
|
+
/* keyboard-reachable lead-cell link that looks identical to plain text */
|
|
295
|
+
.cell-link { color: inherit; text-decoration: none; }
|
|
296
|
+
.cell-link:hover { text-decoration: none; }
|
|
258
297
|
|
|
259
298
|
/* ----------------------------------------------------------------- progress */
|
|
260
299
|
.progress { height: 7px; background: var(--bg-sunken); border-radius: 999px; overflow: hidden; }
|
|
@@ -377,6 +416,9 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
377
416
|
.modal__head { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; }
|
|
378
417
|
.modal__head h3 { font-size: 15px; }
|
|
379
418
|
.modal__body { padding: 20px; overflow-y: auto; }
|
|
419
|
+
.modal__error { margin: 0 20px 4px; padding: 10px 12px; border-radius: var(--radius); border: 1px solid var(--red-border);
|
|
420
|
+
background: var(--red-soft); color: var(--red); font-size: 13px; line-height: 1.5; }
|
|
421
|
+
.modal__error[hidden] { display: none; }
|
|
380
422
|
.modal__foot { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
|
|
381
423
|
@keyframes pop { from { transform: translate(-50%,-46%); opacity: 0; } to { transform: translate(-50%,-50%); opacity: 1; } }
|
|
382
424
|
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
|
|
@@ -413,6 +455,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
413
455
|
.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; }
|
|
414
456
|
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
|
|
415
457
|
.hide { display: none !important; }
|
|
458
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
416
459
|
|
|
417
460
|
/* score color helper */
|
|
418
461
|
.score-strong { color: var(--green); } .score-mid { color: var(--amber); } .score-weak { color: var(--red); }
|
|
@@ -424,3 +467,27 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
424
467
|
.callout--privacy svg { color: var(--text-tertiary); }
|
|
425
468
|
.callout__title { font-weight: 600; margin-bottom: 2px; }
|
|
426
469
|
.callout__body { color: var(--text-secondary); font-size: 13px; }
|
|
470
|
+
|
|
471
|
+
/* ----------------------------------------------------------------- phone layout */
|
|
472
|
+
/* Placed last so these win over the base widths (media queries add no specificity). */
|
|
473
|
+
@media (max-width: 640px){
|
|
474
|
+
.topbar{ padding: 0 12px; gap: 8px; }
|
|
475
|
+
/* title takes the room between hamburger and actions and truncates instead of
|
|
476
|
+
collapsing to vertical text; the flex spacer isn't needed at this width */
|
|
477
|
+
.topbar__title{ flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
478
|
+
.topbar__spacer{ display: none; }
|
|
479
|
+
.input--inline{ width: 110px; max-width: 32vw; }
|
|
480
|
+
.view{ padding: 16px; }
|
|
481
|
+
/* jobs table: drop the secondary 위치·마감일 column so 회사·직무·적합도·상태 all fit */
|
|
482
|
+
.table--jobs th:nth-child(2), .table--jobs td:nth-child(2){ display: none; }
|
|
483
|
+
/* keep the 적합도 score ("88점") on one line in the narrow column */
|
|
484
|
+
.table--jobs th:nth-child(3), .table--jobs td:nth-child(3){ white-space: nowrap; }
|
|
485
|
+
.table--jobs td:nth-child(3) .tnum{ font-size: 13px; }
|
|
486
|
+
/* any remaining wide table can still scroll instead of crushing columns */
|
|
487
|
+
.table-scroll{ overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
488
|
+
/* stack key/value so long emails·paths·URLs aren't clipped by the fixed label column */
|
|
489
|
+
.kv{ grid-template-columns: 1fr; gap: 0; }
|
|
490
|
+
.kv dt{ margin-top: 10px; }
|
|
491
|
+
.kv dd{ margin: 0 0 2px; }
|
|
492
|
+
.page-head{ flex-wrap: wrap; }
|
|
493
|
+
}
|