fix post generate
This commit is contained in:
@@ -47,7 +47,7 @@ async function renderServices(did: string, handle: string): Promise<string> {
|
||||
const items = Array.from(serviceMap.entries()).map(([domain, info]) => {
|
||||
return `
|
||||
<li class="service-list-item">
|
||||
<a href="?mode=browser&handle=${handle}&service=${encodeURIComponent(domain)}" class="service-list-link">
|
||||
<a href="/at/${handle}/${domain}" class="service-list-link">
|
||||
<img src="${info.favicon}" class="service-list-favicon" alt="" onerror="this.style.display='none'">
|
||||
<span class="service-list-name">${info.name}</span>
|
||||
<span class="service-list-count">${info.count}</span>
|
||||
@@ -84,7 +84,7 @@ async function renderCollections(did: string, handle: string, serviceDomain: str
|
||||
const items = filtered.map(col => {
|
||||
return `
|
||||
<li class="collection-item">
|
||||
<a href="?mode=browser&handle=${handle}&collection=${encodeURIComponent(col)}" class="collection-link">
|
||||
<a href="/at/${handle}/${col}" class="collection-link">
|
||||
<span class="collection-nsid">${col}</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -111,7 +111,7 @@ async function renderRecordList(did: string, handle: string, collection: string)
|
||||
const preview = rec.value.title || rec.value.text?.slice(0, 50) || rkey
|
||||
return `
|
||||
<li class="record-item">
|
||||
<a href="?mode=browser&handle=${handle}&collection=${encodeURIComponent(collection)}&rkey=${rkey}" class="record-link">
|
||||
<a href="/at/${handle}/${collection}/${rkey}" class="record-link">
|
||||
<span class="record-rkey">${rkey}</span>
|
||||
<span class="record-preview">${preview}</span>
|
||||
</a>
|
||||
@@ -178,16 +178,16 @@ export async function mountAtBrowser(
|
||||
let nav = ''
|
||||
|
||||
if (collection && rkey) {
|
||||
nav = `<a href="?mode=browser&handle=${handle}&collection=${encodeURIComponent(collection)}" class="back-link">← Back</a>`
|
||||
nav = `<a href="/at/${handle}/${collection}" class="back-link">← Back</a>`
|
||||
content = await renderRecordDetail(did, handle, collection, rkey, canDelete)
|
||||
} else if (collection) {
|
||||
// Get service from collection for back link
|
||||
const info = getServiceInfo(collection)
|
||||
const backService = info ? info.domain : ''
|
||||
nav = `<a href="?mode=browser&handle=${handle}&service=${encodeURIComponent(backService)}" class="back-link">← ${info?.name || 'Back'}</a>`
|
||||
nav = `<a href="/at/${handle}/${backService}" class="back-link">← ${info?.name || 'Back'}</a>`
|
||||
content = await renderRecordList(did, handle, collection)
|
||||
} else if (service) {
|
||||
nav = `<a href="?mode=browser&handle=${handle}" class="back-link">← Services</a>`
|
||||
nav = `<a href="/at/${handle}" class="back-link">← Services</a>`
|
||||
content = await renderCollections(did, handle, service)
|
||||
} else {
|
||||
content = await renderServices(did, handle)
|
||||
@@ -213,7 +213,7 @@ export async function mountAtBrowser(
|
||||
btn.textContent = 'Deleting...'
|
||||
await deleteRecord(col, rk)
|
||||
// Go back to collection
|
||||
window.location.href = `?mode=browser&handle=${handle}&collection=${encodeURIComponent(col)}`
|
||||
window.location.href = `/at/${handle}/${col}`
|
||||
} catch (err) {
|
||||
alert('Delete failed: ' + err)
|
||||
btn.disabled = false
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { BlogPost } from '../types.js'
|
||||
import { putRecord } from '../lib/auth.js'
|
||||
import { renderMarkdown } from '../lib/markdown.js'
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
@@ -28,7 +29,7 @@ export function mountPostList(container: HTMLElement, posts: BlogPost[]): void {
|
||||
const rkey = post.uri.split('/').pop()
|
||||
return `
|
||||
<li class="post-item">
|
||||
<a href="?rkey=${rkey}" class="post-link">
|
||||
<a href="/post/${rkey}" class="post-link">
|
||||
<span class="post-title">${escapeHtml(post.title)}</span>
|
||||
<span class="post-date">${formatDate(post.createdAt)}</span>
|
||||
</a>
|
||||
@@ -41,7 +42,7 @@ export function mountPostList(container: HTMLElement, posts: BlogPost[]): void {
|
||||
|
||||
export function mountPostDetail(container: HTMLElement, post: BlogPost, handle: string, collection: string, canEdit: boolean = false): void {
|
||||
const rkey = post.uri.split('/').pop() || ''
|
||||
const jsonUrl = `?mode=browser&handle=${handle}&collection=${encodeURIComponent(collection)}&rkey=${rkey}`
|
||||
const jsonUrl = `/at/${handle}/${collection}/${rkey}`
|
||||
|
||||
const editBtn = canEdit ? `<button class="edit-btn" id="edit-btn">edit</button>` : ''
|
||||
|
||||
@@ -55,7 +56,7 @@ export function mountPostDetail(container: HTMLElement, post: BlogPost, handle:
|
||||
${editBtn}
|
||||
</div>
|
||||
</header>
|
||||
<div class="post-content" id="post-content">${escapeHtml(post.content)}</div>
|
||||
<div class="post-content" id="post-content">${renderMarkdown(post.content)}</div>
|
||||
</article>
|
||||
|
||||
<div class="edit-form-container" id="edit-form-container" style="display: none;">
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function renderServices(handle: string): Promise<string> {
|
||||
}
|
||||
|
||||
const items = Array.from(serviceMap.entries()).map(([domain, info]) => {
|
||||
const url = `?mode=browser&handle=${handle}&service=${encodeURIComponent(domain)}`
|
||||
const url = `/at/${handle}/${domain}`
|
||||
|
||||
return `
|
||||
<a href="${url}" class="service-item" title="${info.collections.join(', ')}">
|
||||
|
||||
71
src/lib/markdown.ts
Normal file
71
src/lib/markdown.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { marked, Renderer } from 'marked'
|
||||
import hljs from 'highlight.js/lib/core'
|
||||
|
||||
// Import only common languages
|
||||
import javascript from 'highlight.js/lib/languages/javascript'
|
||||
import typescript from 'highlight.js/lib/languages/typescript'
|
||||
import bash from 'highlight.js/lib/languages/bash'
|
||||
import json from 'highlight.js/lib/languages/json'
|
||||
import yaml from 'highlight.js/lib/languages/yaml'
|
||||
import markdown from 'highlight.js/lib/languages/markdown'
|
||||
import css from 'highlight.js/lib/languages/css'
|
||||
import xml from 'highlight.js/lib/languages/xml'
|
||||
import python from 'highlight.js/lib/languages/python'
|
||||
import rust from 'highlight.js/lib/languages/rust'
|
||||
import go from 'highlight.js/lib/languages/go'
|
||||
|
||||
hljs.registerLanguage('javascript', javascript)
|
||||
hljs.registerLanguage('js', javascript)
|
||||
hljs.registerLanguage('typescript', typescript)
|
||||
hljs.registerLanguage('ts', typescript)
|
||||
hljs.registerLanguage('bash', bash)
|
||||
hljs.registerLanguage('sh', bash)
|
||||
hljs.registerLanguage('shell', bash)
|
||||
hljs.registerLanguage('json', json)
|
||||
hljs.registerLanguage('yaml', yaml)
|
||||
hljs.registerLanguage('yml', yaml)
|
||||
hljs.registerLanguage('markdown', markdown)
|
||||
hljs.registerLanguage('md', markdown)
|
||||
hljs.registerLanguage('css', css)
|
||||
hljs.registerLanguage('html', xml)
|
||||
hljs.registerLanguage('xml', xml)
|
||||
hljs.registerLanguage('python', python)
|
||||
hljs.registerLanguage('py', python)
|
||||
hljs.registerLanguage('rust', rust)
|
||||
hljs.registerLanguage('rs', rust)
|
||||
hljs.registerLanguage('go', go)
|
||||
|
||||
// Custom renderer with syntax highlighting
|
||||
const renderer = new Renderer()
|
||||
|
||||
renderer.code = function({ text, lang }: { text: string; lang?: string }) {
|
||||
let highlighted: string
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
highlighted = hljs.highlight(text, { language: lang }).value
|
||||
} catch {
|
||||
highlighted = escapeHtml(text)
|
||||
}
|
||||
} else {
|
||||
// No auto-detect, just escape
|
||||
highlighted = escapeHtml(text)
|
||||
}
|
||||
return `<pre><code class="hljs">${highlighted}</code></pre>`
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
}
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
renderer,
|
||||
})
|
||||
|
||||
export function renderMarkdown(content: string): string {
|
||||
return marked.parse(content) as string
|
||||
}
|
||||
86
src/lib/router.ts
Normal file
86
src/lib/router.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
export interface Route {
|
||||
type: 'blog' | 'post' | 'browser-services' | 'browser-collections' | 'browser-record' | 'new'
|
||||
handle?: string
|
||||
collection?: string
|
||||
rkey?: string
|
||||
service?: string
|
||||
}
|
||||
|
||||
export function parseRoute(pathname: string): Route {
|
||||
const parts = pathname.split('/').filter(Boolean)
|
||||
|
||||
// / - Blog top
|
||||
if (parts.length === 0) {
|
||||
return { type: 'blog' }
|
||||
}
|
||||
|
||||
// /new - New post form
|
||||
if (parts[0] === 'new') {
|
||||
return { type: 'new' }
|
||||
}
|
||||
|
||||
// /post/${rkey} - Post detail
|
||||
if (parts[0] === 'post' && parts[1]) {
|
||||
return { type: 'post', rkey: parts[1] }
|
||||
}
|
||||
|
||||
// /at/${handle} - Browser services
|
||||
// /at/${handle}/${service-or-collection} - Browser collections or records
|
||||
// /at/${handle}/${collection}/${rkey} - Browser record detail
|
||||
if (parts[0] === 'at' && parts[1]) {
|
||||
const handle = parts[1]
|
||||
|
||||
if (!parts[2]) {
|
||||
// /at/${handle}
|
||||
return { type: 'browser-services', handle }
|
||||
}
|
||||
|
||||
if (!parts[3]) {
|
||||
// /at/${handle}/${service-or-collection}
|
||||
// If it looks like a domain (2 parts), treat as service
|
||||
// Otherwise treat as collection NSID (3+ parts)
|
||||
const segment = parts[2]
|
||||
if (segment.split('.').length <= 2) {
|
||||
// Likely a service domain like "bsky.app"
|
||||
return { type: 'browser-collections', handle, service: segment }
|
||||
} else {
|
||||
// Likely a collection NSID like "app.bsky.feed.post"
|
||||
// Show record list for this collection
|
||||
return { type: 'browser-record', handle, collection: segment }
|
||||
}
|
||||
}
|
||||
|
||||
// /at/${handle}/${collection}/${rkey}
|
||||
return { type: 'browser-record', handle, collection: parts[2], rkey: parts[3] }
|
||||
}
|
||||
|
||||
// Fallback to blog
|
||||
return { type: 'blog' }
|
||||
}
|
||||
|
||||
export function buildPath(route: Route): string {
|
||||
switch (route.type) {
|
||||
case 'blog':
|
||||
return '/'
|
||||
case 'new':
|
||||
return '/new'
|
||||
case 'post':
|
||||
return `/post/${route.rkey}`
|
||||
case 'browser-services':
|
||||
return `/at/${route.handle}`
|
||||
case 'browser-collections':
|
||||
return `/at/${route.handle}/${route.service}`
|
||||
case 'browser-record':
|
||||
if (route.rkey) {
|
||||
return `/at/${route.handle}/${route.collection}/${route.rkey}`
|
||||
}
|
||||
return `/at/${route.handle}/${route.collection}`
|
||||
default:
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
|
||||
export function navigate(path: string): void {
|
||||
window.history.pushState({}, '', path)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
227
src/main.ts
227
src/main.ts
@@ -6,9 +6,11 @@ import { mountPostList, mountPostDetail } from './components/posts.js'
|
||||
import { mountHeader } from './components/browser.js'
|
||||
import { mountAtBrowser } from './components/atbrowser.js'
|
||||
import { mountPostForm } from './components/postform.js'
|
||||
import { parseRoute, type Route } from './lib/router.js'
|
||||
import type { AppConfig, Networks } from './types.js'
|
||||
|
||||
let authSession: AuthSession | null = null
|
||||
let config: AppConfig
|
||||
|
||||
async function loadConfig(): Promise<AppConfig> {
|
||||
const res = await fetch('/config.json')
|
||||
@@ -30,25 +32,138 @@ function renderFooter(handle: string): string {
|
||||
`
|
||||
}
|
||||
|
||||
function renderTabs(handle: string, mode: string | null, isLoggedIn: boolean): string {
|
||||
const blogActive = !mode || mode === 'blog' ? 'active' : ''
|
||||
const browserActive = mode === 'browser' ? 'active' : ''
|
||||
const postActive = mode === 'post' ? 'active' : ''
|
||||
function renderTabs(route: Route, isLoggedIn: boolean): string {
|
||||
const isBlog = route.type === 'blog' || route.type === 'post'
|
||||
const isBrowser = route.type.startsWith('browser')
|
||||
const isNew = route.type === 'new'
|
||||
|
||||
let tabs = `
|
||||
<a href="?handle=${handle}" class="tab ${blogActive}">Blog</a>
|
||||
<a href="?mode=browser&handle=${handle}" class="tab ${browserActive}">Browser</a>
|
||||
<a href="/" class="tab ${isBlog ? 'active' : ''}">Blog</a>
|
||||
<a href="/at/${config.handle}" class="tab ${isBrowser ? 'active' : ''}">Browser</a>
|
||||
`
|
||||
|
||||
if (isLoggedIn) {
|
||||
tabs += `<a href="?mode=post&handle=${handle}" class="tab ${postActive}">Post</a>`
|
||||
tabs += `<a href="/new" class="tab ${isNew ? 'active' : ''}">Post</a>`
|
||||
}
|
||||
|
||||
return `<div class="mode-tabs">${tabs}</div>`
|
||||
}
|
||||
|
||||
async function render(): Promise<void> {
|
||||
const route = parseRoute(window.location.pathname)
|
||||
|
||||
const profileEl = document.getElementById('profile')
|
||||
const contentEl = document.getElementById('content')
|
||||
const headerEl = document.getElementById('header')
|
||||
const footerEl = document.getElementById('footer')
|
||||
|
||||
if (!profileEl || !contentEl || !headerEl) return
|
||||
|
||||
// Footer
|
||||
if (footerEl) {
|
||||
footerEl.innerHTML = renderFooter(config.handle)
|
||||
}
|
||||
|
||||
const isLoggedIn = !!authSession
|
||||
const handle = route.handle || config.handle
|
||||
|
||||
// Header with login
|
||||
mountHeader(headerEl, handle, isLoggedIn, authSession?.handle, {
|
||||
onBrowse: (newHandle) => {
|
||||
window.location.href = `/at/${newHandle}`
|
||||
},
|
||||
onLogin: async () => {
|
||||
const inputHandle = (document.getElementById('header-input') as HTMLInputElement)?.value || handle
|
||||
try {
|
||||
await login(inputHandle)
|
||||
} catch (err) {
|
||||
console.error('Login error:', err)
|
||||
alert('Login failed: ' + err)
|
||||
}
|
||||
},
|
||||
onLogout: async () => {
|
||||
await logout()
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
|
||||
// Route handling
|
||||
switch (route.type) {
|
||||
case 'new':
|
||||
if (isLoggedIn) {
|
||||
profileEl.innerHTML = renderTabs(route, isLoggedIn)
|
||||
mountPostForm(contentEl, config.collection, () => {
|
||||
window.location.href = '/'
|
||||
})
|
||||
} else {
|
||||
window.location.href = '/'
|
||||
}
|
||||
break
|
||||
|
||||
case 'browser-services':
|
||||
case 'browser-collections':
|
||||
case 'browser-record':
|
||||
profileEl.innerHTML = renderTabs(route, isLoggedIn)
|
||||
const loginDid = authSession?.did || null
|
||||
await mountAtBrowser(
|
||||
contentEl,
|
||||
route.handle || config.handle,
|
||||
route.collection || null,
|
||||
route.rkey || null,
|
||||
route.service || null,
|
||||
loginDid
|
||||
)
|
||||
break
|
||||
|
||||
case 'post':
|
||||
try {
|
||||
const profile = await getProfile(config.handle)
|
||||
profileEl.innerHTML = renderTabs(route, isLoggedIn)
|
||||
const profileContentEl = document.createElement('div')
|
||||
profileEl.appendChild(profileContentEl)
|
||||
mountProfile(profileContentEl, profile)
|
||||
|
||||
const servicesHtml = await renderServices(config.handle)
|
||||
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
||||
|
||||
const post = await getRecord(profile.did, config.collection, route.rkey!)
|
||||
if (post) {
|
||||
const canEdit = isLoggedIn && authSession?.did === profile.did
|
||||
mountPostDetail(contentEl, post, config.handle, config.collection, canEdit)
|
||||
} else {
|
||||
contentEl.innerHTML = '<p>Post not found</p>'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
||||
}
|
||||
break
|
||||
|
||||
case 'blog':
|
||||
default:
|
||||
try {
|
||||
const profile = await getProfile(config.handle)
|
||||
profileEl.innerHTML = renderTabs(route, isLoggedIn)
|
||||
const profileContentEl = document.createElement('div')
|
||||
profileEl.appendChild(profileContentEl)
|
||||
mountProfile(profileContentEl, profile)
|
||||
|
||||
const servicesHtml = await renderServices(config.handle)
|
||||
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
||||
|
||||
const posts = await listRecords(profile.did, config.collection)
|
||||
mountPostList(contentEl, posts)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function init(): Promise<void> {
|
||||
const [config, networks] = await Promise.all([loadConfig(), loadNetworks()])
|
||||
const [configData, networks] = await Promise.all([loadConfig(), loadNetworks()])
|
||||
config = configData
|
||||
|
||||
// Set page title
|
||||
document.title = config.title || 'ailog'
|
||||
@@ -70,102 +185,14 @@ async function init(): Promise<void> {
|
||||
if (callbackSession) {
|
||||
authSession = callbackSession
|
||||
} else {
|
||||
// Try to restore existing session
|
||||
authSession = await restoreSession()
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const mode = params.get('mode')
|
||||
const rkey = params.get('rkey')
|
||||
const collection = params.get('collection')
|
||||
const service = params.get('service')
|
||||
const handle = params.get('handle') || config.handle
|
||||
// Initial render
|
||||
await render()
|
||||
|
||||
const profileEl = document.getElementById('profile')
|
||||
const contentEl = document.getElementById('content')
|
||||
const headerEl = document.getElementById('header')
|
||||
const footerEl = document.getElementById('footer')
|
||||
|
||||
if (!profileEl || !contentEl || !headerEl) return
|
||||
|
||||
// Footer
|
||||
if (footerEl) {
|
||||
footerEl.innerHTML = renderFooter(config.handle)
|
||||
}
|
||||
|
||||
const isLoggedIn = !!authSession
|
||||
|
||||
// Header with login
|
||||
mountHeader(headerEl, handle, isLoggedIn, authSession?.handle, {
|
||||
onBrowse: (newHandle) => {
|
||||
const currentMode = params.get('mode')
|
||||
if (currentMode === 'browser') {
|
||||
window.location.href = `?mode=browser&handle=${newHandle}`
|
||||
} else {
|
||||
window.location.href = `?handle=${newHandle}`
|
||||
}
|
||||
},
|
||||
onLogin: async () => {
|
||||
const inputHandle = (document.getElementById('header-input') as HTMLInputElement)?.value || handle
|
||||
try {
|
||||
await login(inputHandle)
|
||||
} catch (err) {
|
||||
console.error('Login error:', err)
|
||||
alert('Login failed: ' + err)
|
||||
}
|
||||
},
|
||||
onLogout: async () => {
|
||||
await logout()
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
|
||||
// Post mode (requires login)
|
||||
if (mode === 'post' && isLoggedIn) {
|
||||
profileEl.innerHTML = renderTabs(handle, mode, isLoggedIn)
|
||||
mountPostForm(contentEl, config.collection, () => {
|
||||
window.location.href = `?handle=${handle}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// AT Browser mode
|
||||
if (mode === 'browser') {
|
||||
profileEl.innerHTML = renderTabs(handle, mode, isLoggedIn)
|
||||
const loginDid = authSession?.did || null
|
||||
await mountAtBrowser(contentEl, handle, collection, rkey, service, loginDid)
|
||||
return
|
||||
}
|
||||
|
||||
// Blog mode (default)
|
||||
try {
|
||||
const profile = await getProfile(handle)
|
||||
|
||||
profileEl.innerHTML = renderTabs(handle, mode, isLoggedIn)
|
||||
const profileContentEl = document.createElement('div')
|
||||
profileEl.appendChild(profileContentEl)
|
||||
mountProfile(profileContentEl, profile)
|
||||
|
||||
// Add services
|
||||
const servicesHtml = await renderServices(handle)
|
||||
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
||||
|
||||
if (rkey) {
|
||||
const post = await getRecord(profile.did, config.collection, rkey)
|
||||
if (post) {
|
||||
const canEdit = isLoggedIn && authSession?.did === profile.did
|
||||
mountPostDetail(contentEl, post, handle, config.collection, canEdit)
|
||||
} else {
|
||||
contentEl.innerHTML = '<p>Post not found</p>'
|
||||
}
|
||||
} else {
|
||||
const posts = await listRecords(profile.did, config.collection)
|
||||
mountPostList(contentEl, posts)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
||||
}
|
||||
// Handle browser navigation
|
||||
window.addEventListener('popstate', () => render())
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
@@ -429,9 +429,139 @@ body {
|
||||
.post-content {
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Markdown Styles */
|
||||
.post-content h1,
|
||||
.post-content h2,
|
||||
.post-content h3,
|
||||
.post-content h4,
|
||||
.post-content h5,
|
||||
.post-content h6 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post-content h1 { font-size: 1.75em; }
|
||||
.post-content h2 { font-size: 1.5em; }
|
||||
.post-content h3 { font-size: 1.25em; }
|
||||
.post-content h4 { font-size: 1.1em; }
|
||||
|
||||
.post-content p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.post-content ul,
|
||||
.post-content ol {
|
||||
margin-bottom: 1em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.post-content li {
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.post-content a {
|
||||
color: var(--btn-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post-content blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 1em;
|
||||
border-left: 4px solid #ddd;
|
||||
background: #f9f9f9;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.post-content code {
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
font-size: 0.9em;
|
||||
padding: 0.15em 0.4em;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.post-content pre {
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.post-content pre code {
|
||||
display: block;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #d4d4d4;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.post-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.post-content hr {
|
||||
margin: 2em 0;
|
||||
border: none;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.post-content table {
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.post-content th,
|
||||
.post-content td {
|
||||
padding: 0.5em;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.post-content th {
|
||||
background: #f5f5f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Highlight.js Theme Overrides */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-built_in,
|
||||
.hljs-name,
|
||||
.hljs-tag { color: #569cd6; }
|
||||
.hljs-string,
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-addition { color: #ce9178; }
|
||||
.hljs-comment,
|
||||
.hljs-quote,
|
||||
.hljs-deletion,
|
||||
.hljs-meta { color: #6a9955; }
|
||||
.hljs-number,
|
||||
.hljs-regexp,
|
||||
.hljs-symbol,
|
||||
.hljs-variable,
|
||||
.hljs-link { color: #b5cea8; }
|
||||
.hljs-function { color: #dcdcaa; }
|
||||
.hljs-attr { color: #9cdcfe; }
|
||||
|
||||
.post-footer {
|
||||
margin-top: 32px;
|
||||
padding-top: 16px;
|
||||
@@ -776,4 +906,23 @@ body {
|
||||
.delete-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
/* Dark mode markdown */
|
||||
.post-content blockquote {
|
||||
border-color: #444;
|
||||
background: #1a1a1a;
|
||||
color: #aaa;
|
||||
}
|
||||
.post-content code {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
.post-content th {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
.post-content th,
|
||||
.post-content td {
|
||||
border-color: #444;
|
||||
}
|
||||
.post-content hr {
|
||||
border-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user