💄 header nav

This commit is contained in:
2026-03-03 20:07:38 +10:00
parent 682623849e
commit f5953a4952
7 changed files with 216 additions and 3 deletions

View File

@@ -0,0 +1,89 @@
.outline-panel {
width: 160px;
min-width: 160px;
flex-shrink: 0;
background: var(--sidebar-bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.outline-header {
padding: 14px 12px 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--text-muted);
border-bottom: 1px solid var(--border);
text-transform: uppercase;
flex-shrink: 0;
}
.outline-list {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.outline-empty {
padding: 12px 12px;
font-size: 11px;
font-family: var(--font-sans);
color: var(--text-muted);
opacity: 0.5;
}
/* Base item style — reset button defaults */
.outline-item {
display: block;
width: 100%;
background: none;
border: none;
cursor: pointer;
text-align: left;
font-family: var(--font-sans);
font-size: 11px;
color: var(--text-secondary);
padding: 4px 8px;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background 0.1s, color 0.1s;
}
.outline-item:hover {
background: rgba(167, 139, 95, 0.12);
color: var(--text-primary);
}
/* Heading indent levels */
.outline-item--h1 {
padding-left: 8px;
font-weight: 600;
color: var(--text-primary);
}
.outline-item--h2 {
padding-left: 18px;
}
.outline-item--h3 {
padding-left: 28px;
color: var(--text-muted);
font-size: 10.5px;
}
/* Divider row */
.outline-divider {
padding-left: 8px;
color: var(--text-muted);
opacity: 0.5;
letter-spacing: 0.05em;
font-size: 10px;
}
.outline-divider:hover {
opacity: 1;
}

View File

@@ -0,0 +1,82 @@
import { useMemo } from 'react'
import { EditorView } from '@codemirror/view'
import { useEditorStore } from '../../store/editorStore'
import { currentEditorView } from './MarkdownEditor'
import './DocumentOutline.css'
type OutlineItem =
| { type: 'heading'; level: 1 | 2 | 3; text: string; offset: number; key: string }
| { type: 'divider'; offset: number; nth: number; key: string }
function parseOutline(content: string): OutlineItem[] {
const items: OutlineItem[] = []
const lines = content.split('\n')
let offset = 0
let dividerCount = 0
for (const line of lines) {
const headingMatch = line.match(/^(#{1,3})\s+(.+)/)
if (headingMatch) {
const level = headingMatch[1].length as 1 | 2 | 3
const text = headingMatch[2].trim()
items.push({ type: 'heading', level, text, offset, key: `h-${offset}` })
} else if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
dividerCount++
items.push({ type: 'divider', offset, nth: dividerCount, key: `d-${offset}` })
}
offset += line.length + 1
}
return items
}
function navigateTo(offset: number): void {
const view = currentEditorView
if (!view) return
view.dispatch({
selection: { anchor: offset },
effects: EditorView.scrollIntoView(offset, { y: 'start' })
})
view.focus()
}
export function DocumentOutline(): JSX.Element {
const activeFileContent = useEditorStore((s) => s.activeFileContent)
const items = useMemo(() => parseOutline(activeFileContent), [activeFileContent])
return (
<div className="outline-panel">
<div className="outline-header">OUTLINE</div>
<div className="outline-list">
{items.length === 0 ? (
<div className="outline-empty">No headings</div>
) : (
items.map((item) => {
if (item.type === 'heading') {
return (
<button
key={item.key}
className={`outline-item outline-item--h${item.level}`}
onClick={() => navigateTo(item.offset)}
title={item.text}
>
{item.text}
</button>
)
}
return (
<button
key={item.key}
className="outline-item outline-divider"
onClick={() => navigateTo(item.offset)}
title={`Divider ${item.nth}`}
>
</button>
)
})
)}
</div>
</div>
)
}

View File

@@ -63,7 +63,9 @@ export function AnalysisToolbar(): JSX.Element {
setProjectWordCount,
fontSize,
setFontSize,
setRightPanelTab
setRightPanelTab,
outlineOpen,
toggleOutline
} = useEditorStore()
useEffect(() => {
@@ -297,6 +299,13 @@ export function AnalysisToolbar(): JSX.Element {
</div>
<div className="toolbar-right">
<button
className={`toolbar-btn toolbar-btn-outline${outlineOpen ? ' active' : ''}`}
onClick={toggleOutline}
title={outlineOpen ? 'Hide document outline' : 'Show document outline'}
>
</button>
<div className="toolbar-fontsize">
<button
className="toolbar-fontsize-btn"

View File

@@ -141,6 +141,14 @@
opacity: 0.5;
}
/* ── Outline toggle button ───────────────────────────────────────── */
.toolbar-btn-outline {
padding: 4px 8px;
font-size: 14px;
line-height: 1;
flex-shrink: 0;
}
/* ── Story Bible toolbar ─────────────────────────────────────────── */
.toolbar-bible-label {
font-size: 9px;