careermate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/apps/mcp/src/index.ts +66 -0
- package/apps/web/DESIGN_GUIDE.md +105 -0
- package/apps/web/UI_CONTRACT.md +44 -0
- package/apps/web/public/app.js +118 -0
- package/apps/web/public/fonts/PretendardVariable.woff2 +0 -0
- package/apps/web/public/index.html +41 -0
- package/apps/web/public/lib.js +282 -0
- package/apps/web/public/pages/applications.js +98 -0
- package/apps/web/public/pages/documents.js +446 -0
- package/apps/web/public/pages/home.js +263 -0
- package/apps/web/public/pages/interview.js +230 -0
- package/apps/web/public/pages/jobs.js +494 -0
- package/apps/web/public/pages/profile.js +576 -0
- package/apps/web/public/pages/settings.js +233 -0
- package/apps/web/public/styles.css +426 -0
- package/apps/web/src/exports.ts +68 -0
- package/apps/web/src/http.ts +180 -0
- package/apps/web/src/index.ts +49 -0
- package/apps/web/src/info.ts +50 -0
- package/apps/web/src/routes.ts +350 -0
- package/apps/web/src/security.ts +102 -0
- package/apps/web/src/server.ts +141 -0
- package/apps/web/src/settings.ts +88 -0
- package/bin/careermate.mjs +74 -0
- package/dist/careermate.mcpb +0 -0
- package/dist/install-page/index.html +474 -0
- package/dist/install-page/style.css +391 -0
- package/dist/install-page/vercel.json +20 -0
- package/dist/mcp-smoke.err +3 -0
- package/dist/mcp.mjs +23704 -0
- package/dist/mcpb-stage/README.md +219 -0
- package/dist/mcpb-stage/dist/install-page/index.html +434 -0
- package/dist/mcpb-stage/dist/install-page/style.css +407 -0
- package/dist/mcpb-stage/dist/install-page/vercel.json +20 -0
- package/dist/mcpb-stage/dist/mcp.mjs +23704 -0
- package/dist/mcpb-stage/dist/public/app.js +118 -0
- package/dist/mcpb-stage/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/mcpb-stage/dist/public/index.html +41 -0
- package/dist/mcpb-stage/dist/public/lib.js +282 -0
- package/dist/mcpb-stage/dist/public/pages/applications.js +98 -0
- package/dist/mcpb-stage/dist/public/pages/documents.js +446 -0
- package/dist/mcpb-stage/dist/public/pages/home.js +263 -0
- package/dist/mcpb-stage/dist/public/pages/interview.js +230 -0
- package/dist/mcpb-stage/dist/public/pages/jobs.js +494 -0
- package/dist/mcpb-stage/dist/public/pages/profile.js +576 -0
- package/dist/mcpb-stage/dist/public/pages/settings.js +233 -0
- package/dist/mcpb-stage/dist/public/styles.css +420 -0
- package/dist/mcpb-stage/dist/web.mjs +7240 -0
- package/dist/mcpb-stage/manifest.json +40 -0
- package/dist/public/app.js +118 -0
- package/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/public/index.html +41 -0
- package/dist/public/lib.js +282 -0
- package/dist/public/pages/applications.js +98 -0
- package/dist/public/pages/documents.js +446 -0
- package/dist/public/pages/home.js +263 -0
- package/dist/public/pages/interview.js +230 -0
- package/dist/public/pages/jobs.js +494 -0
- package/dist/public/pages/profile.js +576 -0
- package/dist/public/pages/settings.js +233 -0
- package/dist/public/styles.css +426 -0
- package/dist/web.mjs +7240 -0
- package/docs/ARCHITECTURE.md +208 -0
- package/docs/CHANGES_V1.md +103 -0
- package/docs/DATA_MODEL.md +460 -0
- package/docs/DECISIONS.md +277 -0
- package/docs/DEMO.md +242 -0
- package/docs/INSTALL.md +148 -0
- package/docs/INSTALL_AND_USAGE.md +99 -0
- package/docs/MCP_TOOLS.md +233 -0
- package/docs/ROADMAP.md +134 -0
- package/docs/START_WORKFLOW.md +125 -0
- package/docs/SUPPORTED_AI_APPS.md +60 -0
- package/docs/TODO.md +57 -0
- package/docs/UX_NOTES.md +247 -0
- package/docs/WORKFLOWS.md +200 -0
- package/install-page/index.html +474 -0
- package/install-page/style.css +391 -0
- package/install-page/vercel.json +20 -0
- package/package.json +68 -0
- package/packages/core/src/context.ts +74 -0
- package/packages/core/src/index.ts +8 -0
- package/packages/core/src/onboarding.ts +81 -0
- package/packages/core/src/services.ts +146 -0
- package/packages/core/src/summary.ts +104 -0
- package/packages/db/src/connection.ts +46 -0
- package/packages/db/src/index.ts +22 -0
- package/packages/db/src/paths.ts +41 -0
- package/packages/db/src/repositories.ts +828 -0
- package/packages/db/src/runtime.ts +58 -0
- package/packages/db/src/schema.ts +189 -0
- package/packages/exporters/src/html.ts +113 -0
- package/packages/exporters/src/index.ts +364 -0
- package/packages/exporters/src/markdown.ts +178 -0
- package/packages/mcp-tools/src/bridge.ts +83 -0
- package/packages/mcp-tools/src/index.ts +8 -0
- package/packages/mcp-tools/src/result.ts +49 -0
- package/packages/mcp-tools/src/tools.ts +455 -0
- package/packages/parsers/src/html.ts +86 -0
- package/packages/parsers/src/index.ts +228 -0
- package/packages/parsers/src/keywords.ts +151 -0
- package/packages/prompts/src/humanize.ts +59 -0
- package/packages/prompts/src/index.ts +82 -0
- package/packages/prompts/src/install.ts +43 -0
- package/packages/prompts/src/onboarding.ts +35 -0
- package/packages/prompts/src/system.ts +53 -0
- package/packages/shared/src/enums.ts +103 -0
- package/packages/shared/src/index.ts +18 -0
- package/packages/shared/src/schemas.ts +398 -0
- package/packages/workflows/src/definitions.ts +107 -0
- package/packages/workflows/src/index.ts +39 -0
- package/scripts/build-dist.mjs +62 -0
- package/scripts/build-mcpb.mjs +70 -0
- package/scripts/doctor.ts +81 -0
- package/scripts/init.ts +342 -0
- package/scripts/mcp-probe.ts +55 -0
- package/scripts/migrate.ts +6 -0
- package/scripts/run.mjs +33 -0
- package/scripts/seed.ts +129 -0
- package/scripts/test.ts +117 -0
- package/scripts/ui-smoke.ts +73 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime handshake between the two local processes.
|
|
3
|
+
*
|
|
4
|
+
* The web server and the MCP server run independently (the MCP server is
|
|
5
|
+
* launched by the AI client). They find each other through a small JSON file in
|
|
6
|
+
* the data dir: the web server writes its live URL/port/pid on boot; the MCP
|
|
7
|
+
* server reads it so tools like `open_dashboard` can point the browser at the
|
|
8
|
+
* right place — without any network discovery.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { getDataDir } from './paths.ts';
|
|
13
|
+
|
|
14
|
+
export interface RuntimeInfo {
|
|
15
|
+
url: string;
|
|
16
|
+
port: number;
|
|
17
|
+
pid: number;
|
|
18
|
+
started_at: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runtimeFile(): string {
|
|
22
|
+
return path.join(getDataDir(), 'server.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeRuntimeInfo(info: RuntimeInfo): void {
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(runtimeFile(), JSON.stringify(info, null, 2), 'utf8');
|
|
28
|
+
} catch {
|
|
29
|
+
/* non-fatal */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readRuntimeInfo(): RuntimeInfo | null {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(runtimeFile(), 'utf8');
|
|
36
|
+
return JSON.parse(raw) as RuntimeInfo;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True if a process with this pid is currently alive. */
|
|
43
|
+
export function isProcessAlive(pid: number): boolean {
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return (e as NodeJS.ErrnoException).code === 'EPERM';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function clearRuntimeInfo(): void {
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(runtimeFile());
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite schema + lightweight migration runner.
|
|
3
|
+
*
|
|
4
|
+
* Migrations are an ordered list of SQL strings. The current version is tracked
|
|
5
|
+
* in `_meta`. Applying is idempotent and safe to run on every process start, so
|
|
6
|
+
* both the web server and the MCP server can call `migrate()` on boot and share
|
|
7
|
+
* the same database file without coordination.
|
|
8
|
+
*/
|
|
9
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
10
|
+
|
|
11
|
+
export const MIGRATIONS: string[] = [
|
|
12
|
+
// v1 — initial schema
|
|
13
|
+
`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS profile (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
name TEXT, email TEXT, phone TEXT, location TEXT,
|
|
17
|
+
headline TEXT, summary TEXT,
|
|
18
|
+
desired_roles TEXT NOT NULL DEFAULT '[]',
|
|
19
|
+
desired_conditions TEXT,
|
|
20
|
+
preferred_tone TEXT,
|
|
21
|
+
emphasis_points TEXT NOT NULL DEFAULT '[]',
|
|
22
|
+
links TEXT NOT NULL DEFAULT '[]',
|
|
23
|
+
created_at TEXT NOT NULL,
|
|
24
|
+
updated_at TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS experiences (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
company TEXT NOT NULL,
|
|
30
|
+
role TEXT,
|
|
31
|
+
employment_type TEXT,
|
|
32
|
+
start_date TEXT,
|
|
33
|
+
end_date TEXT,
|
|
34
|
+
is_current INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
description TEXT,
|
|
36
|
+
achievements TEXT NOT NULL DEFAULT '[]',
|
|
37
|
+
tech TEXT NOT NULL DEFAULT '[]',
|
|
38
|
+
order_index INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
created_at TEXT NOT NULL,
|
|
40
|
+
updated_at TEXT NOT NULL
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
name TEXT NOT NULL,
|
|
46
|
+
role TEXT,
|
|
47
|
+
description TEXT,
|
|
48
|
+
highlights TEXT NOT NULL DEFAULT '[]',
|
|
49
|
+
tech TEXT NOT NULL DEFAULT '[]',
|
|
50
|
+
url TEXT,
|
|
51
|
+
start_date TEXT,
|
|
52
|
+
end_date TEXT,
|
|
53
|
+
order_index INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
created_at TEXT NOT NULL,
|
|
55
|
+
updated_at TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
name TEXT NOT NULL,
|
|
61
|
+
category TEXT,
|
|
62
|
+
level TEXT,
|
|
63
|
+
years REAL,
|
|
64
|
+
order_index INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
created_at TEXT NOT NULL,
|
|
66
|
+
updated_at TEXT NOT NULL
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
70
|
+
id TEXT PRIMARY KEY,
|
|
71
|
+
kind TEXT NOT NULL,
|
|
72
|
+
title TEXT NOT NULL,
|
|
73
|
+
content TEXT NOT NULL DEFAULT '',
|
|
74
|
+
source TEXT NOT NULL DEFAULT 'manual',
|
|
75
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
77
|
+
created_at TEXT NOT NULL,
|
|
78
|
+
updated_at TEXT NOT NULL
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS cover_letters (
|
|
82
|
+
id TEXT PRIMARY KEY,
|
|
83
|
+
title TEXT NOT NULL,
|
|
84
|
+
job_id TEXT,
|
|
85
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
current_version_id TEXT,
|
|
87
|
+
created_at TEXT NOT NULL,
|
|
88
|
+
updated_at TEXT NOT NULL
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS cover_letter_versions (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
cover_letter_id TEXT NOT NULL,
|
|
94
|
+
version_no INTEGER NOT NULL,
|
|
95
|
+
content TEXT NOT NULL,
|
|
96
|
+
note TEXT,
|
|
97
|
+
source TEXT NOT NULL DEFAULT 'ai',
|
|
98
|
+
created_at TEXT NOT NULL
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
company TEXT NOT NULL,
|
|
104
|
+
position TEXT NOT NULL,
|
|
105
|
+
url TEXT,
|
|
106
|
+
location TEXT,
|
|
107
|
+
employment_type TEXT,
|
|
108
|
+
description TEXT,
|
|
109
|
+
requirements TEXT NOT NULL DEFAULT '[]',
|
|
110
|
+
keywords TEXT NOT NULL DEFAULT '[]',
|
|
111
|
+
deadline TEXT,
|
|
112
|
+
source TEXT,
|
|
113
|
+
created_at TEXT NOT NULL,
|
|
114
|
+
updated_at TEXT NOT NULL
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE IF NOT EXISTS fit_analyses (
|
|
118
|
+
id TEXT PRIMARY KEY,
|
|
119
|
+
job_id TEXT NOT NULL,
|
|
120
|
+
score REAL,
|
|
121
|
+
summary TEXT,
|
|
122
|
+
strengths TEXT NOT NULL DEFAULT '[]',
|
|
123
|
+
gaps TEXT NOT NULL DEFAULT '[]',
|
|
124
|
+
matched_keywords TEXT NOT NULL DEFAULT '[]',
|
|
125
|
+
missing_keywords TEXT NOT NULL DEFAULT '[]',
|
|
126
|
+
recommendations TEXT NOT NULL DEFAULT '[]',
|
|
127
|
+
created_at TEXT NOT NULL,
|
|
128
|
+
updated_at TEXT NOT NULL
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
CREATE TABLE IF NOT EXISTS applications (
|
|
132
|
+
id TEXT PRIMARY KEY,
|
|
133
|
+
job_id TEXT NOT NULL UNIQUE,
|
|
134
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
135
|
+
resume_id TEXT,
|
|
136
|
+
cover_letter_id TEXT,
|
|
137
|
+
applied_at TEXT,
|
|
138
|
+
notes TEXT,
|
|
139
|
+
created_at TEXT NOT NULL,
|
|
140
|
+
updated_at TEXT NOT NULL
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS interview_preps (
|
|
144
|
+
id TEXT PRIMARY KEY,
|
|
145
|
+
job_id TEXT NOT NULL UNIQUE,
|
|
146
|
+
questions TEXT NOT NULL DEFAULT '[]',
|
|
147
|
+
star_guides TEXT NOT NULL DEFAULT '[]',
|
|
148
|
+
self_introduction TEXT,
|
|
149
|
+
notes TEXT,
|
|
150
|
+
created_at TEXT NOT NULL,
|
|
151
|
+
updated_at TEXT NOT NULL
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
CREATE TABLE IF NOT EXISTS activities (
|
|
155
|
+
id TEXT PRIMARY KEY,
|
|
156
|
+
type TEXT NOT NULL,
|
|
157
|
+
entity_type TEXT,
|
|
158
|
+
entity_id TEXT,
|
|
159
|
+
summary TEXT NOT NULL,
|
|
160
|
+
created_at TEXT NOT NULL
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_fit_job ON fit_analyses(job_id);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_activities_created ON activities(created_at DESC);
|
|
167
|
+
`,
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
export function migrate(db: DatabaseSync): { from: number; to: number } {
|
|
171
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`);
|
|
172
|
+
const row = db.prepare(`SELECT value FROM _meta WHERE key = 'schema_version'`).get() as
|
|
173
|
+
| { value: string }
|
|
174
|
+
| undefined;
|
|
175
|
+
const from = row ? Number(row.value) : 0;
|
|
176
|
+
|
|
177
|
+
for (let v = from; v < MIGRATIONS.length; v++) {
|
|
178
|
+
db.exec(MIGRATIONS[v]!);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const to = MIGRATIONS.length;
|
|
182
|
+
if (to !== from) {
|
|
183
|
+
db.prepare(
|
|
184
|
+
`INSERT INTO _meta (key, value) VALUES ('schema_version', ?)
|
|
185
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
186
|
+
).run(String(to));
|
|
187
|
+
}
|
|
188
|
+
return { from, to };
|
|
189
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/exporters — HTML helpers.
|
|
3
|
+
*
|
|
4
|
+
* `escapeHtml` and `toPrintableHtml` produce a clean, standalone, A4
|
|
5
|
+
* print-optimized HTML document. The print CSS is what powers our "PDF" export:
|
|
6
|
+
* the user opens the HTML in a browser and chooses "Print → Save as PDF", so we
|
|
7
|
+
* never ship a binary PDF library.
|
|
8
|
+
*/
|
|
9
|
+
import { markdownToHtml } from './markdown.ts';
|
|
10
|
+
|
|
11
|
+
/** Escape the five HTML-significant characters. */
|
|
12
|
+
export function escapeHtml(input: string | null | undefined): string {
|
|
13
|
+
return (input ?? '')
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/</g, '<')
|
|
16
|
+
.replace(/>/g, '>')
|
|
17
|
+
.replace(/"/g, '"')
|
|
18
|
+
.replace(/'/g, ''');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Heuristic: does this string already look like an HTML fragment? */
|
|
22
|
+
function looksLikeHtml(s: string): boolean {
|
|
23
|
+
return /<\/?(p|h[1-6]|ul|ol|li|div|section|article|br|hr|strong|em|a|table)\b/i.test(s);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wrap a body (markdown OR ready HTML) in a complete, print-optimized HTML
|
|
28
|
+
* document. If `body` already looks like HTML it is used verbatim; otherwise it
|
|
29
|
+
* is rendered through the tiny markdown renderer.
|
|
30
|
+
*/
|
|
31
|
+
export function toPrintableHtml(title: string, bodyMarkdownOrHtml: string): string {
|
|
32
|
+
const body = looksLikeHtml(bodyMarkdownOrHtml)
|
|
33
|
+
? bodyMarkdownOrHtml
|
|
34
|
+
: markdownToHtml(bodyMarkdownOrHtml);
|
|
35
|
+
const safeTitle = escapeHtml(title);
|
|
36
|
+
|
|
37
|
+
return `<!doctype html>
|
|
38
|
+
<html lang="ko">
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="utf-8">
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
42
|
+
<title>${safeTitle}</title>
|
|
43
|
+
<style>
|
|
44
|
+
:root {
|
|
45
|
+
--ink: #1a1a1a;
|
|
46
|
+
--muted: #555;
|
|
47
|
+
--line: #d8d8d8;
|
|
48
|
+
--accent: #2b4a6f;
|
|
49
|
+
--maxw: 800px;
|
|
50
|
+
}
|
|
51
|
+
* { box-sizing: border-box; }
|
|
52
|
+
html, body {
|
|
53
|
+
margin: 0;
|
|
54
|
+
padding: 0;
|
|
55
|
+
color: var(--ink);
|
|
56
|
+
background: #f4f4f5;
|
|
57
|
+
font-family: "Pretendard", "Apple SD Gothic Neo", "Malgun Gothic",
|
|
58
|
+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
59
|
+
font-size: 14px;
|
|
60
|
+
line-height: 1.7;
|
|
61
|
+
-webkit-print-color-adjust: exact;
|
|
62
|
+
print-color-adjust: exact;
|
|
63
|
+
}
|
|
64
|
+
.page {
|
|
65
|
+
max-width: var(--maxw);
|
|
66
|
+
margin: 24px auto;
|
|
67
|
+
padding: 48px 56px;
|
|
68
|
+
background: #fff;
|
|
69
|
+
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
|
|
70
|
+
}
|
|
71
|
+
h1, h2, h3, h4, h5, h6 {
|
|
72
|
+
color: var(--accent);
|
|
73
|
+
line-height: 1.3;
|
|
74
|
+
margin: 1.4em 0 0.5em;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
}
|
|
77
|
+
h1 { font-size: 1.9em; margin-top: 0; border-bottom: 2px solid var(--accent); padding-bottom: 0.25em; }
|
|
78
|
+
h2 { font-size: 1.4em; border-bottom: 1px solid var(--line); padding-bottom: 0.2em; }
|
|
79
|
+
h3 { font-size: 1.15em; }
|
|
80
|
+
h4, h5, h6 { font-size: 1em; }
|
|
81
|
+
p { margin: 0.6em 0; }
|
|
82
|
+
ul, ol { margin: 0.5em 0 0.8em; padding-left: 1.4em; }
|
|
83
|
+
li { margin: 0.2em 0; }
|
|
84
|
+
a { color: var(--accent); text-decoration: none; }
|
|
85
|
+
a:hover { text-decoration: underline; }
|
|
86
|
+
strong { font-weight: 700; }
|
|
87
|
+
hr { border: none; border-top: 1px solid var(--line); margin: 1.5em 0; }
|
|
88
|
+
@page {
|
|
89
|
+
size: A4;
|
|
90
|
+
margin: 18mm 16mm;
|
|
91
|
+
}
|
|
92
|
+
@media print {
|
|
93
|
+
html, body { background: #fff; font-size: 12px; }
|
|
94
|
+
.page {
|
|
95
|
+
max-width: none;
|
|
96
|
+
margin: 0;
|
|
97
|
+
padding: 0;
|
|
98
|
+
box-shadow: none;
|
|
99
|
+
}
|
|
100
|
+
h1, h2, h3, h4, h5, h6 { page-break-after: avoid; }
|
|
101
|
+
ul, ol, p { page-break-inside: avoid; }
|
|
102
|
+
a { color: var(--ink); }
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
<main class="page">
|
|
108
|
+
${body}
|
|
109
|
+
</main>
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
112
|
+
`;
|
|
113
|
+
}
|