Files
TREK/client/src/components/Help/HelpPanel.tsx
T
Maurice ba70ad28b8 fix(client): spacing and alignment follow the reading direction
Arabic runs the app right to left, but margins, paddings, insets, text
alignment, borders and corner radii were written for the left side, so
icons sat against the wrong edge of their labels, indents and badges
pointed the wrong way and amount columns aligned left. They now use the
logical forms (ms-, ps-, start-, text-start, marginInlineStart,
insetInlineEnd and their CSS equivalents), which are the very same sides
in a left to right language. Geometry stays physical on purpose: maps,
positions measured from the pointer or a bounding box, time axes,
sliders, transforms and print output.

Amounts drawn as several spans keep their order in a right to left row,
the time picker keeps hours before minutes, and two switches whose thumb
slid out of its track in Arabic now move the right way.

lint:rtl counts what is still physical per file against a baseline that
may only shrink (1512 uses in 370 files before, 319 in 113 now), and CI
runs it, so new code reaches for the logical forms.
2026-10-07 22:33:58 +00:00

132 lines
5.0 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { X } from 'lucide-react'
import { useTranslation } from '../../i18n'
import { useHelpStore } from '../../store/helpStore'
import { useHelpShortcut } from '../../hooks/useHelpShortcut'
import { getHelpContext, getHelpGuide } from '../../help/registry'
import HelpNav from './HelpNav'
import HelpHome from './HelpHome'
import HelpGuideView from './HelpGuideView'
import HelpSearchResults from './HelpSearchResults'
import HelpLightbox from './HelpLightbox'
import './help.css'
/** How long the closing animation runs before the dialog leaves the DOM. */
const CLOSE_MS = 160
/**
* The help center: one large dialog over the app. Navigation and search on the
* left, the screen overview or the open guide on the right. Escape is handled
* by useHelpShortcut; the `?` in the navbar is the only way in.
*/
export default function HelpPanel(): React.ReactElement | null {
const { t } = useTranslation()
const open = useHelpStore(s => s.open)
const contextId = useHelpStore(s => s.contextId)
const browseId = useHelpStore(s => s.browseId)
const guideId = useHelpStore(s => s.guideId)
const query = useHelpStore(s => s.query)
const closeHelp = useHelpStore(s => s.closeHelp)
const loadDocIndex = useHelpStore(s => s.loadDocIndex)
useHelpShortcut()
// Keep the dialog mounted for the length of the exit animation.
const [state, setState] = useState<'closed' | 'open' | 'closing'>(open ? 'open' : 'closed')
const wasOpen = useRef(open)
useEffect(() => {
if (open) {
wasOpen.current = true
setState('open')
return
}
if (!wasOpen.current) return
wasOpen.current = false
setState('closing')
const id = window.setTimeout(() => setState('closed'), CLOSE_MS)
return () => window.clearTimeout(id)
}, [open])
// Doc titles for the links; one fetch per session.
useEffect(() => {
if (open) void loadDocIndex()
}, [open, loadDocIndex])
// The page behind must not scroll while the dialog is up.
useEffect(() => {
if (!open) return
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = prev
}
}, [open])
// Focus back to the `?` when the dialog closes.
useEffect(() => {
if (open) return
const btn = document.querySelector<HTMLElement>('[data-help-button]')
if (btn && document.activeElement === document.body) btn.focus({ preventScroll: true })
}, [open])
// Every view change starts at the top of the content pane.
const shownId = browseId ?? contextId
const scrollRef = useRef<HTMLDivElement>(null)
useEffect(() => {
scrollRef.current?.scrollTo?.({ top: 0 })
}, [guideId, query, shownId])
if (state === 'closed') return null
const context = getHelpContext(shownId)
const guide = getHelpGuide(guideId)
// Typing wins over whatever was open: a search from inside a guide shows
// results, and clearing the box returns to that guide.
const searching = query.trim().length > 0
return createPortal(
<>
<div
className="trek-help-backdrop fixed inset-0 z-[var(--z-overlay)] flex items-center justify-center p-3 sm:p-5 bg-[var(--overlay)] backdrop-blur-sm"
data-state={state === 'open' ? 'open' : 'closing'}
role="presentation"
onClick={closeHelp}
>
<div
className="trek-help-dialog relative w-[min(1520px,100%)] h-[min(960px,100%)] grid grid-cols-1 md:grid-cols-[320px_minmax(0,1fr)] rounded-2xl border border-edge bg-surface-card shadow-modal overflow-hidden"
role="dialog"
aria-modal="true"
aria-label={t('help.center.title')}
onClick={e => e.stopPropagation()}
>
<HelpNav context={context} currentId={contextId} activeGuideId={!searching && guide ? guide.id : null} />
<div className="relative flex flex-col min-h-0 bg-surface" role="main">
<button
type="button"
onClick={closeHelp}
aria-label={t('help.center.close')}
className="absolute top-4 end-4 z-10 w-9 h-9 rounded-full flex items-center justify-center text-content-muted bg-surface-card border border-edge shadow-card hover:text-content hover:bg-surface-hover transition-colors"
>
<X className="w-4 h-4" />
</button>
<div ref={scrollRef} className="trek-help-scroll flex-1 min-h-0 overflow-y-auto overscroll-contain">
<div className="max-w-[1080px] mx-auto px-6 sm:px-10 py-8">
{searching ? (
<HelpSearchResults key="search" />
) : guide ? (
<HelpGuideView key={guide.id} guide={guide} />
) : (
<HelpHome key={context?.id ?? 'none'} context={context} />
)}
</div>
</div>
</div>
</div>
</div>
<HelpLightbox />
</>,
document.body,
)
}