diff --git a/package-lock.json b/package-lock.json index 9186c51..aceec29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,11 @@ "@radix-ui/react-hover-card": "^1.0.6", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.0.7", "@remixicon/react": "^4.2.0", @@ -3444,6 +3446,73 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-scroll-area/node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", @@ -4022,6 +4091,97 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-toast": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz", diff --git a/package.json b/package.json index 78c9c44..a8f5675 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,11 @@ "@radix-ui/react-hover-card": "^1.0.6", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.0.7", "@remixicon/react": "^4.2.0", diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/decent-homes/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/decent-homes/page.tsx new file mode 100644 index 0000000..825b63e --- /dev/null +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/decent-homes/page.tsx @@ -0,0 +1,515 @@ +import { + getPropertyMeta, + getDocument, + getEnergyAssessmentFromS3, +} from "../utils"; + +import { + Card, + CardHeader, + CardTitle, + CardContent, +} from "@/app/shadcn_components/ui/card"; +import { Badge } from "@/app/shadcn_components/ui/badge"; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from "@/app/shadcn_components/ui/tabs"; +import { Wrench } from "lucide-react"; + +const DISPLAY_NAMES: Record = { + // Criterion A - HHSRS hazards + damp_and_mould_growth: "Damp and Mould Growth", + excess_cold: "Excess Cold", + excess_heat: "Excess Heat", + asbestos_and_mm_fibres: "Asbestos and MM Fibres", + biocides: "Biocides", + carbon_monoxide: "Carbon Monoxide", + lead: "Lead", + radiation: "Radiation", + uncombusted_fuel_gas: "Uncombusted Fuel Gas", + volatile_organic_compounds: "Volatile Organic Compounds", + crowding_and_space: "Crowding and Space", + entry_by_intruders: "Entry by Intruders", + lighting: "Lighting", + noise: "Noise", + domestic_hygiene_pests_and_refuse: "Domestic Hygiene, Pests, and Refuse", + food_safety: "Food Safety", + personal_hygiene_sanitation_and_drainage: + "Personal Hygiene, Sanitation, and Drainage", + water_supply: "Water Supply", + falls_associated_with_baths: "Falls Associated with Baths", + falls_on_level_surfaces: "Falls on Level Surfaces", + falls_on_stairs_and_steps: "Falls on Stairs and Steps", + falls_between_levels: "Falls Between Levels", + electrical_hazards: "Electrical Hazards", + fire: "Fire", + flames_hot_surfaces_and_materials: "Flames, Hot Surfaces, and Materials", + collision_and_entrapment: "Collision and Entrapment", + explosions: "Explosions", + ergonomics: "Ergonomics", + structural_collapse_and_falling_elements: + "Structural Collapse and Falling Elements", + + // Criterion B - Key building components + wall_structure: "Wall Structure", + lintels: "Lintels", + wall_finish: "Wall Finish", + roof_structure: "Roof Structure", + roof_finish: "Roof Finish", + chimneys: "Chimneys", + windows: "Windows", + external_doors: "External Doors", + heating_other: "Other Heating Systems", + electrical_systems: "Electrical Systems", + kitchen: "Kitchen", + bathroom: "Bathroom", + + // Criterion C - Modern facilities + kitchen_less_than_20_years_old: "Kitchen Less Than 20 Years Old", + kitchen_adequate_space_and_layout: "Kitchen Has Adequate Space and Layout", + bathroom_less_than_30_years_old: "Bathroom Less Than 30 Years Old", + bathroom_wc_appropriately_located: "Bathroom/WC Appropriately Located", + adequate_external_noise_insulation: "Adequate External Noise Insulation", + + // Criterion D - Thermal comfort + efficient_heating_system_type: "Efficient Heating System Type", + efficient_heating_distribution: "Efficient Heating Distribution", + loft_insulation_sufficient: "Loft Insulation Sufficient", + wall_insulation_sufficient: "Wall Insulation Sufficient", +}; + +const SUB_ITEMS_TEXT: Record = { + // Criterion A - Hazards (keep as-is, not replacements) + damp_and_mould_growth: "Damp and Mould Growth", + excess_cold: "Excess Cold", + excess_heat: "Excess Heat", + asbestos_and_mm_fibres: "Asbestos and MM Fibres", + biocides: "Biocides", + carbon_monoxide: "Carbon Monoxide", + lead: "Lead", + radiation: "Radiation", + uncombusted_fuel_gas: "Uncombusted Fuel Gas", + volatile_organic_compounds: "Volatile Organic Compounds", + crowding_and_space: "Crowding and Space", + entry_by_intruders: "Entry by Intruders", + lighting: "Lighting", + noise: "Noise", + domestic_hygiene_pests_and_refuse: "Domestic Hygiene, Pests, and Refuse", + food_safety: "Food Safety", + personal_hygiene_sanitation_and_drainage: + "Personal Hygiene, Sanitation, and Drainage", + water_supply: "Water Supply", + falls_associated_with_baths: "Falls Associated with Baths", + falls_on_level_surfaces: "Falls on Level Surfaces", + falls_on_stairs_and_steps: "Falls on Stairs and Steps", + falls_between_levels: "Falls Between Levels", + electrical_hazards: "Electrical Hazards", + fire: "Fire", + flames_hot_surfaces_and_materials: "Flames, Hot Surfaces, and Materials", + collision_and_entrapment: "Collision and Entrapment", + explosions: "Explosions", + ergonomics: "Ergonomics", + structural_collapse_and_falling_elements: + "Structural Collapse and Falling Elements", + + // Criterion B - Key components + "Wall Structure in External Area": "Wall Structure Renewal", + "Lintels in External Area": "Lintel Renewal", + "Wall Finish 1 in External Area": "Wall Finish Renewal", + "Brickwork Pointing in External Area": "Brickwork Pointing Renewal", + "Roof Structure 1 in External Area": "Roof Structure Renewal", + "Fascia / Soffit / Bargeboard in External Area": + "Fascia / Soffit / Bargeboard Renewal", + "Gutters in External Area": "Gutter Renewal", + "Downpipes in External Area": "Downpipe Renewal", + "Roof Covering 1 in External Area": "Roof Covering Replacement", + "Chimneys in External Area": "Chimney Renewal", + "Windows in Property": "Window Replacement", + "Windows 1 in External Area": "Window Replacement", + "Type and Location of Front Door in Property": "Front Door Replacement", + "Back and Side Doors 1 in External Area": "Door Replacement", + "Back and Side Doors 2 in External Area": "Door Replacement", + "Type of Water Heating in Property": "Water Heating System Replacement", + "Electrics Required in Property": "Electrical System Renewal", + "Adequacy of Kitchen and Type in Property": "Kitchen Renewal", + "Adequacy of Bathroom Location in Property": "Bathroom Renewal", + + // Criterion C - Modern facilities + kitchen_less_than_20_years_old: "Kitchen Replacement", + kitchen_adequate_space_and_layout: "Kitchen Layout Upgrade", + bathroom_less_than_30_years_old: "Bathroom Replacement", + bathroom_wc_appropriately_located: "Bathroom/WC Layout Upgrade", + adequate_external_noise_insulation: "Noise Insulation Upgrade", + + // Criterion D - Thermal comfort + efficient_heating_system_type: "Heating System Upgrade", + efficient_heating_distribution: "Heating Distribution Upgrade", + loft_insulation_sufficient: "Loft Insulation Upgrade", + wall_insulation_sufficient: "Wall Insulation Upgrade", +}; + +const LABEL_MAP: Record = { + pass: "Pass", + fail: "Fail", + no_data: "Not Assessed", +}; + +function StatusBadge({ status }: { status: string }) { + const colors = + status === "pass" + ? "bg-green-600 hover:bg-green-700" + : status === "fail" + ? "bg-red-700 hover:bg-red-800" + : "bg-gray-500 hover:bg-gray-600"; + + return ( + + {LABEL_MAP[status]} + + ); +} + +// urgency badge +function UrgencyBadge({ label }: { label: string }) { + const colorMap: Record = { + Overdue: "bg-red-600", + "0–6 months": "bg-orange-500", + "6–12 months": "bg-yellow-500", + ">12 months": "bg-green-600", + }; + return ( + + {label} + + ); +} + +function CriterionContent({ + title, + items, +}: { + title: string; + items: { sub_variable: string; result: string }[]; +}) { + const sortedItems = [...items].sort((a, b) => { + const order = { fail: 0, no_data: 1, pass: 2 }; + return ( + order[a.result as keyof typeof order] - + order[b.result as keyof typeof order] + ); + }); + + return ( + + + {title} + + +
    + {sortedItems.map((item, idx) => ( +
  • + + {DISPLAY_NAMES[item.sub_variable] ?? item.sub_variable} + + +
  • + ))} +
+
+
+ + ); +} + +function ReplacementsContent({ + items, +}: { + items: { sub_variable: string; expiry_date: string | null }[]; +}) { + const today = new Date(); + const groups: Record< + string, + { sub_variable: string; expiry: Date; remaining: string }[] + > = { + Overdue: [], + "0–6 months": [], + "6–12 months": [], + ">12 months": [], + }; + + items.forEach((item) => { + if (!item.expiry_date) return; + const expiry = new Date(item.expiry_date); + const diffMs = expiry.getTime() - today.getTime(); + const diffMonths = diffMs / (1000 * 60 * 60 * 24 * 30); + const years = Math.floor(diffMonths / 12); + const months = Math.floor(diffMonths % 12); + const remaining = + diffMs < 0 + ? "Expired" + : `${years > 0 ? `${years}y ` : ""}${months}m remaining`; + + if (diffMs < 0) { + groups.Overdue.push({ + sub_variable: SUB_ITEMS_TEXT[item.sub_variable], + expiry, + remaining, + }); + } else if (diffMonths <= 6) { + groups["0–6 months"].push({ + sub_variable: SUB_ITEMS_TEXT[item.sub_variable], + expiry, + remaining, + }); + } else if (diffMonths <= 12) { + groups["6–12 months"].push({ + sub_variable: SUB_ITEMS_TEXT[item.sub_variable], + expiry, + remaining, + }); + } else { + groups[">12 months"].push({ + sub_variable: SUB_ITEMS_TEXT[item.sub_variable], + expiry, + remaining, + }); + } + }); + + // sort each group by expiry date ascending (soonest first) + Object.values(groups).forEach((comps) => { + comps.sort((a, b) => a.expiry.getTime() - b.expiry.getTime()); + }); + + // order of groups + const groupOrder: (keyof typeof groups)[] = [ + "Overdue", + "0–6 months", + "6–12 months", + ">12 months", + ]; + + return ( + + + Upcoming Replacements + + + {groupOrder.map((urgency) => + groups[urgency].length > 0 ? ( +
+
+ + + {groups[urgency].length} items + +
+
    + {groups[urgency].map((comp, idx) => ( +
  • + + {DISPLAY_NAMES[comp.sub_variable] ?? comp.sub_variable} + + + {comp.remaining} + +
  • + ))} +
+
+ ) : null + )} +
+
+ + ); +} + +function DecentHomesSummary({ + decentHomes, + decentHomesMeta, +}: { + decentHomes: { + uprn: number; + creation_date: string; + criterion_a: string; + criterion_b: string; + criterion_c: string; + criterion_d: string; + decent_homes: string; + }; + decentHomesMeta: { + criteria: string; + sub_variable: string; + result: string; + expiry_date?: string | null; + }[]; +}) { + const overallPass = decentHomes.decent_homes === "pass"; + const lastUpdated = new Date(decentHomes.creation_date).toLocaleDateString( + "en-GB", + { + day: "numeric", + month: "long", + year: "numeric", + } + ); + + const criteriaGroups: Record< + string, + { sub_variable: string; result: string }[] + > = { + A: [], + B: [], + C: [], + D: [], + }; + + const replacements: { sub_variable: string; expiry_date: string | null }[] = + []; + + decentHomesMeta.forEach((item) => { + if (criteriaGroups[item.criteria]) { + criteriaGroups[item.criteria].push({ + sub_variable: item.sub_variable, + result: item.result, + }); + } + if (item.expiry_date) { + replacements.push({ + sub_variable: item.sub_variable, + expiry_date: item.expiry_date, + }); + } + }); + + const soonCount = replacements.filter((r) => { + if (!r.expiry_date) return false; + const expiry = new Date(r.expiry_date); + const diffMs = expiry.getTime() - new Date().getTime(); + return diffMs < 1000 * 60 * 60 * 24 * 365; + }).length; + + return ( +
+ + + + Decent Homes Assessment + + + + + {overallPass ? "Pass" : "Fail"} + +

Last updated: {lastUpdated}

+
+
+ + + + Criterion A + Criterion B + Criterion C + Criterion D + + + Replacements + {soonCount > 0 && ( + + {soonCount} + + )} + + + + + + + + + + + + + + + + + + + +
+ ); +} + +export default async function DecentHomesPage(props: { + params: Promise<{ slug: string; propertyId: string }>; +}) { + const params = await props.params; + const propertyMeta = await getPropertyMeta(params.propertyId); + + const decentHomesSummary = await getDocument({ + uprn: String(propertyMeta.uprn), + documentType: "DECENT_HOMES_SUMMARY", + }); + + const decentPropertyMeta = await getDocument({ + uprn: String(propertyMeta.uprn), + documentType: "DECENT_HOMES_PROPERTY_META", + }); + + if ( + Object.keys(decentHomesSummary).length === 0 || + Object.keys(decentPropertyMeta).length === 0 || + !decentHomesSummary.s3JsonUri || + !decentPropertyMeta.s3JsonUri + ) { + throw new Error("Decent Homes data is missing"); + } + + const decentHomesMeta = await getEnergyAssessmentFromS3( + decentPropertyMeta.s3JsonUri + ); + const decentHomes = await getEnergyAssessmentFromS3( + decentHomesSummary.s3JsonUri + ); + + return ( + + ); +} diff --git a/src/app/shadcn_components/ui/scroll-area.tsx b/src/app/shadcn_components/ui/scroll-area.tsx new file mode 100644 index 0000000..32cf968 --- /dev/null +++ b/src/app/shadcn_components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client"; + +import * as React from "react"; +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; + +import { cn } from "@/lib/utils"; + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/src/app/shadcn_components/ui/tabs.tsx b/src/app/shadcn_components/ui/tabs.tsx new file mode 100644 index 0000000..fb01e0a --- /dev/null +++ b/src/app/shadcn_components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client"; + +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent };