Add contextual "?" help icons linking into Docs (ticket 021)

New HelpIcon component + a DocsNavProvider context (webapp/src/lib/docsNav.tsx)
let any component request "open Docs to section X", bumping a nonce each
time so a repeat click re-triggers even if Docs is already open to that
section. NavMenu forwards that request into DocsPanel as a `target` prop,
which expands the right accordion section, scrolls it into view, and
flashes it (a brief accent-tinted background/border, settling back after
1.2s) — verified visually with a live server + Playwright. Opening Docs via
the plain NavMenu button explicitly clears any pending target, so that path
is unchanged: no forced section, no flash.

Added a "?" icon to FilterBar, LogTimeline, LogPreview, Metrics, Dashboards,
and NavMenu's own Settings/Presets panels, plus new Docs sections for
Timeline, Log preview, Settings, and Presets (previously undocumented).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 15:49:49 +01:00
co-authored by Claude Sonnet 5
parent ce711ab70e
commit abc061a7b1
10 changed files with 206 additions and 20 deletions
+88 -3
View File
@@ -1,4 +1,5 @@
import { useState, type ReactNode } from 'react'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import type { DocsTarget } from '../lib/docsNav'
/// One accordion section: a title plus its body, rendered as plain JSX so
/// each page can mix prose, lists, and inline code without pulling in a
@@ -122,6 +123,32 @@ const SECTIONS: DocSection[] = [
</div>
),
},
{
id: 'timeline',
title: 'Timeline',
body: (
<div className="flex flex-col gap-2">
<p>
The timeline above the log table renders log volume per bucket over the active window. Drag across it to
select a fixed time range — it narrows the log table, metrics, and dashboards below to that window; click
"Clear selection" to go back to the default (rolling "all time") view.
</p>
</div>
),
},
{
id: 'log-preview',
title: 'Log preview',
body: (
<div className="flex flex-col gap-2">
<p>
The log table lists ingested lines newest-last, each with its sequence number, timestamp, and raw text. The
column picker (top-right) adds a column per declared param, showing that line's derived value for it (or a
blank if it didn't match) — a quick way to eyeball params without switching to the Metrics panel.
</p>
</div>
),
},
{
id: 'metrics',
title: 'Metrics',
@@ -200,17 +227,75 @@ const SECTIONS: DocSection[] = [
</div>
),
},
{
id: 'settings',
title: 'Settings',
body: (
<div className="flex flex-col gap-2">
<p>
The Settings panel controls process-wide state: colour scheme, the maximum number of logs kept in memory
(blank means unlimited), and imported/linked CSS. These apply immediately and persist to the single
settings file on disk — no save button needed.
</p>
</div>
),
},
{
id: 'presets',
title: 'Presets',
body: (
<div className="flex flex-col gap-2">
<p>
The Presets panel manages profiles: create, switch, and delete them. Switching activates a profile's
params/metrics in the running engine immediately, with no restart needed — see the <em>General idea</em>{' '}
section above for what a profile stores.
</p>
</div>
),
},
]
export function DocsPanel() {
/** How long a flashed section stays highlighted before settling back to normal. */
const FLASH_DURATION_MS = 1200
export function DocsPanel({ target }: { target?: DocsTarget | null }) {
const [openId, setOpenId] = useState<string | null>(SECTIONS[0].id)
const [flashId, setFlashId] = useState<string | null>(null)
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
// Keyed on `target?.nonce` (not `target?.sectionId`) so clicking the same
// "?" icon twice in a row re-triggers the flash rather than being a no-op
// because the panel is already open to that section.
useEffect(() => {
if (!target) return
setOpenId(target.sectionId)
const raf = requestAnimationFrame(() => {
sectionRefs.current[target.sectionId]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
})
setFlashId(target.sectionId)
const timeout = setTimeout(() => setFlashId(null), FLASH_DURATION_MS)
return () => {
cancelAnimationFrame(raf)
clearTimeout(timeout)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target?.nonce])
return (
<div className="flex flex-col gap-1 text-sm text-text-secondary">
{SECTIONS.map((section) => {
const isOpen = openId === section.id
const isFlashing = flashId === section.id
return (
<div key={section.id} className="rounded border border-border">
<div
key={section.id}
ref={(el) => {
sectionRefs.current[section.id] = el
}}
className={`rounded border transition-colors duration-500 ${
isFlashing ? 'border-accent bg-accent-soft/25' : 'border-border'
}`}
>
<button
type="button"
onClick={() => setOpenId(isOpen ? null : section.id)}