feat(drill): ProviderDrawer — Claims + Activity tabs from extended /providers/{npi}
This commit is contained in:
Generated
+62
@@ -12,6 +12,7 @@
|
|||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.15",
|
||||||
"@tanstack/react-query": "^5.101.0",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"ansi-styles": "^6.2.3",
|
"ansi-styles": "^6.2.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
@@ -1331,6 +1332,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz",
|
||||||
|
"integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.4",
|
||||||
|
"@radix-ui/react-collection": "1.1.10",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.3",
|
||||||
|
"@radix-ui/react-context": "1.1.4",
|
||||||
|
"@radix-ui/react-direction": "1.1.2",
|
||||||
|
"@radix-ui/react-id": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.6",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.2",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.1.tgz",
|
||||||
@@ -1393,6 +1425,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.4",
|
||||||
|
"@radix-ui/react-context": "1.1.4",
|
||||||
|
"@radix-ui/react-direction": "1.1.2",
|
||||||
|
"@radix-ui/react-id": "1.1.2",
|
||||||
|
"@radix-ui/react-presence": "1.1.6",
|
||||||
|
"@radix-ui/react-primitive": "2.1.6",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.13",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.15",
|
||||||
"@tanstack/react-query": "^5.101.0",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"ansi-styles": "^6.2.3",
|
"ansi-styles": "^6.2.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
|||||||
@@ -44,6 +44,51 @@ const SAMPLE_PROVIDER: Provider = {
|
|||||||
outstandingAr: 12450,
|
outstandingAr: 12450,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended provider fixture (SP21 Task 3.1) — adds the
|
||||||
|
* `recent_claims` and `recent_activity` slices the Claims/Activity
|
||||||
|
* tabs read from. Used by the tabs tests below. Field shapes match
|
||||||
|
* `ClaimSummary` and `ActivityEvent` from `@/types`.
|
||||||
|
*/
|
||||||
|
const SAMPLE_PROVIDER_WITH_DETAILS: Provider = {
|
||||||
|
...SAMPLE_PROVIDER,
|
||||||
|
recent_claims: [
|
||||||
|
{
|
||||||
|
id: "CLM-0001",
|
||||||
|
state: "submitted",
|
||||||
|
billedAmount: 250,
|
||||||
|
patientName: "Jane Q Patient",
|
||||||
|
providerNpi: "1881068062",
|
||||||
|
payerName: "Aetna",
|
||||||
|
cptCode: "99213",
|
||||||
|
submissionDate: "2026-06-20",
|
||||||
|
parsedAt: "2026-06-20",
|
||||||
|
status: "submitted",
|
||||||
|
batchId: "batch-001",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
recent_activity: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
ts: "2026-06-20T15:30:00Z",
|
||||||
|
kind: "claim_submitted",
|
||||||
|
batchId: "batch-001",
|
||||||
|
claimId: "CLM-0001",
|
||||||
|
remittanceId: null,
|
||||||
|
payload: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
ts: "2026-06-20T15:35:00Z",
|
||||||
|
kind: "claim_paid",
|
||||||
|
batchId: "batch-001",
|
||||||
|
claimId: "CLM-0001",
|
||||||
|
remittanceId: null,
|
||||||
|
payload: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the mocked hook's return value for a single test. The
|
* Configure the mocked hook's return value for a single test. The
|
||||||
* `refetch` default is a fresh `vi.fn()` — tests that need to assert
|
* `refetch` default is a fresh `vi.fn()` — tests that need to assert
|
||||||
@@ -193,4 +238,66 @@ describe("ProviderDrawer", () => {
|
|||||||
fireEvent.click(closeBtn!);
|
fireEvent.click(closeBtn!);
|
||||||
expect(onClose).toHaveBeenCalledTimes(1);
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -- Tabs (SP21 Task 3.1) -------------------------------------------------
|
||||||
|
//
|
||||||
|
// Radix Tabs only mounts the active `Tabs.Content` into the DOM (no
|
||||||
|
// `forceMount`), so clicking a tab trigger causes the previous panel to
|
||||||
|
// unmount and the new one to mount. The tests below rely on that —
|
||||||
|
// "panel X renders" is asserted by checking that unique text from that
|
||||||
|
// panel's component is present in `document.body` after the click.
|
||||||
|
|
||||||
|
it("test_renders_three_tabs_overview_claims_activity", () => {
|
||||||
|
mockDetail({ data: SAMPLE_PROVIDER_WITH_DETAILS });
|
||||||
|
render(<ProviderDrawer npi="1881068062" onClose={() => {}} />);
|
||||||
|
|
||||||
|
// All three tab triggers are present, with the spec-mandated labels
|
||||||
|
// in the spec-mandated order.
|
||||||
|
const triggers = Array.from(document.querySelectorAll('[role="tab"]'));
|
||||||
|
expect(triggers.length).toBe(3);
|
||||||
|
expect(triggers.map((t) => t.textContent)).toEqual([
|
||||||
|
"Overview",
|
||||||
|
"Claims",
|
||||||
|
"Activity",
|
||||||
|
]);
|
||||||
|
// Overview is the default; its trigger must be selected on mount.
|
||||||
|
expect(triggers[0]?.getAttribute("aria-selected")).toBe("true");
|
||||||
|
expect(triggers[0]?.getAttribute("data-state")).toBe("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("test_switches_to_claims_tab_and_shows_recent_claims", () => {
|
||||||
|
mockDetail({ data: SAMPLE_PROVIDER_WITH_DETAILS });
|
||||||
|
render(<ProviderDrawer npi="1881068062" onClose={() => {}} />);
|
||||||
|
|
||||||
|
const claimsTrigger = Array.from(document.querySelectorAll('[role="tab"]'))
|
||||||
|
.find((t) => t.textContent === "Claims");
|
||||||
|
expect(claimsTrigger).not.toBeNull();
|
||||||
|
// Radix Tabs wires `onMouseDown` (not `onClick`) to its
|
||||||
|
// `onValueChange` handler — fire the matching event so the tab
|
||||||
|
// actually activates under happy-dom.
|
||||||
|
fireEvent.mouseDown(claimsTrigger!);
|
||||||
|
|
||||||
|
// Claims tab is now selected — and ProviderRecentClaims content is in
|
||||||
|
// the DOM (claim id + patient name + the "View all claims" link).
|
||||||
|
expect(claimsTrigger?.getAttribute("aria-selected")).toBe("true");
|
||||||
|
expect(document.body.textContent).toContain("CLM-0001");
|
||||||
|
expect(document.body.textContent).toContain("Jane Q Patient");
|
||||||
|
expect(document.body.textContent).toContain("View all claims");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("test_switches_to_activity_tab_and_shows_recent_activity", () => {
|
||||||
|
mockDetail({ data: SAMPLE_PROVIDER_WITH_DETAILS });
|
||||||
|
render(<ProviderDrawer npi="1881068062" onClose={() => {}} />);
|
||||||
|
|
||||||
|
const activityTrigger = Array.from(document.querySelectorAll('[role="tab"]'))
|
||||||
|
.find((t) => t.textContent === "Activity");
|
||||||
|
expect(activityTrigger).not.toBeNull();
|
||||||
|
fireEvent.mouseDown(activityTrigger!);
|
||||||
|
|
||||||
|
// Activity tab is now selected — and ProviderRecentActivity content
|
||||||
|
// is in the DOM (both `kind` strings from the fixture).
|
||||||
|
expect(activityTrigger?.getAttribute("aria-selected")).toBe("true");
|
||||||
|
expect(document.body.textContent).toContain("claim_submitted");
|
||||||
|
expect(document.body.textContent).toContain("claim_paid");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
|
import { Tabs } from "@/components/ui/tabs";
|
||||||
import { ApiError } from "@/lib/api";
|
import { ApiError } from "@/lib/api";
|
||||||
import { DrillDrawerHeader } from "@/components/drill/DrillDrawerHeader";
|
import { DrillDrawerHeader } from "@/components/drill/DrillDrawerHeader";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useProviderDetail } from "@/hooks/useProviderDetail";
|
import { useProviderDetail } from "@/hooks/useProviderDetail";
|
||||||
import { ProviderOverview } from "./ProviderOverview";
|
import { ProviderOverview } from "./ProviderOverview";
|
||||||
|
import { ProviderRecentClaims } from "./ProviderRecentClaims";
|
||||||
|
import { ProviderRecentActivity } from "./ProviderRecentActivity";
|
||||||
import { ProviderDrawerError } from "./ProviderDrawerError";
|
import { ProviderDrawerError } from "./ProviderDrawerError";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -12,12 +15,17 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider drill-down drawer (SP21 Task 2.2).
|
* Provider drill-down drawer (SP21 Task 2.2 + 3.1).
|
||||||
*
|
*
|
||||||
* Side-panel shell that consumes ``useProviderDetail(npi)`` and renders
|
* Side-panel shell that consumes ``useProviderDetail(npi)`` and renders
|
||||||
* the Overview tab content (Phase 2 ships only this tab; Phase 3 adds
|
* a three-tab body:
|
||||||
* Claims/Activity tabs once ``recent_claims`` and ``recent_activity``
|
*
|
||||||
* rendering lands).
|
* - Overview — base provider fields (identity + activity shape)
|
||||||
|
* - Claims — top-10 claims joined to this provider
|
||||||
|
* (extended `/api/config/providers/{npi}.recent_claims`,
|
||||||
|
* populated by Task 1.6)
|
||||||
|
* - Activity — top-10 events joined to this provider's claims
|
||||||
|
* (extended `/api/config/providers/{npi}.recent_activity`)
|
||||||
*
|
*
|
||||||
* Layout mirrors the ClaimDrawer / RemitDrawer pattern: Radix Dialog
|
* Layout mirrors the ClaimDrawer / RemitDrawer pattern: Radix Dialog
|
||||||
* repositioned to the right edge as a fixed-height side panel, with
|
* repositioned to the right edge as a fixed-height side panel, with
|
||||||
@@ -76,9 +84,22 @@ export function ProviderDrawer({ npi, onClose }: Props) {
|
|||||||
title={data.name}
|
title={data.name}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
<div className="p-6">
|
<Tabs.Root defaultValue="overview" className="px-6 py-4">
|
||||||
<ProviderOverview provider={data} />
|
<Tabs.List>
|
||||||
</div>
|
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="claims">Claims</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="overview">
|
||||||
|
<ProviderOverview provider={data} />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="claims">
|
||||||
|
<ProviderRecentClaims provider={data} />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="activity">
|
||||||
|
<ProviderRecentActivity provider={data} />
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Provider } from "@/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity tab content for the ProviderDrawer (SP21 Task 3.1).
|
||||||
|
*
|
||||||
|
* STUB — renders the top-10 `recent_activity` events as a flat list
|
||||||
|
* (`ts` + `kind`). Real activity-event routing (linking each event to
|
||||||
|
* its source claim/remit/provider) arrives in Phase 4 once
|
||||||
|
* `provider_added` and `remit_received` events get their drawer
|
||||||
|
* surfaces; for now the events aren't drillable from this view —
|
||||||
|
* clicking them does nothing. The ProviderDrawer's Overview already
|
||||||
|
* surfaces the provider's `provider_added` event via the Dashboard
|
||||||
|
* activity feed (Task 2.5).
|
||||||
|
*/
|
||||||
|
export function ProviderRecentActivity({ provider }: { provider: Provider }) {
|
||||||
|
const items = provider.recent_activity ?? [];
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-muted-foreground text-[13px]">No recent activity.</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ul className="space-y-1.5 text-[12.5px]">
|
||||||
|
{items.map((a) => (
|
||||||
|
<li key={a.id} className="flex items-center gap-2">
|
||||||
|
<span className="mono text-[10.5px] text-muted-foreground">
|
||||||
|
{new Date(a.ts).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span>{a.kind}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Provider } from "@/types";
|
||||||
|
import { fmt } from "@/lib/format";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims tab content for the ProviderDrawer (SP21 Task 3.1).
|
||||||
|
*
|
||||||
|
* Reads `provider.recent_claims` — the top-10 claims joined to this
|
||||||
|
* provider from the extended `GET /api/config/providers/{npi}` endpoint
|
||||||
|
* (Task 1.6). Renders one row per claim (`id` + `patientName` +
|
||||||
|
* `billedAmount`) with a "View all claims →" link to the global claims
|
||||||
|
* page at the bottom. Falls back to an empty-state line when the
|
||||||
|
* provider has no recent claims (e.g. legacy callers that hit the
|
||||||
|
* detail endpoint without the recent-claims slice).
|
||||||
|
*/
|
||||||
|
export function ProviderRecentClaims({ provider }: { provider: Provider }) {
|
||||||
|
const claims = provider.recent_claims ?? [];
|
||||||
|
if (claims.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-muted-foreground text-[13px]">No recent claims.</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{claims.map((c) => (
|
||||||
|
<div
|
||||||
|
key={c.id}
|
||||||
|
className="flex items-center gap-3 py-2 border-b border-border/30 last:border-0"
|
||||||
|
>
|
||||||
|
<div className="display mono text-[12.5px] w-32 shrink-0">{c.id}</div>
|
||||||
|
<div className="flex-1 min-w-0 text-[12.5px] text-muted-foreground truncate">
|
||||||
|
{c.patientName ?? "—"}
|
||||||
|
</div>
|
||||||
|
<div className="display mono text-[13px]">
|
||||||
|
{fmt.usd(c.billedAmount)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<a href="/claims" className="text-[12.5px] text-accent hover:underline">
|
||||||
|
View all claims →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tabs primitive — SP21 Universal Drill-Down (Task 3.1).
|
||||||
|
*
|
||||||
|
* Thin wrapper over `@radix-ui/react-tabs`. Mirrors the same re-export
|
||||||
|
* pattern used by `dialog.tsx`: the individual primitives are forwardRef'd
|
||||||
|
* styled components, and the `Tabs` namespace object lets callers use
|
||||||
|
* the spec's `<Tabs.Root>`, `<Tabs.List>`, `<Tabs.Trigger>`,
|
||||||
|
* `<Tabs.Content>` dot-notation directly:
|
||||||
|
*
|
||||||
|
* import { Tabs } from "@/components/ui/tabs";
|
||||||
|
* <Tabs.Root defaultValue="overview">
|
||||||
|
* <Tabs.List>
|
||||||
|
* <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
||||||
|
* ...
|
||||||
|
* </Tabs.List>
|
||||||
|
* <Tabs.Content value="overview">...</Tabs.Content>
|
||||||
|
* </Tabs.Root>
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex gap-2 border-b border-border/30 mb-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-2 text-[12.5px] text-muted-foreground",
|
||||||
|
"data-[state=active]:text-foreground",
|
||||||
|
"data-[state=active]:border-b-2 data-[state=active]:border-accent",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace export matching the spec's `import * as Tabs from ...`
|
||||||
|
* dot-notation. `Root` is the unstyled Radix primitive; the rest are the
|
||||||
|
* styled wrappers above.
|
||||||
|
*/
|
||||||
|
export const Tabs = {
|
||||||
|
Root: TabsPrimitive.Root,
|
||||||
|
List: TabsList,
|
||||||
|
Trigger: TabsTrigger,
|
||||||
|
Content: TabsContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TabsList, TabsTrigger, TabsContent };
|
||||||
Reference in New Issue
Block a user