Private
Public Access
Markdown support was already complete and documented, but nothing in the composer pointed at it -- unlike @, # and : (which advertise themselves when typed), formatting syntax was only discoverable via Help, buried in the account menu. The composer stays a plain markdown textarea; a new "Aa" button opens a toolbar that inserts the syntax for you: bold, italic, strikethrough, inline code, code block, link, quote, bulleted/numbered lists, H1-H3, sub/superscript. Collapsed by default, remembered across reloads and room switches. Clicking a button again removes the formatting (native undo doesn't track programmatic edits), and bold/italic/sub/strike toggles are run-length aware so "***x***" is handled correctly. The selection math is pure functions in lib/markdownFormat.ts, unit-tested. The "?" opens /help#formatting in a new tab so the draft isn't lost; HelpPage now scrolls to the hash once the guide has loaded, since the browser's own hash scroll fired before the async-fetched headings existed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import Markdown from 'markdown-to-jsx'
|
|
import { Link, useLocation } from 'react-router-dom'
|
|
import { createMarkdownOptions, preprocessMarkdown } from '../components/MessageContent'
|
|
import { TopBar } from '../components/TopBar'
|
|
import './HelpPage.css'
|
|
|
|
// Single source of truth is the repo-root USER_GUIDE.md -- frontend/public/
|
|
// symlinks to it, so this fetches the same file a developer sees when
|
|
// browsing the repo, rather than duplicating its content into the bundle.
|
|
const GUIDE_URL = '/USER_GUIDE.md'
|
|
|
|
export function HelpPage() {
|
|
const [content, setContent] = useState<string | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
// #21: heading-id/sub/superscript support -- the user guide is exactly
|
|
// the kind of document that benefits from an explicit anchor override.
|
|
const markdownPreview = useMemo(
|
|
() => (content !== null ? preprocessMarkdown(content) : null),
|
|
[content],
|
|
)
|
|
|
|
// #75: the composer's "?" links straight to /help#formatting, but the guide
|
|
// is fetched after the page loads -- the browser's own scroll-to-hash has
|
|
// already run (and found nothing) by the time the headings exist.
|
|
const { hash } = useLocation()
|
|
useEffect(() => {
|
|
if (!markdownPreview || !hash) return
|
|
document.getElementById(hash.slice(1))?.scrollIntoView()
|
|
}, [markdownPreview, hash])
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
fetch(GUIDE_URL)
|
|
.then((res) => {
|
|
if (!res.ok) throw new Error(`Failed to load guide (${res.status})`)
|
|
return res.text()
|
|
})
|
|
.then((text) => {
|
|
if (!cancelled) setContent(text)
|
|
})
|
|
.catch((err) => {
|
|
if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load guide')
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<div className="help-page">
|
|
<TopBar />
|
|
<div className="help-body">
|
|
<div className="help-header">
|
|
<h1>Help</h1>
|
|
<Link to="/rooms" className="btn-secondary">
|
|
Back to chat
|
|
</Link>
|
|
</div>
|
|
{error && <p className="admin-error">{error}</p>}
|
|
{!error && content === null && <p className="help-loading">Loading…</p>}
|
|
{!error && markdownPreview && (
|
|
<div className="message-text help-content">
|
|
<Markdown options={createMarkdownOptions(markdownPreview.headingIds)}>
|
|
{markdownPreview.text}
|
|
</Markdown>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|