Before Tailwind upgrade

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-31 21:48:43 +00:00
parent 6a98a530ed
commit 031b5dacc1
15 changed files with 628 additions and 329 deletions

2
package-lock.json generated
View file

@ -31,7 +31,7 @@
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-query": "^4.29.12",
"@tanstack/react-table": "^8.9.3",
"@tremor/react": "^3.16.0",
"@tremor/react": "^3.18.7",
"@types/node": "20.2.3",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.1",

View file

@ -37,7 +37,7 @@
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-query": "^4.29.12",
"@tanstack/react-table": "^8.9.3",
"@tremor/react": "^3.16.0",
"@tremor/react": "^3.18.7",
"@types/node": "20.2.3",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.1",

View file

@ -23,13 +23,13 @@ const EpcBarChart = ({
index="name"
categories={["G", "F", "E", "D", "C", "B", "A"]} // Each treated as a separate series
colors={[
"#e41e3b", // Color for 'G'
"#ef8026", // Color for 'F'
"#f3a96a", // Color for 'E'
"#f7cd14", // Color for 'D'
"#8dbd40", // Color for 'C'
"#2da55c", // Color for 'B'
"#117d58", // Color for 'A'
"epc_g", // Color for 'G'
"epc_f", // Color for 'F'
"epc_e", // Color for 'E'
"epc_d", // Color for 'D'
"epc_c", // Color for 'C'
"epc_b", // Color for 'B'
"epc_a", // Color for 'A'
]}
valueFormatter={dataFormatter}
yAxisWidth={48}

View file

@ -0,0 +1,13 @@
import { PlanTypeEnum } from "@/app/db/schema/recommendations";
// Fixed Domna costs per delivery type
export const DOMNA_COST_MAP: Record<PlanTypeEnum, number> & {
default: number;
} = {
solar_eco4: 2250,
solar_hhrsh_eco4: 2250,
empty_cavity_eco: 800,
partial_cavity_eco: 800,
extraction_eco: 800,
default: 800,
};

View file

@ -1,3 +1,5 @@
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@tailwind base;
@tailwind components;
@tailwind utilities;
@ -76,7 +78,9 @@
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
}
@ -116,3 +120,129 @@
.animate-spin {
animation: spin 1s linear infinite;
}
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--animate-hide: hide 150ms cubic-bezier(0.16, 1, 0.3, 1);
--animate-slide-down-and-fade: slideDownAndFade 150ms
cubic-bezier(0.16, 1, 0.3, 1);
--animate-slide-left-and-fade: slideLeftAndFade 150ms
cubic-bezier(0.16, 1, 0.3, 1);
--animate-slide-up-and-fade: slideUpAndFade 150ms
cubic-bezier(0.16, 1, 0.3, 1);
--animate-slide-right-and-fade: slideRightAndFade 150ms
cubic-bezier(0.16, 1, 0.3, 1);
--animate-accordion-open: accordionOpen 150ms cubic-bezier(0.87, 0, 0.13, 1);
--animate-accordion-close: accordionClose 150ms cubic-bezier(0.87, 0, 0.13, 1);
--animate-dialog-overlay-show: dialogOverlayShow 150ms
cubic-bezier(0.16, 1, 0.3, 1);
--animate-dialog-content-show: dialogContentShow 150ms
cubic-bezier(0.16, 1, 0.3, 1);
--animate-drawer-slide-left-and-fade: drawerSlideLeftAndFade 150ms
cubic-bezier(0.16, 1, 0.3, 1);
--animate-drawer-slide-right-and-fade: drawerSlideRightAndFade 150ms ease-in;
@keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideDownAndFade {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideLeftAndFade {
from {
opacity: 0;
transform: translateX(6px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideUpAndFade {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideRightAndFade {
from {
opacity: 0;
transform: translateX(-6px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes accordionOpen {
from {
height: 0px;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordionClose {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0px;
}
}
@keyframes dialogOverlayShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes dialogContentShow {
from {
opacity: 0;
transform: translate(-50%, -45%) scale(0.95);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes drawerSlideLeftAndFade {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes drawerSlideRightAndFade {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}
}

View file

@ -8,7 +8,6 @@ import { cache } from "react";
import { Inter } from "next/font/google";
import { Toaster } from "@/app/shadcn_components/ui/toaster";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { X } from "lucide-react";
// If loading a variable font, you don't need to specify the font weight
const inter = Inter({

View file

@ -3,23 +3,7 @@ import { property } from "@/app/db/schema/property";
import { inArray, eq, and } from "drizzle-orm";
import { surveyDB } from "@/app/db/surveyDB/connection";
import { uploadedFiles } from "@/app/db/surveyDB/schema/surveyDB";
import {
getEnergyAssessmentFromS3,
getConditionReport,
getPropertyMeta,
} from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils";
import {
getAllRoomData,
getRoomsWithDamp,
getRoomsWithDefects,
getRoomsWithBadWindows,
areAllWindowsOk,
getElevationsWithIssues,
hasSufficientSpace,
meetsSapThreshold,
hasEfficientHeatingSystem,
isInsulationAdequate,
} from "@/app/portfolio/[slug]/building-passport/[propertyId]/assessment/decent_homes_utils";
import { getEnergyAssessmentFromS3 } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils";
import DecentHomesDashboard from "./DecentHomesDashboard";
async function getPropertiesWithUprn(

View file

@ -7,14 +7,21 @@ import {
CardTitle,
CardContent,
} from "@/app/shadcn_components/ui/card";
import { BarChart } from "@tremor/react";
import { BarChart, DonutChart } from "@tremor/react";
import { formatNumber } from "@/app/utils";
import { Leaf, PoundSterling, Zap, FileSpreadsheet } from "lucide-react";
const mappedTitles: Record<string, string> = {
solar_eco4: "Solar ECO4 project metrics",
solar_hhrsh_eco4: "Solar HHRSH ECO4 project metrics",
empty_cavity_eco: "Empty Cavity Insulation metrics",
partial_cavity_eco: "Partial Cavity Insulation metrics",
extraction_eco: "Extraction & Refill project metrics",
default: "Select a work type to view metrics",
};
export function ProjectProposal({ plans }: { plans: any[] }) {
const [selectedType, setSelectedType] = useState<string | null>(null);
// Group by planType
const grouped = useMemo(() => {
const map: Record<string, any[]> = {};
for (const plan of plans) {
@ -23,89 +30,135 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
map[plan.planType].push(plan);
}
// Summaries for the chart
return Object.entries(map).map(([type, list]) => ({
planType: type,
count: list.length,
avgClientContribution:
list.reduce((sum, p) => sum + (p.totalFunding ?? 0) * 0.1, 0) /
list.length, // placeholder calc
totalFunding: list.reduce((sum, p) => sum + (p.totalFunding ?? 0), 0),
totalCarbon: list.reduce(
return Object.entries(map).map(([type, list]) => {
const totalFunding = list.reduce(
(sum, p) => sum + (p.totalFunding ?? 0),
0
);
const totalClientContribution = list.reduce(
(sum, p) => sum + (p.clientContribution ?? 0),
0
);
const totalCarbon = list.reduce(
(sum, p) => sum + (p.totalCarbonSavings ?? 0),
0
),
totalBills: list.reduce((sum, p) => sum + (p.totalBillSavings ?? 0), 0),
}));
);
const totalBills = list.reduce(
(sum, p) => sum + (p.totalBillSavings ?? 0),
0
);
return {
planType: type,
count: list.length,
avgClientContribution: totalClientContribution / list.length,
totalClientContribution,
totalFunding,
totalCarbon,
totalBills,
};
});
}, [plans]);
const selectedData = selectedType
? grouped.find((d) => d.planType === selectedType)
: null;
useMemo(() => {
if (grouped.length === 1 && !selectedType)
setSelectedType(grouped[0].planType);
}, [grouped, selectedType]);
const selectedData =
selectedType && grouped.length
? grouped.find((d) => d.planType === selectedType)
: grouped.length === 1
? grouped[0]
: null;
const domnaPalette = [
"#14163d", // brandblue (deep navy)
"#2d348f", // midblue
"#3943b7", // brandmidblue
"#c4a47c", // brandbrown
"#d3b488", // brandtan
"#eff6fc", // brandlightblue (for subtle items)
];
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 my-8">
{/* Left: Chart */}
<Card className="lg:col-span-2">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Chart */}
<Card className="lg:col-span-2 border border-gray-200 bg-white">
<CardHeader>
<CardTitle>Plans by Work Type</CardTitle>
<CardTitle className="text-base font-medium text-brandblue">
Homes by Work Type
</CardTitle>
</CardHeader>
<CardContent>
<BarChart
data={grouped}
index="planType"
categories={["count"]}
colors={["blue"]}
valueFormatter={(v) => v.toString()}
onValueChange={(v) => setSelectedType(String(v) || null)}
className="h-64"
/>
{grouped.length > 1 ? (
<BarChart
data={grouped}
index="planType"
categories={["count"]}
colors={domnaPalette}
valueFormatter={(v) => v.toString()}
onValueChange={(v) =>
setSelectedType(
v && typeof v === "object" && "planType" in v
? String((v as any).planType)
: null
)
}
className="h-64"
/>
) : (
<DonutChart
data={grouped}
category="count"
index="planType"
colors={["midblue"]}
valueFormatter={(v) => `${v} home${v === 1 ? "" : "s"}`}
/>
)}
</CardContent>
</Card>
{/* Right: Details */}
<Card>
{/* Metrics */}
<Card className="border border-gray-200 bg-white">
<CardHeader>
<CardTitle>
{selectedType
? selectedType.replaceAll("_", " ")
: "Select a work type"}
<CardTitle className="text-lg font-semibold text-brandblue">
{mappedTitles[selectedType || "default"]}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{selectedType && selectedData ? (
<>
<div className="text-sm text-gray-600">
Average client contribution
</div>
<div className="text-xl font-semibold">
£{formatNumber(selectedData.avgClientContribution || 0)}
</div>
<CardContent className="space-y-6">
<div>
<p className="text-sm text-gray-500 mb-1">
Total client contribution
</p>
<p className="text-2xl font-semibold text-brandblue">
£{formatNumber(selectedData?.totalClientContribution || 0)}
</p>
<p className="text-xs text-gray-500">
Avg per home £
{formatNumber(selectedData?.avgClientContribution || 0)}
</p>
</div>
<div className="text-sm text-gray-600">Carbon savings</div>
<div className="text-xl font-semibold">
{(selectedData.totalCarbon * 1000).toFixed(2)} kgCOe
</div>
<div className="text-sm text-gray-600">Bill savings</div>
<div className="text-xl font-semibold">
£{formatNumber(selectedData.totalBills)}
</div>
<div className="text-sm text-gray-600">
Total estimated contribution
</div>
<div className="text-xl font-semibold">
£
{formatNumber(
selectedData.totalFunding +
(selectedType.includes("cavity") ? 1500 : 500) // example extra cost rule
)}
</div>
</>
) : (
<p className="text-gray-500 italic">Click a bar to view details</p>
)}
<div className="grid grid-cols-3 gap-4 text-center border-t pt-4">
<div>
<p className="text-xs text-gray-500 mb-1">Funding</p>
<p className="text-sm font-medium text-brandblue">
£{formatNumber(selectedData?.totalFunding || 0)}
</p>
</div>
<div>
<p className="text-xs text-gray-500 mb-1">Carbon</p>
<p className="text-sm font-medium text-brandblue">
{((selectedData?.totalCarbon || 0) * 1000).toFixed(2)} kgCOe
</p>
</div>
<div>
<p className="text-xs text-gray-500 mb-1">Bills</p>
<p className="text-sm font-medium text-brandblue">
£{formatNumber(selectedData?.totalBills || 0)}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
@ -114,11 +167,11 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
export function DashboardSummary({ plans }: { plans: any[] }) {
const totalFunding = plans.reduce((sum, p) => sum + (p.totalFunding || 0), 0);
const totalCarbonSavings = plans.reduce(
const totalCarbon = plans.reduce(
(sum, p) => sum + (p.totalCarbonSavings || 0),
0
);
const totalBillSavings = plans.reduce(
const totalBills = plans.reduce(
(sum, p) => sum + (p.totalBillSavings || 0),
0
);
@ -128,39 +181,42 @@ export function DashboardSummary({ plans }: { plans: any[] }) {
{
title: "Total Funding",
value: `£${formatNumber(totalFunding)}`,
icon: <PoundSterling className="h-6 w-6 text-brandblue" />,
subtitle: "Domna will help you unlock this much funding.",
},
{
title: "Total Carbon Savings",
value: `${(totalCarbonSavings * 1000).toFixed(2)} kgCO₂e`,
icon: <Leaf className="h-6 w-6 text-green-600" />,
title: "Carbon Savings",
value: `${(totalCarbon * 1000).toFixed(2)} kgCO₂e`,
subtitle: "Your projects total estimated CO₂e savings.",
},
{
title: "Total Bill Savings",
value: `£${formatNumber(totalBillSavings)}`,
icon: <Zap className="h-6 w-6 text-yellow-500" />,
title: "Bill Savings",
value: `£${formatNumber(totalBills)}`,
subtitle: "Expected total bill reductions across all homes.",
},
{
title: "Number of Plans",
title: "Number of Homes",
value: planCount,
icon: <FileSpreadsheet className="h-6 w-6 text-brandbrown" />,
subtitle: "Properties included across your project plans.",
},
];
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 my-6">
{cards.map((card) => (
<Card key={card.title} className="shadow-sm border border-gray-200">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{cards.map((c) => (
<Card
key={c.title}
className="border border-gray-200 bg-white hover:bg-brandlightblue/10 transition-colors"
>
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-600">
{card.title}
{c.title}
</CardTitle>
{card.icon}
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold text-gray-900">
{card.value}
<div className="text-3xl font-semibold text-brandblue mb-1">
{c.value}
</div>
<p className="text-xs text-gray-500">{c.subtitle}</p>
</CardContent>
</Card>
))}

View file

@ -1,13 +1,30 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown, Leaf, PoundSterling, Zap } from "lucide-react";
import { ArrowUpDown } from "lucide-react";
import { Button } from "@/app/shadcn_components/ui/button";
import { formatNumber } from "@/app/utils";
import StatusBadge from "@/app/components/StatusBadge";
import { PlanWithTotals } from "./utils";
export const planColumns: ColumnDef<PlanWithTotals>[] = [
{
accessorKey: "landlordPropertyId",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Property Reference
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div className="text-gray-700 text-center">
{row.original.landlordPropertyId || "—"}
</div>
),
},
{
accessorKey: "address",
header: ({ column }) => (
@ -20,7 +37,9 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
</Button>
),
cell: ({ row }) => (
<div className="text-gray-700">{row.original.address || "—"}</div>
<div className="text-gray-700 text-center text-sm">
{row.original.address || "—"}
</div>
),
},
{
@ -35,7 +54,9 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
</Button>
),
cell: ({ row }) => (
<div className="text-gray-700">{row.original.postcode || "—"}</div>
<div className="text-gray-700 text-center">
{row.original.postcode || "—"}
</div>
),
},
@ -50,7 +71,7 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
isProperty={false}
/>
) : (
<span className="text-gray-500">None</span>
<span className="text-gray-500 text-center">None</span>
)}
</div>
),
@ -67,7 +88,7 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
</Button>
),
cell: ({ row }) => (
<div className="font-medium text-gray-800">
<div className="font-medium text-gray-800 text-center">
{String(row.original.planType).replaceAll("_", " ")}
</div>
),
@ -77,7 +98,6 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
header: () => <div className="text-center">Total Funding</div>,
cell: ({ row }) => (
<div className="flex items-center justify-center gap-1">
<PoundSterling className="h-4 w-4 text-brandblue" />
<span className="font-medium">
£{formatNumber(row.original.totalFunding || 0)}
</span>
@ -89,8 +109,7 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
header: () => <div className="text-center">Carbon Savings</div>,
cell: ({ row }) => (
<div className="flex items-center justify-center gap-1">
<Leaf className="h-4 w-4 text-green-600" />
<span className="font-medium">
<span className="font-medium text-center">
{((row.original.totalCarbonSavings || 0) * 1000).toFixed(2)} kgCOe
</span>
</div>
@ -101,11 +120,22 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
header: () => <div className="text-center">Bill Savings</div>,
cell: ({ row }) => (
<div className="flex items-center justify-center gap-1">
<Zap className="h-4 w-4 text-yellow-500" />
<span className="font-medium">
<span className="font-medium text-center">
£{formatNumber(row.original.totalBillSavings || 0)}
</span>
</div>
),
},
{
accessorKey: "clientContribution",
header: () => <div className="text-center">Investment</div>,
cell: ({ row }) => (
<div className="flex items-center justify-center gap-1">
<span className="font-semibold text-center">
£{formatNumber(row.original.clientContribution || 0)}
</span>
</div>
),
sortingFn: "alphanumeric",
},
];

View file

@ -11,18 +11,26 @@ export default async function YourProjectsPage({
const { slug: portfolioId } = await params;
const latestPlans = await getPlansWithTotals(portfolioId);
console.log("latestPlans", latestPlans);
return (
<div className="container mx-auto px-4 py-6 space-y-8">
<div className="max-w-7xl mx-auto px-6 py-10 space-y-10">
<header>
<h1 className="text-3xl font-semibold text-brandblue mb-2">
Your Retrofit Projects
</h1>
<p className="text-gray-600 text-sm">
Review project performance and funding insights across your portfolio.
</p>
</header>
<DashboardSummary plans={latestPlans} />
<ProjectProposal plans={latestPlans} />
<div>
<h2 className="text-xl font-semibold text-gray-800 mb-2">
Plans Overview
<section>
<h2 className="text-xl font-semibold text-brandblue mb-3">
Your Homes
</h2>
<DataTable data={latestPlans} columns={planColumns} />
</div>
</section>
</div>
);
}

View file

@ -1,5 +1,7 @@
import { db } from "@/app/db/db";
import { sql } from "drizzle-orm";
import { DOMNA_COST_MAP } from "@/app/domna/financials";
import { PlanTypeEnum } from "@/app/db/schema/recommendations";
export interface PlanWithTotals extends Record<string, unknown> {
planId: string;
@ -14,6 +16,9 @@ export interface PlanWithTotals extends Record<string, unknown> {
totalFunding: number | null;
totalCarbonSavings: number | null;
totalBillSavings: number | null;
totalRecommendationCost?: number | null;
surveyCost?: number;
clientContribution?: number;
}
export async function getPlansWithTotals(
@ -30,7 +35,7 @@ export async function getPlansWithTotals(
p.address AS "address",
p.postcode AS "postcode",
fp.scheme AS "fundingScheme",
COALESCE(SUM(r.estimated_cost), 0) AS "totalFunding",
COALESCE(fp.project_funding, 0) AS "totalFunding",
COALESCE(SUM(r.co2_equivalent_savings), 0) AS "totalCarbonSavings",
COALESCE(SUM(r.energy_cost_savings), 0) AS "totalBillSavings",
COALESCE(SUM(r.estimated_cost), 0) AS "totalRecommendationCost"
@ -61,9 +66,30 @@ export async function getPlansWithTotals(
p.landlord_property_id,
p.address,
p.postcode,
fp.scheme
fp.scheme,
fp.project_funding
ORDER BY pl.created_at DESC;
`);
return result.rows;
const data = result.rows.map((plan) => {
const planType = plan.planType as PlanTypeEnum | null;
const surveyCost = planType
? (DOMNA_COST_MAP[planType] ?? DOMNA_COST_MAP.default)
: DOMNA_COST_MAP.default;
const totalCost = plan.totalRecommendationCost ?? 0;
const funding = plan.totalFunding ?? 0;
const rawContribution = totalCost + surveyCost - funding;
const clientContribution = rawContribution > 0 ? rawContribution : 0;
return {
...plan,
surveyCost,
clientContribution,
};
});
return data;
}

View file

@ -10,7 +10,6 @@ export default async function RemoteAssessmentPage(props: {
const params = await props.params;
const portfolioId = params.slug;
// 🔹 Replace this with your real Drizzle query
const scenarios = await getPortfolioScenarios(portfolioId);
return (

204
src/lib/chartUtils.ts Normal file
View file

@ -0,0 +1,204 @@
// Tremor Raw chartColors [v0.1.0]
export type ColorUtility = "bg" | "stroke" | "fill" | "text";
export const chartColors = {
blue: {
bg: "bg-blue-500",
stroke: "stroke-blue-500",
fill: "fill-blue-500",
text: "text-blue-500",
},
emerald: {
bg: "bg-emerald-500",
stroke: "stroke-emerald-500",
fill: "fill-emerald-500",
text: "text-emerald-500",
},
violet: {
bg: "bg-violet-500",
stroke: "stroke-violet-500",
fill: "fill-violet-500",
text: "text-violet-500",
},
amber: {
bg: "bg-amber-500",
stroke: "stroke-amber-500",
fill: "fill-amber-500",
text: "text-amber-500",
},
gray: {
bg: "bg-gray-500",
stroke: "stroke-gray-500",
fill: "fill-gray-500",
text: "text-gray-500",
},
cyan: {
bg: "bg-cyan-500",
stroke: "stroke-cyan-500",
fill: "fill-cyan-500",
text: "text-cyan-500",
},
pink: {
bg: "bg-pink-500",
stroke: "stroke-pink-500",
fill: "fill-pink-500",
text: "text-pink-500",
},
lime: {
bg: "bg-lime-500",
stroke: "stroke-lime-500",
fill: "fill-lime-500",
text: "text-lime-500",
},
fuchsia: {
bg: "bg-fuchsia-500",
stroke: "stroke-fuchsia-500",
fill: "fill-fuchsia-500",
text: "text-fuchsia-500",
},
brandblue: {
bg: "bg-[#14163d]",
stroke: "stroke-[#14163d]",
fill: "fill-[#14163d]",
text: "text-[#14163d]",
},
midblue: {
bg: "bg-[#2d348f]",
stroke: "stroke-[#2d348f]",
fill: "fill-[#2d348f]",
text: "text-[#2d348f]",
},
brandmidblue: {
bg: "bg-[#3943b7]",
stroke: "stroke-[#3943b7]",
fill: "fill-[#3943b7]",
text: "text-[#3943b7]",
},
brandbrown: {
bg: "bg-[#c4a47c]",
stroke: "stroke-[#c4a47c]",
fill: "fill-[#c4a47c]",
text: "text-[#c4a47c]",
},
brandtan: {
bg: "bg-[#d3b488]",
stroke: "stroke-[#d3b488]",
fill: "fill-[#d3b488]",
text: "text-[#d3b488]",
},
brandlightblue: {
bg: "bg-[#eff6fc]",
stroke: "stroke-[#eff6fc]",
fill: "fill-[#eff6fc]",
text: "text-[#eff6fc]",
},
epc_a: {
bg: "bg-[#117d58]",
stroke: "stroke-[#117d58]",
fill: "fill-[#117d58]",
text: "text-[#117d58]",
},
epc_b: {
bg: "bg-[#2da55c]",
stroke: "stroke-[#2da55c]",
fill: "fill-[#2da55c]",
text: "text-[#2da55c]",
},
epc_c: {
bg: "bg-[#8dbd40]",
stroke: "stroke-[#8dbd40]",
fill: "fill-[#8dbd40]",
text: "text-[#8dbd40]",
},
epc_d: {
bg: "bg-[#f7cd14]",
stroke: "stroke-[#f7cd14]",
fill: "fill-[#f7cd14]",
text: "text-[#f7cd14]",
},
epc_e: {
bg: "bg-[#f3a96a]",
stroke: "stroke-[#f3a96a]",
fill: "fill-[#f3a96a]",
text: "text-[#f3a96a]",
},
epc_f: {
bg: "bg-[#ef8026]",
stroke: "stroke-[#ef8026]",
fill: "fill-[#ef8026]",
text: "text-[#ef8026]",
},
epc_g: {
bg: "bg-[#e41e3b]",
stroke: "stroke-[#e41e3b]",
fill: "fill-[#e41e3b]",
text: "text-[#e41e3b]",
},
} as const satisfies {
[color: string]: {
[key in ColorUtility]: string;
};
};
export type AvailableChartColorsKeys = keyof typeof chartColors;
export const AvailableChartColors: AvailableChartColorsKeys[] = Object.keys(
chartColors
) as Array<AvailableChartColorsKeys>;
export const constructCategoryColors = (
categories: string[],
colors: AvailableChartColorsKeys[]
): Map<string, AvailableChartColorsKeys> => {
const categoryColors = new Map<string, AvailableChartColorsKeys>();
categories.forEach((category, index) => {
categoryColors.set(category, colors[index % colors.length]);
});
return categoryColors;
};
export const getColorClassName = (
color: AvailableChartColorsKeys,
type: ColorUtility
): string => {
const fallbackColor = {
bg: "bg-gray-500",
stroke: "stroke-gray-500",
fill: "fill-gray-500",
text: "text-gray-500",
};
return chartColors[color]?.[type] ?? fallbackColor[type];
};
// Tremor Raw getYAxisDomain [v0.0.0]
export const getYAxisDomain = (
autoMinValue: boolean,
minValue: number | undefined,
maxValue: number | undefined
) => {
const minDomain = autoMinValue ? "auto" : (minValue ?? 0);
const maxDomain = maxValue ?? "auto";
return [minDomain, maxDomain];
};
// Tremor Raw hasOnlyOneValueForKey [v0.1.0]
export function hasOnlyOneValueForKey(
array: any[],
keyToCheck: string
): boolean {
const val: any[] = [];
for (const obj of array) {
if (Object.prototype.hasOwnProperty.call(obj, keyToCheck)) {
val.push(obj[keyToCheck]);
if (val.length > 1) {
return false;
}
}
}
return true;
}

View file

@ -1,6 +1,42 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn (...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
// Tremor Raw cx [v0.0.0]
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function cx(...args: ClassValue[]) {
return twMerge(clsx(...args));
}
// Tremor focusInput [v0.0.2]
export const focusInput = [
// base
"focus:ring-2",
// ring color
"focus:ring-blue-200 dark:focus:ring-blue-700/30",
// border color
"focus:border-blue-500 dark:focus:border-blue-700",
];
// Tremor Raw focusRing [v0.0.1]
export const focusRing = [
// base
"outline outline-offset-2 outline-0 focus-visible:outline-2",
// outline color
"outline-blue-500 dark:outline-blue-500",
];
// Tremor Raw hasErrorInput [v0.0.1]
export const hasErrorInput = [
// base
"ring-2",
// border color
"border-red-500 dark:border-red-700",
// ring color
"ring-red-200 dark:ring-red-700/30",
];

View file

@ -28,64 +28,6 @@ module.exports = {
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
colors: {
tremor: {
brand: {
faint: "colors.blue[50]",
muted: "colors.blue[200]",
subtle: "colors.blue[400]",
DEFAULT: "colors.blue[500]",
emphasis: "colors.blue[700]",
inverted: "colors.white",
},
background: {
muted: "colors.gray[50]",
subtle: "colors.gray[100]",
DEFAULT: "colors.white",
emphasis: "colors.gray[700]",
},
border: {
DEFAULT: "colors.gray[200]",
},
ring: {
DEFAULT: "colors.gray[200]",
},
content: {
subtle: "colors.gray[400]",
DEFAULT: "colors.gray[500]",
emphasis: "colors.gray[700]",
strong: "colors.gray[900]",
inverted: "colors.white",
},
},
"dark-tremor": {
brand: {
faint: "#0B1229",
muted: "colors.blue[950]",
subtle: "colors.blue[800]",
DEFAULT: "colors.blue[500]",
emphasis: "colors.blue[400]",
inverted: "colors.blue[950]",
},
background: {
muted: "#131A2B",
subtle: "colors.gray[800]",
DEFAULT: "colors.gray[900]",
emphasis: "colors.gray[300]",
},
border: {
DEFAULT: "colors.gray[800]",
},
ring: {
DEFAULT: "colors.gray[800]",
},
content: {
subtle: "colors.gray[600]",
DEFAULT: "colors.gray[500]",
emphasis: "colors.gray[200]",
strong: "colors.gray[50]",
inverted: "colors.gray[950]",
},
},
epc_a: "#117d58",
epc_b: "#2da55c",
epc_c: "#8dbd40",
@ -146,11 +88,6 @@ module.exports = {
brandmidblue: "#3943b7",
brandlightblue: "#00a9f4",
},
borderRadius: {
"tremor-small": "0.375rem",
"tremor-default": "0.5rem",
"tremor-full": "9999px",
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
},
@ -197,44 +134,6 @@ module.exports = {
maxWidth: {
"8xl": "90rem",
},
boxShadow: {
"tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
"tremor-card":
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
"tremor-dropdown":
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
"dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
"dark-tremor-card":
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
"dark-tremor-dropdown":
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
},
fontSize: {
"tremor-label": [
"0.75rem",
{
lineHeight: "1rem",
},
],
"tremor-default": [
"0.875rem",
{
lineHeight: "1.25rem",
},
],
"tremor-title": [
"1.125rem",
{
lineHeight: "1.75rem",
},
],
"tremor-metric": [
"1.875rem",
{
lineHeight: "2.25rem",
},
],
},
},
},
variants: {
@ -270,91 +169,6 @@ module.exports = {
pattern:
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
},
// This enables the EPC colours for tremor. They're listed from EPC G -> A
"bg-[#e41e3b]",
"border-[#e41e3b]",
"hover:bg-[#e41e3b]",
"hover:border-[#e41e3b]",
"hover:text-[#e41e3b]",
"fill-[#e41e3b]",
"ring-[#e41e3b]",
"stroke-[#e41e3b]",
"text-[#e41e3b]",
"ui-selected:bg-[#e41e3b]",
"ui-selected:border-[#e41e3b]",
"ui-selected:text-[#e41e3b]",
"bg-[#ef8026]",
"border-[#ef8026]",
"hover:bg-[#ef8026]",
"hover:border-[#ef8026]",
"hover:text-[#ef8026]",
"fill-[#ef8026]",
"ring-[#ef8026]",
"stroke-[#ef8026]",
"text-[#ef8026]",
"ui-selected:bg-[#ef8026]",
"ui-selected:border-[#ef8026]",
"ui-selected:text-[#ef8026]",
"bg-[#f3a96a]",
"border-[#f3a96a]",
"hover:bg-[#f3a96a]",
"hover:border-[#f3a96a]",
"hover:text-[#f3a96a]",
"fill-[#f3a96a]",
"ring-[#f3a96a]",
"stroke-[#f3a96a]",
"text-[#f3a96a]",
"ui-selected:bg-[#f3a96a]",
"ui-selected:border-[#f3a96a]",
"ui-selected:text-[#f3a96a]",
"bg-[#f7cd14]",
"border-[#f7cd14]",
"hover:bg-[#f7cd14]",
"hover:border-[#f7cd14]",
"hover:text-[#f7cd14]",
"fill-[#f7cd14]",
"ring-[#f7cd14]",
"stroke-[#f7cd14]",
"text-[#f7cd14]",
"ui-selected:bg-[#f7cd14]",
"ui-selected:border-[#f7cd14]",
"ui-selected:text-[#f7cd14]",
"bg-[#8dbd40]",
"border-[#8dbd40]",
"hover:bg-[#8dbd40]",
"hover:border-[#8dbd40]",
"hover:text-[#8dbd40]",
"fill-[#8dbd40]",
"ring-[#8dbd40]",
"stroke-[#8dbd40]",
"text-[#8dbd40]",
"ui-selected:bg-[#8dbd40]",
"ui-selected:border-[#8dbd40]",
"ui-selected:text-[#8dbd40]",
"bg-[#2da55c]",
"border-[#2da55c]",
"hover:bg-[#2da55c]",
"hover:border-[#2da55c]",
"hover:text-[#2da55c]",
"fill-[#2da55c]",
"ring-[#2da55c]",
"stroke-[#2da55c]",
"text-[#2da55c]",
"ui-selected:bg-[#2da55c]",
"ui-selected:border-[#2da55c]",
"ui-selected:text-[#2da55c]",
"bg-[#117d58]",
"border-[#117d58]",
"hover:bg-[#117d58]",
"hover:border-[#117d58]",
"hover:text-[#117d58]",
"fill-[#117d58]",
"ring-[#117d58]",
"stroke-[#117d58]",
"text-[#117d58]",
"ui-selected:bg-[#117d58]",
"ui-selected:border-[#117d58]",
"ui-selected:text-[#117d58]",
],
plugins: [
function ({ addVariant }) {