Help on every option and every page

- Each option's explanation moves from a line of text under its label to
  a small tooltip on an ⓘ beside it, so forms read calmer and the help is
  still one hover away.
- Help text is ours where written (src/help/texts.ts: domains, people,
  DNS providers, blocked addresses, and the main pages), and the schema's
  description elsewhere.
- A "?" on every list and form opens a side panel: what the page is for,
  what people usually do there, and every option explained.
- Every tooltip and panel carries a stable help id (x:Domain.dnsManagement,
  x:Domain), and manual.ts turns an id into a link to the admin manual
  once one is configured (VITE_MANUAL_URL, or <meta name="manual-url">).
  Until then no link shows.
This commit is contained in:
2026-09-19 01:59:14 -07:00
parent 7ab0b8099c
commit 824427ef46
12 changed files with 512 additions and 18 deletions
+62
View File
@@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import { ArrowUpRight, Info } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { manualUrl } from './manual';
/**
* The ⓘ beside an option: a sentence or two on what it does, on hover or
* focus, and a way into the manual once there is one. `id` is the option's
* stable help id, the key the manual links hang on.
*/
export function HelpTip({ id, text, className }: { id?: string; text?: string | null; className?: string }) {
const { t } = useTranslation();
if (!text) return null;
const more = id ? manualUrl(id) : null;
return (
<TooltipProvider delayDuration={150}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
data-help-id={id}
aria-label={t('help.about', 'About this option')}
className={cn(
'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground/60 transition-colors hover:text-primary focus-visible:text-primary focus-visible:outline-none',
className,
)}
>
<Info className="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
className="max-w-xs space-y-1.5 border bg-popover px-3 py-2 text-xs leading-relaxed text-popover-foreground shadow-soft"
>
<div className="[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_p]:m-0">
<ReactMarkdown>{text.replace(/\\n/g, '\n')}</ReactMarkdown>
</div>
{more && (
<a
href={more}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-0.5 font-medium text-primary hover:underline"
>
{t('help.learnMore', 'Learn more')}
<ArrowUpRight className="h-3 w-3" />
</a>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}