Files
siftlode/frontend/src/components/ui/Banner.tsx
T
peter 6d8b7be6d8 feat!: remove the shared demo account
The demo account — one shared user that whitelisted emails entered from
the login page, reset on a schedule — goes end to end (owner decision,
E34): demo.py and the reset job, users.is_demo and demo_whitelist
(migration 0098 deletes the demo user, its audit rows, the reset job's
override and notices, then drops the column and table; the downgrade
restores the empty schema), the /auth/demo entry, the admin whitelist and
reset, the Demo tab, the banner, the login page's "Try the demo", and
every is_demo branch in routes, userscope, scheduler and the UI.

require_human only ever refused the demo account, so its 87 uses become
current_user (owner-approved); require_write_scope keeps its own check.
The login page's "or" divider now shows only beside the Google button:
without the demo entry under it, an instance without Google would have
said "or" to nothing.

A contract test (removedFeatures) fails on the word anywhere in the
backend, frontend, e2e or scripts — seen red on a planted hit. Size
ceilings tightened for the modules that shrank; admin.py is back under
the limit.
2026-10-05 00:19:38 +02:00

81 lines
2.4 KiB
TypeScript

import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import type { LucideIcon } from "lucide-react";
import { X } from "lucide-react";
import { Button } from "./Button";
import { IconButton } from "./IconButton";
type BannerTone = "accent" | "warning";
const TONES: Record<BannerTone, { bar: string; icon: string; action: "primary" | "warn" }> = {
accent: {
bar: "bg-accent/15 border-accent/30",
icon: "text-accent",
action: "primary",
},
warning: {
bar: "bg-warn/15 border-warn/30",
icon: "text-warn",
action: "warn",
},
};
// Shared top-of-app notification bar. VersionBanner and VerifyBanner are thin wrappers over this;
// a future news-ticker variant can build on the same shell (rotating content as children).
export default function Banner({
tone,
icon: Icon,
children,
action,
onDismiss,
dismissTitle,
role,
}: {
tone: BannerTone;
icon: LucideIcon;
children: ReactNode;
action?: { label: string; onClick: () => void };
onDismiss?: () => void;
/** Accessible name (and tooltip) for the dismiss button. Optional at the call site; the component
* falls back to the generic "Close" rather than shipping an unnamed icon button — a nameless
* control is not an acceptable default, and `Button`'s type now says so. */
dismissTitle?: string;
/** `"status"` for a bar that APPEARS in response to something the user just did, so a screen
* reader announces it. Left unset for the permanent bars (version): those are part of the
* page when it loads, and announcing them on every visit is noise, not information. */
role?: "status";
}) {
const { t } = useTranslation();
const c = TONES[tone];
return (
<div
role={role}
className={`shrink-0 flex items-center gap-2 px-4 py-2 text-sm border-b text-fg ${c.bar}`}
>
<Icon className={`w-4 h-4 shrink-0 ${c.icon}`} />
<span className="min-w-0">{children}</span>
{action && (
<Button
variant={c.action}
size="sm"
onClick={action.onClick}
className="ml-1 font-semibold"
>
{action.label}
</Button>
)}
{onDismiss && (
<IconButton
label={dismissTitle ?? t("common.close")}
size="sm"
variant="ghost"
onClick={onDismiss}
className="ml-auto"
>
<X />
</IconButton>
)}
</div>
);
}