Tidy the token, connector and share-link settings cards

The API tokens card names every kind of token it lists and reads in
sentence case. Each ingest card and the connector card fold the
shown-once note into the paragraph about what the token can do, and
their name fields suggest a fitting example instead of one phone
shortcut everywhere, short enough to stay whole on a phone. The
connector card drops the status badge that repeated its switch. Share
links get their own card next to the one that creates them.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 20:35:56 +02:00
1 parent c90d28e597
commit c816946140
10 files changed
+315 -280

No files matched your search

+10 -4
View File
@@ -91,6 +91,7 @@ function MeasurementTokensCard() {
description: t("settings.measurementsToken.description"),
detail: t("settings.measurementsToken.detail"),
scopeNote: t("settings.measurementsToken.scopeNote"),
namePlaceholder: t("settings.measurementsToken.namePlaceholder"),
createFailed: t("settings.measurementsToken.createFailed"),
}}
/>
@@ -114,6 +115,7 @@ function WorkoutTokensCard() {
description: t("settings.workoutsToken.description"),
detail: t("settings.workoutsToken.detail"),
scopeNote: t("settings.workoutsToken.scopeNote"),
namePlaceholder: t("settings.workoutsToken.namePlaceholder"),
createFailed: t("settings.workoutsToken.createFailed"),
}}
/>
@@ -137,6 +139,7 @@ function DocumentTokensCard() {
description: t("settings.documentsToken.description"),
detail: t("settings.documentsToken.detail"),
scopeNote: t("settings.documentsToken.scopeNote"),
namePlaceholder: t("settings.documentsToken.namePlaceholder"),
createFailed: t("settings.documentsToken.createFailed"),
}}
/>
@@ -161,6 +164,7 @@ function IngestTokenCard({
description: string;
detail: string;
scopeNote: string;
namePlaceholder: string;
createFailed: string;
};
}) {
@@ -234,16 +238,18 @@ function IngestTokenCard({
/>
<div className="space-y-3">
<p className="text-sm leading-relaxed">{copy.detail}</p>
<p className="text-muted-foreground text-sm leading-relaxed">
{copy.scopeNote}
{/* "Shown once" and what the token can do are one statement about
the credential, so they read as one paragraph. */}
<p className="text-sm leading-relaxed">
{copy.detail} {copy.scopeNote}
</p>
<form onSubmit={handleCreate} className="flex items-center gap-2">
<Input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder={t("settings.tokenNamePlaceholder")}
placeholder={copy.namePlaceholder}
aria-label={t("settings.tokenNameLabel")}
maxLength={100}
className="flex-1"
/>
+7 -13
View File
@@ -232,15 +232,6 @@ function McpEnableCard() {
icon={Plug}
title={t("settings.mcp.enableTitle")}
description={t("settings.mcp.enableDescription")}
status={
enabled ? (
<Badge className="bg-success/15 text-success">
{t("settings.mcp.enabled")}
</Badge>
) : (
<Badge variant="outline">{t("settings.mcp.disabled")}</Badge>
)
}
/>
<div className="space-y-4">
@@ -371,10 +362,12 @@ function McpTokensCard() {
title={t("settings.mcp.tokensTitle")}
description={t("settings.mcp.tokensDescription")}
/>
<p className="text-sm">{t("settings.mcp.tokensDetail")}</p>
<div className="space-y-4">
<p className="text-sm leading-relaxed">{t("settings.mcp.scopeNote")}</p>
{/* "Shown once" is part of the same statement as the scope note, so
it reads inside that paragraph rather than as a line of its own. */}
<p className="text-sm leading-relaxed">
{t("settings.mcp.tokensDetail")} {t("settings.mcp.scopeNote")}
</p>
<div className="border-border bg-muted/30 space-y-2 rounded-lg border p-3">
<div className="flex items-center justify-between gap-4">
@@ -396,7 +389,8 @@ function McpTokensCard() {
<Input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder={t("settings.tokenNamePlaceholder")}
placeholder={t("settings.mcp.tokenNamePlaceholder")}
aria-label={t("settings.tokenNameLabel")}
maxLength={100}
className="flex-1"
/>
+199 -185
View File
@@ -23,7 +23,7 @@
*/
import { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { FileText, KeyRound, Link2, Trash2 } from "lucide-react";
import { FileText, KeyRound, Link2, List, Trash2 } from "lucide-react";
import {
AlertDialog,
@@ -138,213 +138,227 @@ function ShareLinksCard() {
[links],
);
// Two cards: making a link and the links already out are separate jobs, and
// a growing list under the form pushed the create action out of reach.
return (
<SettingsCard>
<SettingsCardHeader
icon={Link2}
title={t("settings.sharing.createTitle")}
description={t("settings.sharing.createDescription")}
/>
<p className="text-sm">{t("settings.sharing.createDetail")}</p>
<>
<SettingsCard id="share-link-create" className="scroll-mt-28">
<SettingsCardHeader
icon={Link2}
title={t("settings.sharing.createTitle")}
description={t("settings.sharing.createDescription")}
/>
<p className="text-sm">{t("settings.sharing.createDetail")}</p>
<ShareLinkCreateForm
key={prefill?.key ?? "new"}
initialLabel={prefill?.label}
initialRangeDays={prefill?.rangeDays}
initialExpiryDays={prefill?.expiryDays}
/>
<ShareLinkCreateForm
key={prefill?.key ?? "new"}
initialLabel={prefill?.label}
initialRangeDays={prefill?.rangeDays}
initialExpiryDays={prefill?.expiryDays}
/>
</SettingsCard>
{isError ? (
// A read failure must not read as "no active links" on a clinical
// sharing surface — that is the §6 fall-through this card must avoid.
<QueryErrorCard onRetry={() => refetch()} />
) : isLoading ? (
<div className="space-y-2" data-testid="share-links-loading">
<Skeleton className="h-24 w-full rounded-lg" />
<Skeleton className="h-24 w-full rounded-lg" />
</div>
) : (
<>
<div className="space-y-2">
<h3 className="text-sm font-medium">
{t("settings.sharing.activeTitle")}
</h3>
{activeLinks.length === 0 ? (
<EmptyState
size="compact"
data-testid="share-active-empty"
title={t("settings.sharing.noActive")}
/>
) : (
<ul className="space-y-2" data-testid="share-active-list">
{activeLinks.map((link) => (
<li
key={link.id}
className="bg-muted/30 border-border space-y-2 rounded-lg border p-3"
>
<div className="flex flex-wrap items-start justify-between gap-2">
<p className="min-w-0 flex-1 text-sm font-medium break-words">
{link.label}
</p>
<div className="flex flex-wrap items-center gap-1.5">
<Badge className="bg-success/15 text-success text-xs">
{t("settings.sharing.statusActive")}
</Badge>
{link.protected && (
<Badge
variant="outline"
className="gap-1 text-xs"
data-testid="share-protected-badge"
>
<KeyRound className="h-2.5 w-2.5" />
{t("settings.sharing.protected")}
</Badge>
)}
{link.documentCount > 0 && (
<Badge
variant="outline"
className="gap-1 text-xs"
data-testid="share-doc-count-badge"
aria-label={t("settings.sharing.documentCount", {
count: link.documentCount,
})}
>
<FileText className="h-2.5 w-2.5" />
{link.documentCount}
</Badge>
)}
</div>
</div>
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.created")}:
</span>{" "}
{formatDate(link.createdAt)}
</p>
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.expires")}:
</span>{" "}
{formatDateTime(link.expiresAt)}
</p>
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.accessCount")}:
</span>{" "}
{link.accessCount}
{link.lastAccessAt
? ` · ${formatDateTime(link.lastAccessAt)}`
: ""}
</p>
{/* §12 — a right-aligned row action, not a full-width
destructive strip stacked once per link (which reads as
an alarm wall on a list of shares). */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="ml-auto flex min-h-11 w-fit sm:min-h-9"
>
<Trash2 className="mr-2 h-3.5 w-3.5" />
{t("settings.sharing.revoke")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("settings.sharing.revoke")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("settings.sharing.revokeDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("common.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => revokeMutation.mutate(link.id)}
>
{t("settings.sharing.revoke")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</li>
))}
</ul>
)}
<SettingsCard data-testid="share-links-card">
<SettingsCardHeader
icon={List}
title={t("settings.sharing.activeTitle")}
description={t("settings.sharing.activeDescription")}
/>
{isError ? (
// A read failure must not read as "no active links" on a clinical
// sharing surface — that is the §6 fall-through this card must avoid.
<QueryErrorCard onRetry={() => refetch()} />
) : isLoading ? (
<div className="space-y-2" data-testid="share-links-loading">
<Skeleton className="h-24 w-full rounded-lg" />
<Skeleton className="h-24 w-full rounded-lg" />
</div>
{inactiveLinks.length > 0 && (
) : (
<>
<div className="space-y-2">
<button
type="button"
onClick={() => setShowRevoked((prev) => !prev)}
className="text-foreground hover:text-primary text-sm font-medium transition-colors"
>
{t("settings.sharing.inactiveTitle", {
count: inactiveLinks.length,
})}
</button>
{showRevoked && (
<ul className="space-y-2" data-testid="share-inactive-list">
{inactiveLinks.map((link) => (
{activeLinks.length === 0 ? (
<EmptyState
size="compact"
data-testid="share-active-empty"
title={t("settings.sharing.noActive")}
/>
) : (
<ul className="space-y-2" data-testid="share-active-list">
{activeLinks.map((link) => (
<li
key={link.id}
className="bg-muted/20 border-border space-y-1.5 rounded-lg border p-3"
className="bg-muted/30 border-border space-y-2 rounded-lg border p-3"
>
<div className="flex flex-wrap items-start justify-between gap-2">
<p className="min-w-0 flex-1 text-sm font-medium break-words">
{link.label}
</p>
<Badge variant="secondary" className="text-xs">
{link.needsReselection
? t("settings.sharing.statusNeedsReselect")
: link.revokedAt
? t("settings.sharing.statusRevoked")
: t("settings.sharing.statusExpired")}
</Badge>
</div>
{/* A link whose frozen scope predates the selection model.
It served a scope nobody chose, so it was closed; the
owner re-mints above with the label, window and expiry
still in front of them. */}
{link.needsReselection ? (
<div
className="space-y-1.5"
data-testid={`share-needs-reselect-${link.id}`}
>
<p className="text-muted-foreground text-xs">
{t("settings.sharing.reselectReason")}
</p>
<Button
type="button"
variant="outline"
size="sm"
className="min-h-11 sm:min-h-9"
data-testid={`share-reselect-${link.id}`}
onClick={() => setPrefill(prefillFrom(link))}
>
{t("settings.sharing.reselectAction")}
</Button>
<div className="flex flex-wrap items-center gap-1.5">
<Badge className="bg-success/15 text-success text-xs">
{t("settings.sharing.statusActive")}
</Badge>
{link.protected && (
<Badge
variant="outline"
className="gap-1 text-xs"
data-testid="share-protected-badge"
>
<KeyRound className="h-2.5 w-2.5" />
{t("settings.sharing.protected")}
</Badge>
)}
{link.documentCount > 0 && (
<Badge
variant="outline"
className="gap-1 text-xs"
data-testid="share-doc-count-badge"
aria-label={t("settings.sharing.documentCount", {
count: link.documentCount,
})}
>
<FileText className="h-2.5 w-2.5" />
{link.documentCount}
</Badge>
)}
</div>
) : null}
</div>
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.created")}:
</span>{" "}
{formatDate(link.createdAt)}
</p>
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.expires")}:
</span>{" "}
{formatDateTime(link.expiresAt)}
</p>
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.accessCount")}:
</span>{" "}
{link.accessCount}
{link.lastAccessAt
? ` · ${formatDateTime(link.lastAccessAt)}`
: ""}
</p>
{/* §12 — a right-aligned row action, not a full-width
destructive strip stacked once per link (which reads as
an alarm wall on a list of shares). */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="ml-auto flex min-h-11 w-fit sm:min-h-9"
>
<Trash2 className="mr-2 h-3.5 w-3.5" />
{t("settings.sharing.revoke")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("settings.sharing.revoke")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("settings.sharing.revokeDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("common.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => revokeMutation.mutate(link.id)}
>
{t("settings.sharing.revoke")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</li>
))}
</ul>
)}
</div>
)}
</>
)}
</SettingsCard>
{inactiveLinks.length > 0 && (
<div className="space-y-2">
<button
type="button"
onClick={() => setShowRevoked((prev) => !prev)}
className="text-foreground hover:text-primary text-sm font-medium transition-colors"
>
{t("settings.sharing.inactiveTitle", {
count: inactiveLinks.length,
})}
</button>
{showRevoked && (
<ul className="space-y-2" data-testid="share-inactive-list">
{inactiveLinks.map((link) => (
<li
key={link.id}
className="bg-muted/20 border-border space-y-1.5 rounded-lg border p-3"
>
<div className="flex flex-wrap items-start justify-between gap-2">
<p className="min-w-0 flex-1 text-sm font-medium break-words">
{link.label}
</p>
<Badge variant="secondary" className="text-xs">
{link.needsReselection
? t("settings.sharing.statusNeedsReselect")
: link.revokedAt
? t("settings.sharing.statusRevoked")
: t("settings.sharing.statusExpired")}
</Badge>
</div>
{/* A link whose frozen scope predates the selection model.
It served a scope nobody chose, so it was closed; the
owner re-mints above with the label, window and expiry
still in front of them. */}
{link.needsReselection ? (
<div
className="space-y-1.5"
data-testid={`share-needs-reselect-${link.id}`}
>
<p className="text-muted-foreground text-xs">
{t("settings.sharing.reselectReason")}
</p>
<Button
type="button"
variant="outline"
size="sm"
className="min-h-11 sm:min-h-9"
data-testid={`share-reselect-${link.id}`}
onClick={() => {
setPrefill(prefillFrom(link));
// The form it fills sits in the card above.
document
.getElementById("share-link-create")
?.scrollIntoView({ block: "start" });
}}
>
{t("settings.sharing.reselectAction")}
</Button>
</div>
) : null}
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.accessCount")}:
</span>{" "}
{link.accessCount}
</p>
</li>
))}
</ul>
)}
</div>
)}
</>
)}
</SettingsCard>
</>
);
}