Simpl-Resultat/src/components/import/SourceCard.tsx
le king fu ed5eeb0265 feat(onboarding): step card with highlight, and the Import page's card
On the page of the step in progress, a card takes the place of the
banner. `StepCard` is driven by props: an h2 title that takes the focus
("Step n of N", then the step), the page's sentence, a status region
always rendered that receives "Done.", "Skip this step" until the step is
done, then "Next step" or the end of the path. `OnboardingStepCard` is
the only piece that reads the path: it renders on the latched card page
only, so the card survives its own success, focuses its title once per
arrival by "Next step" and consumes the navigation state.

The control to use carries `data-onboarding-highlight`, drawn in
styles.css as a 3px accent outline with a pulse bounded to three cycles
and none under prefers-reduced-motion; a test reads the stylesheet and
checks 3:1 against the background and the cards in both themes. The
target is passed by props to EmptyState's primary action, the first
SourceList card and WizardNavigation's next button, never queried.

`importCardModel` reads the wizard and the checklist in order: importing
before the counters, a report that added nothing as a failure, and the
"steps 3 and 4 checked" row at rest, where it can apply. CARD_PAGES gets
/import; the Import page builds the input, renders the card first and
highlights the model's target only while the card shows, and its empty
state waits for the folder to be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:34:45 -04:00

63 lines
2.1 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { FolderOpen, FileText, CheckCircle } from "lucide-react";
import type { ScannedSource } from "../../shared/types";
import { onboardingTargetAttributes } from "../../utils/onboardingPath";
interface SourceCardProps {
source: ScannedSource;
isConfigured: boolean;
newFileCount: number;
onClick: () => void;
/** Highlight this card for the getting-started card (#398). */
onboardingTarget?: boolean;
}
/** `data-onboarding-target` of a highlighted source card. */
export const SOURCE_CARD_TARGET = "source-card";
export default function SourceCard({
source,
isConfigured,
newFileCount,
onClick,
onboardingTarget,
}: SourceCardProps) {
const { t } = useTranslation();
return (
<button
onClick={onClick}
className="w-full text-left bg-[var(--card)] rounded-xl p-4 border border-[var(--border)] hover:border-[var(--primary)] hover:shadow-sm transition-all"
{...onboardingTargetAttributes(SOURCE_CARD_TARGET, onboardingTarget)}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<FolderOpen size={18} className="text-[var(--primary)]" />
<h3 className="font-medium text-[var(--foreground)]">
{source.folder_name}
</h3>
</div>
<div className="flex items-center gap-1.5">
{newFileCount > 0 && (
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-[var(--accent)] text-white">
{newFileCount} {t("import.sources.new")}
</span>
)}
{isConfigured && (
<CheckCircle
size={16}
className="text-[var(--positive)]"
/>
)}
</div>
</div>
<div className="flex items-center gap-1 text-sm text-[var(--muted-foreground)]">
<FileText size={14} />
<span>
{source.files.length}{" "}
{t("import.sources.fileCount", { count: source.files.length })}
</span>
</div>
</button>
);
}