careermate 0.0.10 → 0.0.13
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/AGENTS.md +3 -2
- package/INSTALL.md +6 -6
- package/LICENSE +1 -1
- package/README.md +18 -16
- package/dist/career-os/README.md +2 -2
- package/dist/doctor.mjs +597 -59
- package/dist/mcp.mjs +624 -60
- package/dist/migrate.mjs +23 -6
- package/dist/public/app.js +1 -7
- package/dist/public/i18n/en/settings.js +1 -6
- package/dist/public/i18n/ko/settings.js +1 -6
- package/dist/public/index.html +0 -1
- package/dist/public/lib.js +13 -4
- package/dist/public/pages/documents.js +18 -5
- package/dist/public/pages/home.js +3 -1
- package/dist/public/pages/settings.js +13 -12
- package/dist/public/styles.css +30 -0
- package/dist/web.mjs +332 -17
- package/docs/FAQ.md +3 -4
- package/docs/LOCAL_FIRST.md +1 -1
- package/docs/MCP_TOOLS.md +6 -3
- package/docs/career-os/README.md +2 -2
- package/package.json +1 -8
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/job-categories.data.ts +140 -0
- package/packages/core/src/job-categories.ts +106 -0
- package/packages/core/src/jobsearch.ts +292 -21
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/interview-retrospectives.ts +2 -6
- package/packages/db/src/job-search.ts +100 -0
- package/packages/db/src/repositories.ts +1 -0
- package/packages/mcp-tools/src/tools.ts +115 -11
- package/scripts/init.ts +2 -0
package/dist/migrate.mjs
CHANGED
|
@@ -536,7 +536,7 @@ function ensureTable2() {
|
|
|
536
536
|
getDb().exec(`
|
|
537
537
|
CREATE TABLE IF NOT EXISTS interview_retrospectives (
|
|
538
538
|
id TEXT PRIMARY KEY,
|
|
539
|
-
job_id TEXT NOT NULL,
|
|
539
|
+
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
540
540
|
interview_round TEXT,
|
|
541
541
|
occurred_at TEXT,
|
|
542
542
|
questions TEXT NOT NULL DEFAULT '[]',
|
|
@@ -549,15 +549,32 @@ function ensureTable2() {
|
|
|
549
549
|
);
|
|
550
550
|
CREATE INDEX IF NOT EXISTS idx_interview_retrospectives_job
|
|
551
551
|
ON interview_retrospectives(job_id, occurred_at DESC, updated_at DESC);
|
|
552
|
-
|
|
553
|
-
AFTER DELETE ON jobs
|
|
554
|
-
BEGIN
|
|
555
|
-
DELETE FROM interview_retrospectives WHERE job_id = OLD.id;
|
|
556
|
-
END;
|
|
552
|
+
DROP TRIGGER IF EXISTS trg_interview_retrospectives_delete_job;
|
|
557
553
|
`);
|
|
558
554
|
}
|
|
559
555
|
ensureTable2();
|
|
560
556
|
|
|
557
|
+
// packages/db/src/job-search.ts
|
|
558
|
+
function ensureTables() {
|
|
559
|
+
getDb().exec(`
|
|
560
|
+
CREATE TABLE IF NOT EXISTS job_search_seen (
|
|
561
|
+
source TEXT NOT NULL,
|
|
562
|
+
external_id TEXT NOT NULL,
|
|
563
|
+
first_seen_at TEXT NOT NULL,
|
|
564
|
+
last_seen_at TEXT NOT NULL,
|
|
565
|
+
PRIMARY KEY (source, external_id)
|
|
566
|
+
);
|
|
567
|
+
CREATE TABLE IF NOT EXISTS job_search_preferences (
|
|
568
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
569
|
+
keyword TEXT NOT NULL DEFAULT '',
|
|
570
|
+
remember_categories TEXT NOT NULL DEFAULT '[]',
|
|
571
|
+
saramin_categories TEXT NOT NULL DEFAULT '[]',
|
|
572
|
+
updated_at TEXT NOT NULL
|
|
573
|
+
);
|
|
574
|
+
`);
|
|
575
|
+
}
|
|
576
|
+
ensureTables();
|
|
577
|
+
|
|
561
578
|
// packages/shared/src/enums.ts
|
|
562
579
|
var APPLICATION_STATUSES = [
|
|
563
580
|
"draft",
|
package/dist/public/app.js
CHANGED
|
@@ -24,10 +24,6 @@ const actionsEl = document.getElementById('topbar-actions');
|
|
|
24
24
|
const routeStatusEl = document.getElementById('route-status');
|
|
25
25
|
const moduleCache = new Map();
|
|
26
26
|
|
|
27
|
-
function removePrivateRepositoryLinks(root = document) {
|
|
28
|
-
root.querySelectorAll?.('a[href*="github.com/osntak/careermate"]').forEach((link) => link.remove());
|
|
29
|
-
}
|
|
30
|
-
|
|
31
27
|
function parseHash() {
|
|
32
28
|
const raw = location.hash.replace(/^#\/?/, '');
|
|
33
29
|
const segments = raw.split('/').filter(Boolean);
|
|
@@ -95,7 +91,6 @@ async function route() {
|
|
|
95
91
|
if (token !== renderToken) return;
|
|
96
92
|
clear(view);
|
|
97
93
|
await mod.render(ctx);
|
|
98
|
-
removePrivateRepositoryLinks(view);
|
|
99
94
|
if (routeStatusEl) routeStatusEl.textContent = titleEl.textContent || t('nav.' + def.id);
|
|
100
95
|
} catch (err) {
|
|
101
96
|
if (token !== renderToken) return;
|
|
@@ -142,7 +137,6 @@ function localizeShell() {
|
|
|
142
137
|
faq.href = en ? 'https://careermate.life/en/faq' : 'https://careermate.life/faq';
|
|
143
138
|
faq.title = en ? 'FAQ' : '자주 묻는 질문';
|
|
144
139
|
}
|
|
145
|
-
removePrivateRepositoryLinks();
|
|
146
140
|
}
|
|
147
141
|
|
|
148
142
|
async function boot() {
|
|
@@ -164,4 +158,4 @@ async function boot() {
|
|
|
164
158
|
await route();
|
|
165
159
|
}
|
|
166
160
|
|
|
167
|
-
boot();
|
|
161
|
+
boot();
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
export default {
|
|
3
3
|
// shared
|
|
4
4
|
'settings.copy': 'Copy',
|
|
5
|
-
'settings.confirm': 'OK',
|
|
6
5
|
'settings.cancel': 'Cancel',
|
|
7
6
|
'settings.copyPath': 'Copy path',
|
|
8
7
|
|
|
@@ -17,8 +16,6 @@ export default {
|
|
|
17
16
|
|
|
18
17
|
// connection
|
|
19
18
|
'settings.connection.title': 'Connection',
|
|
20
|
-
'settings.connection.shortcutDemoToast': "Here's a preview of how the shortcut works.",
|
|
21
|
-
'settings.connection.shortcutToast': 'Created a dashboard shortcut.',
|
|
22
19
|
'settings.connection.shortcutDemoTitle': 'Shortcut preview',
|
|
23
20
|
'settings.connection.shortcutTitle': 'Shortcut created',
|
|
24
21
|
'settings.connection.shortcutDemoCalloutTitle': "The demo doesn't create files",
|
|
@@ -123,8 +120,6 @@ export default {
|
|
|
123
120
|
|
|
124
121
|
// help / report a problem
|
|
125
122
|
'settings.support.title': 'Help & report a problem',
|
|
126
|
-
'settings.support.sub': 'Report bugs
|
|
127
|
-
'settings.support.report': 'Report a problem',
|
|
128
|
-
'settings.support.ask': 'Ask a question',
|
|
123
|
+
'settings.support.sub': 'Report bugs, questions, and feature requests by email. Do not include private career data in support messages.',
|
|
129
124
|
'settings.support.email': 'Email us',
|
|
130
125
|
};
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
export default {
|
|
3
3
|
// 공통
|
|
4
4
|
'settings.copy': '복사',
|
|
5
|
-
'settings.confirm': '확인',
|
|
6
5
|
'settings.cancel': '취소',
|
|
7
6
|
'settings.copyPath': '경로 복사',
|
|
8
7
|
|
|
@@ -17,8 +16,6 @@ export default {
|
|
|
17
16
|
|
|
18
17
|
// 연결 상태
|
|
19
18
|
'settings.connection.title': '연결 상태',
|
|
20
|
-
'settings.connection.shortcutDemoToast': '데모 바로가기 동작을 미리 보여드려요.',
|
|
21
|
-
'settings.connection.shortcutToast': '대시보드 바로가기를 만들었어요.',
|
|
22
19
|
'settings.connection.shortcutDemoTitle': '바로가기 미리보기',
|
|
23
20
|
'settings.connection.shortcutTitle': '바로가기 생성',
|
|
24
21
|
'settings.connection.shortcutDemoCalloutTitle': '데모에서는 파일을 만들지 않습니다',
|
|
@@ -123,8 +120,6 @@ export default {
|
|
|
123
120
|
|
|
124
121
|
// help / report a problem
|
|
125
122
|
'settings.support.title': '도움말·문제 신고',
|
|
126
|
-
'settings.support.sub': '
|
|
127
|
-
'settings.support.report': '문제 신고',
|
|
128
|
-
'settings.support.ask': '질문하기',
|
|
123
|
+
'settings.support.sub': '\ubc84\uadf8, \uc0ac\uc6a9\ubc95 \uc9c8\ubb38, \uae30\ub2a5 \uc694\uccad\uc740 \uc774\uba54\uc77c\ub85c \ubcf4\ub0b4 \uc8fc\uc138\uc694. \ubb38\uc758\uc5d0\ub294 \uac1c\uc778 \ucee4\ub9ac\uc5b4 \ub370\uc774\ud130\ub97c \ub123\uc9c0 \ub9c8\uc138\uc694.',
|
|
129
124
|
'settings.support.email': '이메일 문의',
|
|
130
125
|
};
|
package/dist/public/index.html
CHANGED
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
<div class="sidebar__foot">
|
|
29
29
|
<div><span class="dot"></span><span id="foot-status">로컬에서 실행 중</span></div>
|
|
30
30
|
<div id="foot-path" class="truncate" style="margin-top:4px"></div>
|
|
31
|
-
<a class="sidebar__foot-link" href="https://github.com/osntak/careermate" target="_blank" rel="noopener" title="GitHub에서 소스 보기"><svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg><span>GitHub</span></a>
|
|
32
31
|
<a class="sidebar__foot-link" id="foot-faq" href="https://careermate.life/faq" target="_blank" rel="noopener" title="자주 묻는 질문"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg><span>FAQ</span></a>
|
|
33
32
|
</div>
|
|
34
33
|
</aside>
|
package/dist/public/lib.js
CHANGED
|
@@ -411,6 +411,9 @@ export function Spinner() { return el('div', { class: 'view' }, el('div', { clas
|
|
|
411
411
|
|
|
412
412
|
/* ------------------------------------------------------------------ toast */
|
|
413
413
|
let toastRoot;
|
|
414
|
+
function clearToasts() {
|
|
415
|
+
if (toastRoot) clear(toastRoot);
|
|
416
|
+
}
|
|
414
417
|
export function toast(message, { title, type = 'default', timeout = 3800 } = {}) {
|
|
415
418
|
if (!toastRoot) { toastRoot = el('div', { class: 'toasts' }); document.body.append(toastRoot); }
|
|
416
419
|
const t = el('div', { class: `toast toast--${type}` },
|
|
@@ -436,22 +439,27 @@ function ensureModalRoot() {
|
|
|
436
439
|
}
|
|
437
440
|
export function closeModal() {
|
|
438
441
|
if (modalRoot) { modalRoot.classList.remove('is-open'); clear(modalRoot); }
|
|
442
|
+
delete document.body.dataset.modalIntent;
|
|
439
443
|
// Return focus to whatever opened the modal so keyboard users aren't dumped at the page top.
|
|
440
444
|
if (modalReturnFocus) { try { modalReturnFocus.focus(); } catch { /* element gone */ } modalReturnFocus = null; }
|
|
441
445
|
}
|
|
442
446
|
/**
|
|
443
447
|
* Open a modal. `render(close)` returns the modal body content. Provide title,
|
|
444
|
-
* optional footer buttons via `footer(close)`.
|
|
448
|
+
* optional footer buttons via `footer(close)`. `intent` only changes
|
|
449
|
+
* interruption weight; pages should not hand-roll dialog styles.
|
|
445
450
|
*/
|
|
446
|
-
export function openModal({ title, body, footer, size }) {
|
|
451
|
+
export function openModal({ title, body, footer, size, intent = 'dialog' }) {
|
|
447
452
|
const root = ensureModalRoot();
|
|
448
453
|
const close = closeModal;
|
|
454
|
+
if (intent === 'contextual' || intent === 'alert') clearToasts();
|
|
455
|
+
root.dataset.intent = intent;
|
|
456
|
+
document.body.dataset.modalIntent = intent;
|
|
449
457
|
modalReturnFocus = document.activeElement; // restored by closeModal
|
|
450
458
|
const closeBtn = IconBtn('plus', { title: t('common.close'), onClick: close }); // rotated to look like ×
|
|
451
459
|
closeBtn.style.transform = 'rotate(45deg)';
|
|
452
460
|
const modal = el('div', {
|
|
453
|
-
class: `modal${size === 'lg' ? ' modal--lg' : ''}`,
|
|
454
|
-
attrs: { role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'modal-title' },
|
|
461
|
+
class: `modal${size === 'lg' ? ' modal--lg' : ''}${intent === 'alert' ? ' modal--alert' : ''}`,
|
|
462
|
+
attrs: { role: intent === 'alert' ? 'alertdialog' : 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'modal-title' },
|
|
455
463
|
},
|
|
456
464
|
el('div', { class: 'modal__head' }, el('h3', { id: 'modal-title' }, title), el('div', { class: 'topbar__spacer' }), closeBtn),
|
|
457
465
|
);
|
|
@@ -482,6 +490,7 @@ export function confirmDialog({ title = t('common.confirm'), message, confirmLab
|
|
|
482
490
|
return new Promise((resolve) => {
|
|
483
491
|
openModal({
|
|
484
492
|
title,
|
|
493
|
+
intent: danger ? 'alert' : 'dialog',
|
|
485
494
|
body: el('p', { class: 'text-secondary', style: { margin: 0, lineHeight: '1.6' } }, message),
|
|
486
495
|
footer: (close) => [
|
|
487
496
|
Btn(t('common.cancel'), { onClick: () => { close(); resolve(false); } }),
|
|
@@ -21,6 +21,7 @@ export async function render(ctx) {
|
|
|
21
21
|
const root = el('div', { class: 'stack-4' });
|
|
22
22
|
|
|
23
23
|
const panel = el('div', {});
|
|
24
|
+
let activeHasItems = true;
|
|
24
25
|
// Tabs are keyboard-operable (activate = role/tabindex/Enter·Space), with tab
|
|
25
26
|
// semantics for AT — role은 attrs가 먼저 tab으로 지정하므로 activate의 button 기본값을 덮지 않는다.
|
|
26
27
|
const makeTab = (key, label) =>
|
|
@@ -43,7 +44,7 @@ export async function render(ctx) {
|
|
|
43
44
|
node.classList.toggle('is-active', tab === key);
|
|
44
45
|
node.setAttribute('aria-selected', String(tab === key));
|
|
45
46
|
}
|
|
46
|
-
setActions();
|
|
47
|
+
ctx.setActions([]);
|
|
47
48
|
renderPanel();
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -74,9 +75,14 @@ export async function render(ctx) {
|
|
|
74
75
|
async function renderPanel() {
|
|
75
76
|
mount(panel, el('div', { class: 'muted text-sm' }, t('documents.loading')));
|
|
76
77
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const content = tab === 'cover'
|
|
79
|
+
? await CoverLettersTab(reload)
|
|
80
|
+
: tab === 'career'
|
|
81
|
+
? await CareerDescriptionsTab(reload)
|
|
82
|
+
: await DocumentsTab(reload);
|
|
83
|
+
activeHasItems = content.dataset.hasItems === 'true';
|
|
84
|
+
mount(panel, content);
|
|
85
|
+
setActions();
|
|
80
86
|
applyFilter();
|
|
81
87
|
await openRequestedDocument();
|
|
82
88
|
} catch (err) {
|
|
@@ -102,6 +108,10 @@ export async function render(ctx) {
|
|
|
102
108
|
|
|
103
109
|
// topbar: search (filters the active tab) + the tab's create action
|
|
104
110
|
function setActions() {
|
|
111
|
+
if (!activeHasItems) {
|
|
112
|
+
ctx.setActions([]);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
105
115
|
const placeholder = tab === 'cover' ? t('documents.search.cover') : tab === 'career' ? t('documents.search.career') : t('documents.search.docs');
|
|
106
116
|
const search = Input({ type: 'search', placeholder, value: query, attrs: { 'aria-label': t('documents.search.aria') } });
|
|
107
117
|
search.classList.add('input--inline');
|
|
@@ -115,7 +125,7 @@ export async function render(ctx) {
|
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
mount(ctx.view, root);
|
|
118
|
-
setActions();
|
|
128
|
+
ctx.setActions([]);
|
|
119
129
|
await renderPanel();
|
|
120
130
|
}
|
|
121
131
|
|
|
@@ -124,6 +134,7 @@ export async function render(ctx) {
|
|
|
124
134
|
async function CoverLettersTab(reload) {
|
|
125
135
|
const { cover_letters: list } = await get('/api/cover-letters');
|
|
126
136
|
const wrap = el('div', { class: 'stack-3' });
|
|
137
|
+
wrap.dataset.hasItems = list.length ? 'true' : 'false';
|
|
127
138
|
|
|
128
139
|
if (!list.length) {
|
|
129
140
|
return mountInto(wrap, EmptyState({
|
|
@@ -376,6 +387,7 @@ async function CareerDescriptionsTab(reload) {
|
|
|
376
387
|
const [{ documents: list }, m] = await Promise.all([get('/api/documents?kind=career_description'), meta()]);
|
|
377
388
|
const kindLabels = Object.fromEntries((m.document_kinds || []).map((k) => [k.value, t('kind.' + k.value)]));
|
|
378
389
|
const wrap = el('div', { class: 'stack-3' });
|
|
390
|
+
wrap.dataset.hasItems = list.length ? 'true' : 'false';
|
|
379
391
|
|
|
380
392
|
if (!list.length) {
|
|
381
393
|
return mountInto(wrap, EmptyState({
|
|
@@ -397,6 +409,7 @@ async function DocumentsTab(reload) {
|
|
|
397
409
|
const list = (documents || []).filter((doc) => doc.kind !== 'career_description');
|
|
398
410
|
const kindLabels = Object.fromEntries((m.document_kinds || []).map((k) => [k.value, t('kind.' + k.value)]));
|
|
399
411
|
const wrap = el('div', { class: 'stack-3' });
|
|
412
|
+
wrap.dataset.hasItems = list.length ? 'true' : 'false';
|
|
400
413
|
|
|
401
414
|
if (!list.length) {
|
|
402
415
|
return mountInto(wrap, EmptyState({
|
|
@@ -262,13 +262,15 @@ function ActionLane(s) {
|
|
|
262
262
|
onClick: it.job ? () => navigate(`/jobs/${it.job.id}`) : null,
|
|
263
263
|
}));
|
|
264
264
|
}
|
|
265
|
-
|
|
265
|
+
const card = Card({
|
|
266
266
|
title: t('home.actionLane.title'),
|
|
267
267
|
actions: rows.length ? Btn(t('home.actionLane.viewAll'), { sm: true, variant: 'ghost', onClick: () => navigate('/applications') }) : null,
|
|
268
268
|
body: rows.length
|
|
269
269
|
? el('div', {}, ...rows)
|
|
270
270
|
: el('p', { class: 'muted', style: { margin: 0 } }, t('home.actionLane.empty')),
|
|
271
271
|
});
|
|
272
|
+
card.classList.add('home-action-card');
|
|
273
|
+
return card;
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
/* ----------------------------------------------- recent jobs */
|
|
@@ -30,12 +30,16 @@ export async function render(ctx) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/* ----------------------------------------------------- 데이터 저장 위치 */
|
|
33
|
-
function kvRow(label, value, copyable) {
|
|
33
|
+
function kvRow(label, value, copyable, { wrapValue = false } = {}) {
|
|
34
34
|
return [
|
|
35
35
|
el('dt', {}, label),
|
|
36
36
|
el('dd', {},
|
|
37
37
|
el('div', { class: 'flex between gap-3' },
|
|
38
|
-
el('span', {
|
|
38
|
+
el('span', {
|
|
39
|
+
class: `${wrapValue ? 'kv__value-wrap' : 'truncate'} tnum`,
|
|
40
|
+
style: { fontFamily: 'var(--mono, ui-monospace, monospace)' },
|
|
41
|
+
title: value,
|
|
42
|
+
}, value || '—'),
|
|
39
43
|
copyable && value
|
|
40
44
|
? IconBtn('copy', { title: t('settings.copy'), onClick: () => copyText(value) })
|
|
41
45
|
: null)),
|
|
@@ -70,9 +74,9 @@ function Connection(info) {
|
|
|
70
74
|
async function createShortcut() {
|
|
71
75
|
try {
|
|
72
76
|
const { shortcut } = await post('/api/settings/dashboard-shortcut', { open: true });
|
|
73
|
-
toastOk(isDemo ? t('settings.connection.shortcutDemoToast') : t('settings.connection.shortcutToast'));
|
|
74
77
|
openModal({
|
|
75
78
|
title: isDemo ? t('settings.connection.shortcutDemoTitle') : t('settings.connection.shortcutTitle'),
|
|
79
|
+
intent: 'contextual',
|
|
76
80
|
body: el('div', { class: 'stack-3' },
|
|
77
81
|
el('div', { class: 'callout' },
|
|
78
82
|
icon('info'),
|
|
@@ -83,11 +87,11 @@ function Connection(info) {
|
|
|
83
87
|
? t('settings.connection.shortcutDemoCalloutBody')
|
|
84
88
|
: t('settings.connection.shortcutCalloutBody')))),
|
|
85
89
|
el('dl', { class: 'kv' },
|
|
86
|
-
...kvRow(t('settings.connection.shortcutFolder'), shortcut.shortcut_dir, true),
|
|
87
|
-
...kvRow(t('settings.connection.shortcutLauncher'), shortcut.launcher_path, true))),
|
|
90
|
+
...kvRow(t('settings.connection.shortcutFolder'), shortcut.shortcut_dir, true, { wrapValue: true }),
|
|
91
|
+
...kvRow(t('settings.connection.shortcutLauncher'), shortcut.launcher_path, true, { wrapValue: true }))),
|
|
88
92
|
footer: (close) => [
|
|
89
|
-
Btn(t('settings.copyPath'), { icon: 'copy', onClick: () => copyText(shortcut.launcher_path) }),
|
|
90
|
-
Btn(t('
|
|
93
|
+
Btn(t('settings.copyPath'), { icon: 'copy', variant: 'primary', onClick: () => copyText(shortcut.launcher_path) }),
|
|
94
|
+
Btn(t('common.close'), { variant: 'ghost', onClick: close }),
|
|
91
95
|
],
|
|
92
96
|
});
|
|
93
97
|
} catch (e) {
|
|
@@ -360,6 +364,7 @@ function openImportModal({ fileName, backup, preview, rerender }) {
|
|
|
360
364
|
openModal({
|
|
361
365
|
title: t('settings.import.title'),
|
|
362
366
|
size: 'lg',
|
|
367
|
+
intent: 'alert',
|
|
363
368
|
body: el('div', { class: 'stack-3' },
|
|
364
369
|
el('div', { class: 'callout' },
|
|
365
370
|
icon('info'),
|
|
@@ -409,15 +414,10 @@ function openImportModal({ fileName, backup, preview, rerender }) {
|
|
|
409
414
|
/* -------------------------------------------------- 위험 구역 (Danger zone) */
|
|
410
415
|
/* ----------------------------------------------------- 도움말 · 문제 신고 */
|
|
411
416
|
function Support() {
|
|
412
|
-
const ext = (href, iconName, label) =>
|
|
413
|
-
el('a', { class: 'btn btn--ghost btn--sm', href, attrs: { target: '_blank', rel: 'noopener noreferrer' } },
|
|
414
|
-
icon(iconName), el('span', {}, label));
|
|
415
417
|
return Card({
|
|
416
418
|
title: t('settings.support.title'),
|
|
417
419
|
sub: t('settings.support.sub'),
|
|
418
420
|
body: el('div', { class: 'flex gap-2 wrap' },
|
|
419
|
-
ext('https://github.com/osntak/careermate/issues/new?template=bug_report.yml', 'external', t('settings.support.report')),
|
|
420
|
-
ext('https://github.com/osntak/careermate/discussions', 'external', t('settings.support.ask')),
|
|
421
421
|
el('a', { class: 'btn btn--ghost btn--sm', href: 'mailto:osntak@gmail.com' }, icon('mail'), el('span', {}, t('settings.support.email'))),
|
|
422
422
|
),
|
|
423
423
|
});
|
|
@@ -437,6 +437,7 @@ function DangerZone(ctx) {
|
|
|
437
437
|
|
|
438
438
|
openModal({
|
|
439
439
|
title: t('settings.reset.title'),
|
|
440
|
+
intent: 'alert',
|
|
440
441
|
body: el('div', { class: 'stack-3' },
|
|
441
442
|
el('div', { class: 'callout' },
|
|
442
443
|
icon('info'),
|
package/dist/public/styles.css
CHANGED
|
@@ -406,6 +406,10 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
406
406
|
.list-row:focus-within .row-action { opacity: 1; pointer-events: auto; }
|
|
407
407
|
@media (hover: none) { .list-row .row-action { opacity: 1; pointer-events: auto; } }
|
|
408
408
|
|
|
409
|
+
/* Home's "next action" lane is scanned repeatedly on mobile, so keep it tighter
|
|
410
|
+
than generic cards while preserving the shared ListRow structure. */
|
|
411
|
+
.home-action-card .list-row__trail .badge { max-width: 116px; overflow: hidden; text-overflow: ellipsis; }
|
|
412
|
+
|
|
409
413
|
/* ----------------------------------------------------------------- pipeline bar + funnel
|
|
410
414
|
Visualizes the application status_breakdown. Segment color == badge--{status} hue,
|
|
411
415
|
so a colour means the same stage everywhere (board, badges, here). */
|
|
@@ -467,6 +471,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
467
471
|
/* min-width:0 overrides a grid item's default min-width:auto so the 1fr column
|
|
468
472
|
can shrink below its content — otherwise a long path/value overflows the grid. */
|
|
469
473
|
.kv dd { margin: 0; min-width: 0; }
|
|
474
|
+
.kv__value-wrap { min-width: 0; white-space: normal; word-break: keep-all; overflow-wrap: anywhere; line-height: 1.5; }
|
|
470
475
|
.settings-action-row { align-items: flex-start; min-width: 0; }
|
|
471
476
|
.settings-action-row__main { flex: 1 1 auto; min-width: 0; }
|
|
472
477
|
.settings-action-row__main > div:last-child { min-width: 0; }
|
|
@@ -476,10 +481,13 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
476
481
|
.modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
|
|
477
482
|
.modal-root.is-open { display: block; }
|
|
478
483
|
.modal__scrim { position: absolute; inset: 0; background: rgba(16,16,20,.42); backdrop-filter: blur(2px); animation: fade .15s; }
|
|
484
|
+
.modal-root[data-intent="contextual"] .modal__scrim { background: rgba(16,16,20,.22); backdrop-filter: blur(1px); }
|
|
485
|
+
.modal-root[data-intent="alert"] .modal__scrim { background: rgba(16,16,20,.52); }
|
|
479
486
|
.modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: min(640px, calc(100vw - 32px));
|
|
480
487
|
max-height: calc(100vh - 48px); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
|
481
488
|
box-shadow: var(--shadow-lg); display: flex; flex-direction: column; animation: pop .16s ease; }
|
|
482
489
|
.modal--lg { width: min(880px, calc(100vw - 32px)); }
|
|
490
|
+
.modal--alert { width: min(520px, calc(100vw - 32px)); }
|
|
483
491
|
.modal__head { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; }
|
|
484
492
|
.modal__head h3 { font-size: 15px; }
|
|
485
493
|
.modal__body { padding: 20px; overflow-y: auto; }
|
|
@@ -488,6 +496,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
488
496
|
.modal__error[hidden] { display: none; }
|
|
489
497
|
.modal__foot { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
|
|
490
498
|
@keyframes pop { from { transform: translate(-50%,-46%); opacity: 0; } to { transform: translate(-50%,-50%); opacity: 1; } }
|
|
499
|
+
@keyframes sheetup { from { transform: translateY(18px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
491
500
|
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
|
|
492
501
|
|
|
493
502
|
/* Dropdown menu (Menu/ExportMenu) — fixed popover anchored to its trigger so it
|
|
@@ -507,6 +516,7 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
507
516
|
|
|
508
517
|
/* ----------------------------------------------------------------- toast */
|
|
509
518
|
.toasts { position: fixed; bottom: 20px; right: 20px; z-index: 80; display: flex; flex-direction: column; gap: 10px; }
|
|
519
|
+
body[data-modal-intent] .toasts { top: 20px; bottom: auto; }
|
|
510
520
|
.toast { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius);
|
|
511
521
|
box-shadow: var(--shadow-md); padding: 12px 16px; min-width: 260px; max-width: 380px; animation: slidein .2s ease; font-size: 14px; }
|
|
512
522
|
.toast--success { border-left-color: var(--green); }
|
|
@@ -578,7 +588,27 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
578
588
|
.settings-action-row > .btn{ width: 100%; justify-content: center; }
|
|
579
589
|
.card__head{ align-items: flex-start; flex-wrap: wrap; }
|
|
580
590
|
.card__head .right{ margin-left: 0; width: 100%; justify-content: flex-start; }
|
|
591
|
+
.home-action-card .card__head{ align-items: center; flex-wrap: nowrap; padding: 12px 14px; }
|
|
592
|
+
.home-action-card .card__head .right{ margin-left: auto; width: auto; justify-content: flex-end; }
|
|
593
|
+
.home-action-card .card__body{ padding: 8px 14px; }
|
|
594
|
+
.home-action-card .list-row{ gap: 10px; padding: 8px 0; }
|
|
595
|
+
.home-action-card .list-row__trail{ gap: 6px; }
|
|
581
596
|
.page-head{ flex-wrap: wrap; }
|
|
597
|
+
.modal-root[data-intent="contextual"] .modal{
|
|
598
|
+
top: auto; left: 0; right: 0; bottom: 0; transform: none; width: 100%;
|
|
599
|
+
max-height: min(86vh, calc(100vh - 24px)); border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
600
|
+
border-bottom: 0; animation: sheetup .16s ease;
|
|
601
|
+
}
|
|
602
|
+
.modal-root[data-intent="contextual"] .modal__foot{ justify-content: stretch; }
|
|
603
|
+
.modal-root[data-intent="contextual"] .modal__foot .btn{ flex: 1 1 auto; justify-content: center; }
|
|
604
|
+
.modal__foot{
|
|
605
|
+
justify-content: stretch; flex-wrap: wrap;
|
|
606
|
+
padding-bottom: max(14px, env(safe-area-inset-bottom));
|
|
607
|
+
}
|
|
608
|
+
.modal__foot .btn{ flex: 1 1 0; min-width: 0; min-height: 44px; justify-content: center; }
|
|
609
|
+
.toasts{ left: 16px; right: 16px; bottom: max(16px, env(safe-area-inset-bottom)); }
|
|
610
|
+
body[data-modal-intent] .toasts{ top: calc(var(--topbar-h) + 16px); bottom: auto; }
|
|
611
|
+
.toast{ width: 100%; min-width: 0; max-width: none; }
|
|
582
612
|
}
|
|
583
613
|
|
|
584
614
|
/* ---------- reduced motion ----------
|