Add command palette, footer clinic menu, patient Sheet, Analysis page, favicon

Several navigation/UX additions:

- Command palette (⌘K): components/command-palette.tsx wraps the app shell
  (mounted in app/(app)/layout.tsx) with a controlled COSS CommandDialog listing
  the nav pages; a "Quick nav" Kbd button in the sidebar footer also opens it.
  Installed @coss/kbd. Extracted the nav list into lib/nav.ts so the sidebar and
  palette share one source of truth.
- Clinic switcher moved from the sidebar body into the footer; its menu now
  opens a read-only "Clinic info" dialog and a "Create clinic" dialog instead of
  routing to /onboarding. Shared CreateClinicForm (components/clinic/) is reused
  by onboarding.
- Patients: clicking a row opens a right-side Sheet with the full record
  (components/patients/patient-detail-sheet.tsx) instead of navigating to the
  chat; PatientResult gained a vertical "column" layout for the Sheet.
- New Analysis page (app/(app)/analysis/) — a mock dashboard (revenue/profit,
  patient volume, appointments, operations) reusing Sparkline + Card + Badge.
- Logo: set metadata.icons so the new mark appears as the browser-tab favicon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-04 19:40:11 +03:00
parent 2c5624e049
commit a4fc9a4fe9
15 changed files with 796 additions and 120 deletions
+10
View File
@@ -0,0 +1,10 @@
import { AnalysisView } from "@/components/analysis/analysis-view";
import { SidebarInset } from "@/components/ui/sidebar";
export default function AnalysisPage() {
return (
<SidebarInset className="flex flex-1 flex-col overflow-y-auto">
<AnalysisView />
</SidebarInset>
);
}
+9 -6
View File
@@ -1,4 +1,5 @@
import { AppAuthGuard } from "@/components/auth/app-auth-guard";
import { CommandPaletteProvider } from "@/components/command-palette";
import { DashboardSidebar } from "@/components/sidebar-02/app-sidebar";
import { SidebarProvider } from "@/components/ui/sidebar";
@@ -9,12 +10,14 @@ export default function AppLayout({
}) {
return (
<AppAuthGuard>
<SidebarProvider>
<div className="relative flex h-dvh w-full">
<DashboardSidebar />
{children}
</div>
</SidebarProvider>
<CommandPaletteProvider>
<SidebarProvider>
<div className="relative flex h-dvh w-full">
<DashboardSidebar />
{children}
</div>
</SidebarProvider>
</CommandPaletteProvider>
</AppAuthGuard>
);
}
+4 -77
View File
@@ -1,32 +1,16 @@
"use client";
import { useRouter } from "next/navigation";
import { type FormEvent, useEffect, useState } from "react";
import { useEffect } from "react";
import { AuthShell, Field, FormAlert } from "@/components/auth/auth-ui";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { AuthShell } from "@/components/auth/auth-ui";
import { CreateClinicForm } from "@/components/clinic/create-clinic-form";
import { authClient } from "@/lib/auth-client";
import { notify } from "@/lib/toast";
function slugify(value: string): string {
return value
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
export default function OnboardingPage() {
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
const [name, setName] = useState("");
const [slug, setSlug] = useState("");
const [slugEdited, setSlugEdited] = useState(false);
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
// Send unauthenticated users to login. Authenticated users (whether brand
// new or creating an additional clinic) stay on this page.
useEffect(() => {
@@ -34,69 +18,12 @@ export default function OnboardingPage() {
if (!session?.user) router.replace("/login");
}, [session, isPending, router]);
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
if (submitting) return;
setSubmitting(true);
setError(null);
const finalSlug = (slugEdited ? slug : slugify(name)) || slugify(name);
const { data: org, error: createErr } = await authClient.organization.create(
{ name: name.trim(), slug: finalSlug }
);
if (createErr || !org) {
const message = createErr?.message ?? "Could not create the clinic.";
setError(message);
notify.error("Couldn't create clinic", message);
setSubmitting(false);
return;
}
await authClient.organization.setActive({ organizationId: org.id });
notify.success("Clinic created", `${org.name} is ready.`);
router.push("/");
};
return (
<AuthShell
subtitle="Create your clinic to start organizing patient records"
title="Set up your clinic"
>
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
{error && <FormAlert>{error}</FormAlert>}
<Field htmlFor="name" label="Clinic name">
<Input
id="name"
onChange={(e) => {
setName(e.target.value);
if (!slugEdited) setSlug(slugify(e.target.value));
}}
placeholder="North Side Family Practice"
required
value={name}
/>
</Field>
<Field
hint="Used in links and invitations. Lowercase letters, numbers and dashes."
htmlFor="slug"
label="Clinic URL slug"
>
<Input
id="slug"
onChange={(e) => {
setSlugEdited(true);
setSlug(slugify(e.target.value));
}}
placeholder="north-side-family-practice"
required
value={slug}
/>
</Field>
<Button className="mt-1 w-full" disabled={submitting} type="submit">
{submitting ? "Creating clinic…" : "Create clinic"}
</Button>
</form>
<CreateClinicForm onCreated={() => router.push("/")} />
</AuthShell>
);
}
+1
View File
@@ -15,6 +15,7 @@ export const metadata: Metadata = {
title: "temetro — AI assistant for clinicians",
description:
"Retrieve patient information by simply asking. The open-source AI assistant for clinicians.",
icons: { icon: "/temetro-logo.png", apple: "/temetro-logo.png" },
};
export default function RootLayout({