The app HAS a platform layer -- AnchoredPopover (22 importers, with arrows/typeahead/Escape/anchor-flip), QueryState (21), Modal (17), PageShell, DataTable, Tabs, Tooltip and ~26 more -- but it sat flat among ~160 feature components with nothing marking it as one. Several of those files describe themselves as 'the one place' or 'canonical' and were still bypassed by hand-rolled markup, because a layer nobody can see is a layer nobody uses; three separate inventory passes of this codebase misread it the same way. Moves the 33 domain-free modules into components/ui/, rewrites the imports in 89 files, and makes the boundary a lint rule rather than a convention: inside ui/ a one-level-up import (a feature component) and any lib/api slice are errors. The rule was verified to fire on both and to leave generic lib/ helpers alone. README.md states which side of the line a component belongs on and why. No behaviour change: 3x tsc, eslint, knip, prettier and 687 vitest tests all green; no backend file touched.
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { Bug, Sparkles } from "lucide-react";
|
|
import { RELEASE_NOTES } from "../lib/releaseNotes";
|
|
import Modal from "./ui/Modal";
|
|
|
|
function fmtDate(d: string): string {
|
|
const t = new Date(d);
|
|
return isNaN(t.getTime())
|
|
? d
|
|
: t.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
|
}
|
|
|
|
// Public release notes (chores are intentionally omitted here). `highlight` rings the
|
|
// version the user just updated to when opened from the new-version banner.
|
|
export default function ReleaseNotes({
|
|
onClose,
|
|
highlight,
|
|
}: {
|
|
onClose: () => void;
|
|
highlight?: string;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
// Ring the newest note if `highlight` isn't one of the listed versions — e.g. a Vite dev build
|
|
// where the caller's FRONTEND_VERSION is "dev", or a VERSION bump whose note hasn't landed yet —
|
|
// so the banner always rings *something* current instead of nothing.
|
|
const active = RELEASE_NOTES.some((r) => r.version === highlight)
|
|
? highlight
|
|
: RELEASE_NOTES[0]?.version;
|
|
return (
|
|
<Modal title={t("about.releaseNotes")} onClose={onClose} maxWidth="max-w-2xl">
|
|
<div className="flex flex-col gap-6">
|
|
{RELEASE_NOTES.map((r) => (
|
|
<section
|
|
key={r.version}
|
|
className={active === r.version ? "rounded-xl ring-1 ring-accent/40 p-3 -m-3" : ""}
|
|
>
|
|
<div className="flex items-baseline gap-2 flex-wrap">
|
|
<h3 className="text-base font-semibold">v{r.version}</h3>
|
|
<span className="text-xs text-muted">{fmtDate(r.date)}</span>
|
|
{r.sha && (
|
|
<span className="text-2xs font-mono text-muted bg-surface px-1.5 py-0.5 rounded-sm">
|
|
{r.sha}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{r.summary && <p className="text-sm text-muted mt-1">{r.summary}</p>}
|
|
|
|
{r.features?.length ? (
|
|
<div className="mt-3">
|
|
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-accent">
|
|
<Sparkles className="w-3.5 h-3.5" /> {t("about.new")}
|
|
</div>
|
|
<ul className="mt-1.5 space-y-1.5 text-sm">
|
|
{r.features.map((f, i) => (
|
|
<li key={i} className="flex gap-2">
|
|
<span className="text-accent">•</span>
|
|
<span>{f}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : null}
|
|
|
|
{r.fixes?.length ? (
|
|
<div className="mt-3">
|
|
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted">
|
|
<Bug className="w-3.5 h-3.5" /> {t("about.fixes")}
|
|
</div>
|
|
<ul className="mt-1.5 space-y-1.5 text-sm">
|
|
{r.fixes.map((f, i) => (
|
|
<li key={i} className="flex gap-2">
|
|
<span className="text-muted">•</span>
|
|
<span>{f}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : null}
|
|
</section>
|
|
))}
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|