Files
fusion/packages/dashboard/app/components/RuntimeCardShell.tsx
gsxdsm 1e49494bac feat(i18n): full-sweep string migration — 5,930 keys across 5 locales (#1352)
Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
  English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
  carry ~5,930 keys each across common/app/errors/cli namespaces;
  CLI bundles regenerated (6 locales incl. ko)

Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
  plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
  moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
  literal {{placeholders}}); dashboard vitest.setup boots a minimal en
  i18next instance for the same reason

Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:06:53 -07:00

165 lines
4.9 KiB
TypeScript

/**
* Shared visual shell for the three runtime-provider settings cards
* (Hermes, OpenClaw, Paperclip).
*
* Renders a consistent header (logo + name + status badge + Learn more link),
* a description block, a slot for connection-mode tabs, the form contents the
* caller passes as children, and a footer with Test/Save/Save & Test buttons
* plus a status toast that surfaces probe + save outcomes.
*/
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Loader2 } from "lucide-react";
export type RuntimeStatusKind = "neutral" | "ok" | "err" | "loading";
export interface RuntimeCardShellProps {
/** Name shown next to the logo (e.g. "Hermes"). */
name: string;
/** Optional sub-line under the name (e.g. "by Nous Research"). */
subname?: string;
/** The provider logo. Use a `<ProviderIcon>` or an inline `<img>` for brand SVGs. */
logo: ReactNode;
/** "Learn more" target — official documentation/site. */
learnMoreHref: string;
/** Status kind drives the badge color. */
statusKind: RuntimeStatusKind;
/** Status text (e.g. "✓ Detected v0.8.0"). */
statusText: string;
/** Description shown under the header. */
description: ReactNode;
/** Optional extra header chrome (e.g. tabs). Rendered above the form. */
tabs?: ReactNode;
/** The form fields. */
children: ReactNode;
/** Disabled state for the action row. */
busy: "loading" | "saving" | "testing" | "save-test" | null;
/** Disable Test if some required input is missing. */
canTest?: boolean;
/** Toast under the action row — shown when not null. */
toast?: { kind: "ok" | "err"; message: string } | null;
onTest: () => void;
onSave: () => void;
onSaveAndTest: () => void;
/** Optional extra content right above the footer (e.g. an install hint). */
belowForm?: ReactNode;
/** A test-id forwarded to the root element. */
testId?: string;
}
export function RuntimeCardShell(props: RuntimeCardShellProps) {
const { t } = useTranslation("app");
const {
name,
subname,
logo,
learnMoreHref,
statusKind,
statusText,
description,
tabs,
children,
busy,
canTest = true,
toast,
onTest,
onSave,
onSaveAndTest,
belowForm,
testId,
} = props;
const statusClass =
statusKind === "ok"
? "runtime-card__status runtime-card__status--ok"
: statusKind === "err"
? "runtime-card__status runtime-card__status--err"
: "runtime-card__status runtime-card__status--neutral";
return (
<div className="runtime-card" data-testid={testId} aria-live="polite">
<header className="runtime-card__header">
<span className="runtime-card__logo">{logo}</span>
<div className="runtime-card__title">
<h3 className="runtime-card__name">{name}</h3>
{subname && (
<small className="runtime-card__cobrand">{subname}</small>
)}
<small className={statusClass}>
{statusKind === "loading" && <Loader2 size={12} className="animate-spin" />}
{statusText}
</small>
</div>
<a
className="runtime-card__learn-more btn btn-sm btn-ghost"
href={learnMoreHref}
target="_blank"
rel="noreferrer"
>
{t("common.learnMore", "Learn more →")}
</a>
</header>
<p className="runtime-card__description">{description}</p>
{tabs}
<div className="runtime-card__form">{children}</div>
{belowForm}
<footer className="runtime-card__footer">
{toast && (
<span
className={
toast.kind === "ok"
? "runtime-card__toast runtime-card__toast--ok"
: "runtime-card__toast runtime-card__toast--err"
}
role="status"
>
{toast.message}
</span>
)}
<button
type="button"
className="btn btn-sm"
onClick={onTest}
disabled={busy !== null || !canTest}
>
{busy === "testing" ? (
<>
<Loader2 size={12} className="animate-spin" /> {t("common.testing", "Testing…")}
</>
) : (
t("common.test", "Test")
)}
</button>
<button
type="button"
className="btn btn-sm"
onClick={onSave}
disabled={busy !== null}
>
{busy === "saving" ? t("common.saving", "Saving…") : t("common.save", "Save")}
</button>
<button
type="button"
className="btn btn-primary btn-sm"
onClick={onSaveAndTest}
disabled={busy !== null || !canTest}
>
{busy === "save-test" ? (
<>
<Loader2 size={12} className="animate-spin" /> {t("common.saving", "Saving…")}
</>
) : (
t("common.saveAndTest", "Save & Test")
)}
</button>
</footer>
</div>
);
}