Backend - dev-api.py: Q&A suggest endpoint, Log Contact, cf-orch node detection in wizard hardware step, canonical search_profiles format (profiles:[...]), connections settings endpoints, Resume Library endpoints - db_migrate.py: migrations 002/003/004 — ATS columns, resume review, final resume struct - discover.py: _normalize_profiles() for legacy wizard YAML format compat - resume_optimizer.py: section-by-section resume parsing + scoring - task_runner.py: Q&A and contact-log task types - company_research.py: accessibility brief column wiring - generate_cover_letter.py: restore _candidate module-level binding Frontend - InterviewPrepView.vue: Q&A chat tab, Log Contact form, MarkdownView rendering - InterviewCard.vue: new reusable card component for interviews kanban - InterviewsView.vue: rejected analytics section with stage breakdown chips - ResumeProfileView.vue: sync with new resume store shape - SearchPrefsView.vue: cf-orch toggle, profile format migration - SystemSettingsView.vue: connections settings wiring - ConnectionsSettingsView.vue: new view for integration connections - MarkdownView.vue: new component for safe markdown rendering - ApplyWorkspace.vue: a11y — h1→h2 demotion, aria-expanded on Q&A toggle, confirmation dialog on Reject action (#98 #99 #100) - peregrine.css: explicit [data-theme="dark"] token block for light-OS users (#101), :focus-visible outline (#97) - wizard.css: cf-orch hardware step styles - WizardHardwareStep.vue: cf-orch node display, profile selection with orch option - WizardLayout.vue: hardware step wiring Infra - compose.yml / compose.cloud.yml: cf-orch agent sidecar, llm.cloud.yaml mount - Dockerfile.cfcore: cf-core editable install in image build - HANDOFF-xanderland.md: Podman/systemd setup guide for beta tester - podman-standalone.sh: standalone Podman run script Tests - test_dev_api_settings.py: remove stale worktree path bootstrap (credential_store now in main repo); fix job_boards fixture to use non-empty list - test_wizard_api.py: update profiles assertion to superset check (cf-orch added); update step6 assertion to canonical profiles[].titles format
77 lines
2.5 KiB
Vue
77 lines
2.5 KiB
Vue
<template>
|
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
<div class="markdown-body" :class="className" v-html="rendered" />
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { marked } from 'marked'
|
|
import DOMPurify from 'dompurify'
|
|
|
|
const props = defineProps<{
|
|
content: string
|
|
className?: string
|
|
}>()
|
|
|
|
// Configure marked: gfm for GitHub-flavored markdown, breaks converts \n → <br>
|
|
marked.setOptions({ gfm: true, breaks: true })
|
|
|
|
const rendered = computed(() => {
|
|
if (!props.content?.trim()) return ''
|
|
const html = marked(props.content) as string
|
|
return DOMPurify.sanitize(html, {
|
|
ALLOWED_TAGS: ['p','br','strong','em','b','i','ul','ol','li','h1','h2','h3','h4','blockquote','code','pre','a','hr'],
|
|
ALLOWED_ATTR: ['href','target','rel'],
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.markdown-body { line-height: 1.6; color: var(--color-text); }
|
|
.markdown-body :deep(p) { margin: 0 0 0.75em; }
|
|
.markdown-body :deep(p:last-child) { margin-bottom: 0; }
|
|
.markdown-body :deep(ul), .markdown-body :deep(ol) { margin: 0 0 0.75em; padding-left: 1.5em; }
|
|
.markdown-body :deep(li) { margin-bottom: 0.25em; }
|
|
.markdown-body :deep(h1), .markdown-body :deep(h2), .markdown-body :deep(h3), .markdown-body :deep(h4) {
|
|
font-weight: 700; margin: 1em 0 0.4em; color: var(--color-text);
|
|
}
|
|
.markdown-body :deep(h1) { font-size: 1.2em; }
|
|
.markdown-body :deep(h2) { font-size: 1.1em; }
|
|
.markdown-body :deep(h3) { font-size: 1em; }
|
|
.markdown-body :deep(strong), .markdown-body :deep(b) { font-weight: 700; }
|
|
.markdown-body :deep(em), .markdown-body :deep(i) { font-style: italic; }
|
|
.markdown-body :deep(code) {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.875em;
|
|
background: var(--color-surface-alt);
|
|
border: 1px solid var(--color-border-light);
|
|
padding: 0.1em 0.3em;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
.markdown-body :deep(pre) {
|
|
background: var(--color-surface-alt);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--space-3);
|
|
overflow-x: auto;
|
|
font-size: 0.875em;
|
|
}
|
|
.markdown-body :deep(pre code) { background: none; border: none; padding: 0; }
|
|
.markdown-body :deep(blockquote) {
|
|
border-left: 3px solid var(--color-accent);
|
|
margin: 0.75em 0;
|
|
padding: 0.25em 0 0.25em 1em;
|
|
color: var(--color-text-muted);
|
|
font-style: italic;
|
|
}
|
|
.markdown-body :deep(hr) {
|
|
border: none;
|
|
border-top: 1px solid var(--color-border);
|
|
margin: 1em 0;
|
|
}
|
|
.markdown-body :deep(a) {
|
|
color: var(--color-accent);
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
</style>
|