Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 31 additions & 31 deletions src/components/clinical-dashboard/account-setup-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { type FormEvent, useRef, useState } from "react";
import { Clock3, FileText, Heart, LockKeyhole, Mail, ShieldCheck, SlidersHorizontal, X } from "lucide-react";
import { Clock3, Heart, LockKeyhole, Mail, ShieldCheck, SlidersHorizontal, X } from "lucide-react";

import { BrandMark } from "@/components/clinical-dashboard/brand";
import { ProviderBrandIcon, type SsoProvider } from "@/components/clinical-dashboard/provider-brand-icons";
Expand All @@ -19,8 +19,8 @@ import { useAuthSession } from "@/lib/supabase/client";

const accountBenefits = [
{
label: "Search history",
detail: "Pick up recent questions on any device.",
label: "Local recents",
detail: "Recent questions stay in this browser session.",
icon: Clock3,
},
{
Expand All @@ -29,9 +29,9 @@ const accountBenefits = [
icon: SlidersHorizontal,
},
{
label: "Saved sources",
detail: "Keep the guidelines you rely on close.",
icon: FileText,
label: "Saved favourites",
detail: "Reopen favourite clinical tools across signed-in devices.",
icon: Heart,
},
] as const;

Expand All @@ -42,8 +42,8 @@ const favouritesAccountBenefits = [
icon: Heart,
},
{
label: "Search history",
detail: "Pick up recent questions on any device.",
label: "Local recents",
detail: "Recent questions stay in this browser session.",
icon: Clock3,
},
{
Expand All @@ -55,8 +55,8 @@ const favouritesAccountBenefits = [

const securitySummary = [
{
label: "Private workspace",
detail: "Your data stays private and is never shared.",
label: "Account-scoped saves",
detail: "Favourites and preferences are stored with your account.",
icon: ShieldCheck,
},
{
Expand Down Expand Up @@ -84,37 +84,26 @@ export function AccountSetupDialog({
const auth = useAuthSession();
const emailInputRef = useRef<HTMLInputElement>(null);
const [email, setEmail] = useState("");
const [providerNotice, setProviderNotice] = useState<string | null>(null);
const [emailAttempted, setEmailAttempted] = useState(false);
const busy = auth.status === "loading";
const statusMessage = providerNotice ?? (emailAttempted ? auth.error : null);
// Only a submit error (not the SSO provider notice) marks the email field
// invalid and is associated to it, so assistive tech ties the alert to the
// control the user must correct.
const emailHasError = !providerNotice && Boolean(statusMessage);
const statusMessage = emailAttempted ? auth.error : null;
const emailHasError = Boolean(statusMessage);
const isFavouritesIntent = intent === "favourites";
const benefits = isFavouritesIntent ? favouritesAccountBenefits : accountBenefits;
const title = isFavouritesIntent ? "Sign up to save favourites" : "Set up your workspace";
const subtitle = isFavouritesIntent
? "Create an account to save clinical favourites and access them across devices."
: "Sync source preferences, search history, and clinical defaults across devices.";
const benefitsHeading = isFavouritesIntent
? "Favourites stay with your account"
: "Everything syncs across your devices";
: "Sync favourites and clinical defaults across signed-in devices. Recent searches stay in this browser session.";
const benefitsHeading = isFavouritesIntent ? "Favourites stay with your account" : "What your account saves";

async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const trimmedEmail = email.trim();
if (!trimmedEmail) return;
setProviderNotice(null);
setEmailAttempted(true);
await auth.signInWithEmail(trimmedEmail);
}

function chooseProvider(provider: SsoProvider) {
setProviderNotice(`${provider} sign-in is not connected yet. Continue with email to set up this workspace.`);
}

return (
<Sheet
open={open}
Expand Down Expand Up @@ -193,15 +182,18 @@ export function AccountSetupDialog({
<div className="grid gap-3">
<div className="flex items-center gap-3 text-xs font-medium text-[color:var(--text-soft)]">
<span className="h-px flex-1 bg-[color:var(--border)]" />
<span>or continue with</span>
<span>Social sign-in unavailable</span>
<span className="h-px flex-1 bg-[color:var(--border)]" />
</div>

<div className="grid grid-cols-3 gap-2">
{(["Apple", "Google", "Microsoft"] as const).map((provider) => (
<ProviderButton key={provider} provider={provider} onClick={() => chooseProvider(provider)} />
<ProviderButton key={provider} provider={provider} />
))}
</div>
<p className={cn("text-center text-xs leading-5", textMuted)}>
Continue with email. Social sign-in is not available in this setup.
</p>
</div>

<section
Expand Down Expand Up @@ -272,7 +264,7 @@ export function AccountSetupDialog({
{statusMessage ? (
<p
id="account-setup-status"
role={providerNotice ? "status" : "alert"}
role="alert"
className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-inset)] px-3 py-2 text-xs font-medium leading-5 text-[color:var(--text-muted)]"
>
{statusMessage}
Expand All @@ -296,15 +288,23 @@ export function AccountSetupDialog({
);
}

function ProviderButton({ provider, onClick }: { provider: SsoProvider; onClick: () => void }) {
function ProviderButton({ provider }: { provider: SsoProvider }) {
const descriptionId = `account-${provider.toLowerCase()}-sign-in-unavailable`;

return (
<button
type="button"
onClick={onClick}
className="flex min-h-tap min-w-0 items-center justify-center gap-1.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-lux)] px-1.5 text-xs font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] min-[375px]:gap-2 min-[375px]:px-2 sm:text-sm"
disabled
title={`${provider} sign-in is unavailable — coming soon`}
aria-label={`${provider} sign-in unavailable`}
aria-describedby={descriptionId}
className="flex min-h-tap min-w-0 items-center justify-center gap-1.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-lux)] px-1.5 text-xs font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:bg-[color:var(--surface-inset)] disabled:text-[color:var(--disabled)] disabled:opacity-75 disabled:shadow-none min-[375px]:gap-2 min-[375px]:px-2 sm:text-sm"
>
<ProviderBrandIcon provider={provider} className="h-5 w-5" />
<span className="min-w-0 text-2xs leading-none min-[375px]:text-xs sm:text-sm">{provider}</span>
<span id={descriptionId} className="sr-only">
{provider} sign-in is unavailable. Continue with email.
</span>
</button>
);
}
55 changes: 32 additions & 23 deletions src/components/clinical-dashboard/auth-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type FormEvent, useState, useSyncExternalStore } from "react";
import {
ChevronRight,
Clock3,
FileText,
Heart,
LogOut,
Mail,
ShieldAlert,
Expand All @@ -14,7 +14,7 @@ import {
} from "lucide-react";

import { ProviderBrandIcon } from "@/components/clinical-dashboard/provider-brand-icons";
import { AUTH_EMAIL_STORAGE_KEY, type OAuthProvider, useAuthSession } from "@/lib/supabase/client";
import { AUTH_EMAIL_STORAGE_KEY, useAuthSession } from "@/lib/supabase/client";
import {
AsyncButton,
cn,
Expand Down Expand Up @@ -65,7 +65,6 @@ export function AuthPanel() {
const { status, error, notice, isConfigured, signInWithEmail, signInWithOAuth, signOut, session } = useAuthSession();
const savedEmail = useSyncExternalStore(subscribeAuthEmail, getAuthEmailSnapshot, getServerAuthEmailSnapshot);
const [draftEmail, setDraftEmail] = useState<string | null>(null);
const [providerNotice, setProviderNotice] = useState<string | null>(null);
const [emailError, setEmailError] = useState<string | null>(null);
const email = draftEmail ?? savedEmail;
const busy = status === "loading";
Expand All @@ -85,19 +84,11 @@ export function AuthPanel() {
return;
}
setEmailError(null);
setProviderNotice(null);
await signInWithEmail(trimmed);
}

async function chooseProvider(provider: "Apple" | "Google" | "Microsoft") {
setProviderNotice(null);
const providerId: OAuthProvider | null =
provider === "Google" ? "google" : provider === "Microsoft" ? "azure" : null;
if (providerId) {
await signInWithOAuth(providerId);
return;
}
setProviderNotice(`${provider} sign-in is a placeholder for now. Continue with email to use this workspace.`);
async function chooseProvider(provider: "Google" | "Microsoft") {
await signInWithOAuth(provider === "Google" ? "google" : "azure");
}

if (!isConfigured) {
Expand Down Expand Up @@ -157,7 +148,7 @@ export function AuthPanel() {
<p className={cn("mt-1 text-sm leading-5", textMuted)}>
{isExpired
? "Send a fresh link if this one failed or timed out."
: "Save searches, source history, and clinical defaults. Do not enter PHI."}
: "Save favourites and clinical defaults across signed-in devices. Do not enter PHI."}
</p>
</div>
</div>
Expand Down Expand Up @@ -226,40 +217,58 @@ export function AuthPanel() {
</div>

<div className="grid gap-2">
<ProviderButton provider="Apple" onClick={() => chooseProvider("Apple")} />
<ProviderButton provider="Apple" disabled />
<ProviderButton provider="Google" onClick={() => chooseProvider("Google")} />
<ProviderButton provider="Microsoft" onClick={() => chooseProvider("Microsoft")} />
</div>

<p id="auth-apple-sign-in-unavailable" className="sr-only">
Apple sign-in is unavailable. Continue with email, Google, or Microsoft.
</p>

<div className="grid grid-cols-3 gap-2 rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-subtle)] p-2 shadow-[var(--shadow-inset)]">
<AuthBenefit icon={SlidersHorizontal} label="Clinical defaults" />
<AuthBenefit icon={Clock3} label="Source history" />
<AuthBenefit icon={FileText} label="Saved sources" />
<AuthBenefit icon={Heart} label="Saved favourites" />
<AuthBenefit icon={Clock3} label="Local recents" />
</div>

<p className="flex items-start gap-2 text-xs font-medium leading-5 text-[color:var(--text-muted)]">
<ShieldCheck aria-hidden="true" className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[color:var(--clinical-accent)]" />
Accounts save preferences and search history. No PHI is required.
Accounts save favourites and preferences. Recent searches stay in this browser session. No PHI is required.
</p>

{notice && <InlineNotice tone="success">{notice}</InlineNotice>}
{providerNotice && <InlineNotice tone="info">{providerNotice}</InlineNotice>}
{error && <InlineNotice tone="danger">{error}</InlineNotice>}
</div>
</form>
);
}

function ProviderButton({ provider, onClick }: { provider: "Apple" | "Google" | "Microsoft"; onClick: () => void }) {
function ProviderButton({
provider,
onClick,
disabled = false,
}: {
provider: "Apple" | "Google" | "Microsoft";
onClick?: () => void;
disabled?: boolean;
}) {
return (
<button
type="button"
onClick={onClick}
className="flex min-h-tap w-full items-center gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-lux)] px-3 text-left text-sm font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
disabled={disabled}
title={disabled ? "Apple sign-in is unavailable — coming soon" : undefined}
aria-describedby={disabled ? "auth-apple-sign-in-unavailable" : undefined}
className="flex min-h-tap w-full items-center gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-lux)] px-3 text-left text-sm font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:bg-[color:var(--surface-inset)] disabled:text-[color:var(--disabled)] disabled:opacity-75 disabled:shadow-none"
>
<ProviderMark provider={provider} />
<span className="min-w-0 flex-1 truncate">Continue with {provider}</span>
<ChevronRight aria-hidden="true" className="h-4 w-4 shrink-0 text-[color:var(--text-soft)]" />
<span className="min-w-0 flex-1 truncate">
{disabled ? `${provider} sign-in unavailable` : `Continue with ${provider}`}
</span>
{!disabled ? (
<ChevronRight aria-hidden="true" className="h-4 w-4 shrink-0 text-[color:var(--text-soft)]" />
) : null}
</button>
);
}
Expand Down
34 changes: 24 additions & 10 deletions src/components/clinical-dashboard/settings-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,8 @@ export function SettingsDialog({
setAccountNotice(null);
}

async function chooseSettingsProvider(provider: "Apple" | "Google" | "Microsoft") {
async function chooseSettingsProvider(provider: "Google" | "Microsoft") {
setAccountNotice(null);
if (provider === "Apple") {
setAccountNotice("Apple sign-in is not configured. Continue with email, Google, or Microsoft.");
return;
}
await auth.signInWithOAuth(provider === "Google" ? "google" : "azure");
}

Expand Down Expand Up @@ -478,7 +474,10 @@ export function SettingsDialog({
</div>

<div className="grid gap-2">
<SettingsProviderRow provider="Apple" onClick={() => void chooseSettingsProvider("Apple")} />
<SettingsProviderRow
provider="Apple"
disabledReason="Apple sign-in is unavailable. Continue with email, Google, or Microsoft."
/>
<SettingsProviderRow provider="Google" onClick={() => void chooseSettingsProvider("Google")} />
<SettingsProviderRow
provider="Microsoft"
Expand Down Expand Up @@ -1144,17 +1143,25 @@ function SettingsChip({ label }: { label: string }) {
function SettingsProviderRow({
provider,
onClick,
disabledReason,
}: {
provider: "Apple" | "Google" | "Microsoft" | "email";
onClick: () => void;
onClick?: () => void;
disabledReason?: string;
}) {
const label = provider === "email" ? "Use email instead" : provider;
const label =
provider === "email" ? "Use email instead" : disabledReason ? `${provider} sign-in unavailable` : provider;
const descriptionId = disabledReason ? `settings-provider-${provider.toLowerCase()}-unavailable` : undefined;

return (
<button
type="button"
onClick={onClick}
className="flex min-h-12 w-full items-center gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] px-3 text-left text-sm font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
disabled={Boolean(disabledReason)}
title={disabledReason ? `${disabledReason.replace(/\.$/, "")} — coming soon` : undefined}
aria-label={label}
aria-describedby={descriptionId}
className="flex min-h-12 w-full items-center gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] px-3 text-left text-sm font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:bg-[color:var(--surface-inset)] disabled:text-[color:var(--disabled)] disabled:opacity-75 disabled:shadow-none"
>
{provider === "email" ? (
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)]">
Expand All @@ -1164,7 +1171,14 @@ function SettingsProviderRow({
<ProviderBrandMark provider={provider} />
)}
<span className="min-w-0 flex-1 truncate">{label}</span>
<ChevronRight aria-hidden="true" className="h-4 w-4 shrink-0 text-[color:var(--text-soft)]" />
{disabledReason ? (
<span id={descriptionId} className="sr-only">
{disabledReason}
</span>
) : null}
{!disabledReason ? (
<ChevronRight aria-hidden="true" className="h-4 w-4 shrink-0 text-[color:var(--text-soft)]" />
) : null}
</button>
);
}
Expand Down
Loading