mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-08-02 21:08:21 +00:00
feat(condition): aspect drill-down drawer + clear clickable affordance
Add the element→aspect drill-down agreed via the prototype. Every component (worklist card, horizon/healthy row, roadmap lane, assessment chip) is now a button that opens a right-side drawer showing that element's real aspect_condition rows — aspect, value, install year, renewal, comments — under a header + renewal hero + lifespan bar. Make clickability obvious: a chevron affordance on each row that nudges and darkens on hover, plus hover lift/border on the cards and a "Select a component to view its aspects" hint by the toggle. Drawer uses the app's existing vaul dependency (direction="right") so focus-trap / Esc / scroll-lock come for free — no useEffect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
14f47ef45e
commit
8a08a38ab6
1 changed files with 175 additions and 40 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Drawer as Vaul } from "vaul";
|
||||
import {
|
||||
HomeModernIcon,
|
||||
BuildingOffice2Icon,
|
||||
|
|
@ -13,8 +14,14 @@ import {
|
|||
ChevronRightIcon,
|
||||
Squares2X2Icon,
|
||||
CalendarDaysIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { elementLabel, renewalUrgency, type RenewalUrgency } from "@/lib/condition/labels";
|
||||
import {
|
||||
elementLabel,
|
||||
aspectLabel,
|
||||
renewalUrgency,
|
||||
type RenewalUrgency,
|
||||
} from "@/lib/condition/labels";
|
||||
import {
|
||||
CONDITION_CATEGORIES,
|
||||
categoryKeyOf,
|
||||
|
|
@ -46,7 +53,6 @@ function earliestInstallYear(aspects: AspectInput[]): number | null {
|
|||
const years = aspects.map((a) => yearOf(a.installDate)).filter((y): y is number => y !== null);
|
||||
return years.length ? Math.min(...years) : null;
|
||||
}
|
||||
// Headline value for a component: prefer material, else a rating/condition aspect.
|
||||
function primaryValue(aspects: AspectInput[]): string | null {
|
||||
const mat = aspects.find((a) => a.aspectType === "material" && a.value)?.value;
|
||||
if (mat) return mat;
|
||||
|
|
@ -56,6 +62,10 @@ function primaryValue(aspects: AspectInput[]): string | null {
|
|||
}
|
||||
return aspects.find((a) => a.value)?.value ?? null;
|
||||
}
|
||||
function categoryLabelOf(elementType: string): string {
|
||||
const key = categoryKeyOf(elementType);
|
||||
return CONDITION_CATEGORIES.find((c) => c.key === key)?.label ?? "Component";
|
||||
}
|
||||
|
||||
function ElementIcon({ elementType, className }: { elementType: string; className?: string }) {
|
||||
const t = elementType;
|
||||
|
|
@ -120,7 +130,7 @@ function SectionLabel({ children, count }: { children: React.ReactNode; count?:
|
|||
}
|
||||
|
||||
// ── Priority worklist ─────────────────────────────────────────────────────────
|
||||
function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
||||
function Worklist({ rows, currentYear, onSelect }: { rows: Row[]; currentYear: number; onSelect: (r: Row) => void }) {
|
||||
const comps = rows.filter((r) => !r.isAssessment);
|
||||
const assess = rows.filter((r) => r.isAssessment);
|
||||
const byYear = (a: Row, b: Row) => (a.el.renewalYear ?? Infinity) - (b.el.renewalYear ?? Infinity);
|
||||
|
|
@ -128,6 +138,7 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
const soon = comps.filter((r) => r.urgency === "soon").sort(byYear);
|
||||
const ok = comps.filter((r) => r.urgency === "ok").sort(byYear);
|
||||
const attn = [...overdue, ...soon];
|
||||
const key = (r: Row) => `${r.el.elementType}__${r.el.elementInstance}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -140,9 +151,10 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
const span = row.installed != null && row.el.renewalYear != null ? row.el.renewalYear - row.installed : 0;
|
||||
const pct = span > 0 ? Math.max(0, Math.min(100, ((currentYear - row.installed!) / span) * 100)) : 0;
|
||||
return (
|
||||
<div
|
||||
key={`${row.el.elementType}__${row.el.elementInstance}`}
|
||||
className={`grid grid-cols-[auto_1fr_auto] items-center gap-4 rounded-2xl border bg-white p-4 shadow-sm ${u.border}`}
|
||||
<button
|
||||
key={key(row)}
|
||||
onClick={() => onSelect(row)}
|
||||
className={`group grid w-full grid-cols-[auto_1fr_auto_auto] items-center gap-4 rounded-2xl border bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:shadow-md ${u.border}`}
|
||||
>
|
||||
<div className={`grid h-11 w-11 place-items-center rounded-xl ${u.medallion}`}>
|
||||
<ElementIcon elementType={row.el.elementType} className="h-5 w-5" />
|
||||
|
|
@ -167,7 +179,8 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
{row.urgency === "overdue" ? "Replace" : "Plan"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-gray-300 transition-all group-hover:translate-x-0.5 group-hover:text-brandblue" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -179,16 +192,20 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
<SectionLabel count={`next ${Math.min(6, ok.length)}`}>On the horizon</SectionLabel>
|
||||
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{ok.slice(0, 6).map((row) => (
|
||||
<div
|
||||
key={`${row.el.elementType}__${row.el.elementInstance}`}
|
||||
className="flex items-center justify-between gap-3 rounded-xl border border-gray-100 bg-white px-4 py-3"
|
||||
<button
|
||||
key={key(row)}
|
||||
onClick={() => onSelect(row)}
|
||||
className="group flex items-center justify-between gap-2 rounded-xl border border-gray-100 bg-white px-4 py-3 text-left transition-colors hover:border-brandblue/30 hover:bg-gray-50"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-brandblue">{elementLabel(row.el.elementType)}</p>
|
||||
{row.value && <p className="truncate text-[11px] text-gray-400">{row.value}</p>}
|
||||
{row.value && <p className="truncate text-[11px] text-gray-500">{row.value}</p>}
|
||||
</div>
|
||||
<span className="shrink-0 text-sm font-bold tabular-nums text-emerald-700">{row.el.renewalYear}</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<span className="text-sm font-bold tabular-nums text-emerald-700">{row.el.renewalYear}</span>
|
||||
<ChevronRightIcon className="h-4 w-4 text-gray-300 transition-all group-hover:translate-x-0.5 group-hover:text-brandblue" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -202,13 +219,17 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
</summary>
|
||||
<div className="grid grid-cols-1 border-t border-gray-100 sm:grid-cols-2">
|
||||
{ok.map((row) => (
|
||||
<div
|
||||
key={`${row.el.elementType}__${row.el.elementInstance}`}
|
||||
className="flex items-center justify-between border-t border-gray-50 px-5 py-2.5 text-[13px] first:border-t-0 sm:[&:nth-child(2)]:border-t-0"
|
||||
<button
|
||||
key={key(row)}
|
||||
onClick={() => onSelect(row)}
|
||||
className="group flex items-center justify-between border-t border-gray-50 px-5 py-2.5 text-left text-[13px] transition-colors first:border-t-0 hover:bg-gray-50 sm:[&:nth-child(2)]:border-t-0"
|
||||
>
|
||||
<span className="truncate text-gray-700">{elementLabel(row.el.elementType)}</span>
|
||||
<span className="shrink-0 font-semibold tabular-nums text-gray-500">{row.el.renewalYear}</span>
|
||||
</div>
|
||||
<span className="flex shrink-0 items-center gap-1.5">
|
||||
<span className="font-semibold tabular-nums text-gray-500">{row.el.renewalYear}</span>
|
||||
<ChevronRightIcon className="h-3.5 w-3.5 text-gray-200 transition-all group-hover:translate-x-0.5 group-hover:text-brandblue" />
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
|
|
@ -224,13 +245,15 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
{assess.map((row) => {
|
||||
const flag = /category\s*[12]/i.test(row.value ?? "");
|
||||
return (
|
||||
<span
|
||||
key={`${row.el.elementType}__${row.el.elementInstance}`}
|
||||
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs ${flag ? "border-amber-200 bg-amber-50" : "border-gray-100 bg-white"}`}
|
||||
<button
|
||||
key={key(row)}
|
||||
onClick={() => onSelect(row)}
|
||||
className={`group inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-colors ${flag ? "border-amber-200 bg-amber-50 hover:border-amber-300" : "border-gray-100 bg-white hover:border-brandblue/30"}`}
|
||||
>
|
||||
<span className="font-semibold text-gray-700">{elementLabel(row.el.elementType)}</span>
|
||||
<span className={flag ? "font-bold text-amber-700" : "text-gray-500"}>{row.value ?? "—"}</span>
|
||||
</span>
|
||||
<ChevronRightIcon className="h-3.5 w-3.5 text-gray-300 transition-all group-hover:translate-x-0.5 group-hover:text-brandblue" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -245,7 +268,7 @@ const LO = 2015;
|
|||
const HI = 2060;
|
||||
const xOf = (y: number) => ((Math.min(Math.max(y, LO), HI) - LO) / (HI - LO)) * 100;
|
||||
|
||||
function Roadmap({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
||||
function Roadmap({ rows, currentYear, onSelect }: { rows: Row[]; currentYear: number; onSelect: (r: Row) => void }) {
|
||||
const dated = rows.filter((r) => !r.isAssessment && r.el.renewalYear !== null);
|
||||
const assessCount = rows.length - dated.length;
|
||||
const nowX = xOf(currentYear);
|
||||
|
|
@ -253,12 +276,11 @@ function Roadmap({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
|
||||
const groups = CONDITION_CATEGORIES.map((c) => ({
|
||||
category: c,
|
||||
rows: dated.filter((r) => categoryKeyOf(r.el.elementType) === c.key).sort((a, b) => (a.el.renewalYear! - b.el.renewalYear!)),
|
||||
rows: dated.filter((r) => categoryKeyOf(r.el.elementType) === c.key).sort((a, b) => a.el.renewalYear! - b.el.renewalYear!),
|
||||
})).filter((g) => g.rows.length > 0);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-2xl border border-gray-100 bg-white pb-4 shadow-sm">
|
||||
{/* Axis */}
|
||||
<div className="sticky top-0 z-10 grid grid-cols-[130px_1fr] items-center border-b border-gray-100 bg-white/95 px-4 pb-1.5 pt-3 backdrop-blur sm:grid-cols-[160px_1fr]">
|
||||
<div />
|
||||
<div className="relative h-4">
|
||||
|
|
@ -284,31 +306,33 @@ function Roadmap({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
const x0 = row.installed != null ? xOf(row.installed) : Math.max(0, xOf(row.el.renewalYear!) - 3);
|
||||
const x1 = xOf(row.el.renewalYear!);
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
key={`${row.el.elementType}__${row.el.elementInstance}`}
|
||||
className="grid grid-cols-[130px_1fr] items-center px-4 py-0.5 transition-colors hover:bg-gray-50 sm:grid-cols-[160px_1fr]"
|
||||
onClick={() => onSelect(row)}
|
||||
className="group grid w-full grid-cols-[130px_1fr] items-center px-4 py-0.5 text-left transition-colors hover:bg-gray-50 sm:grid-cols-[160px_1fr]"
|
||||
>
|
||||
<div className="truncate pr-2 text-xs font-semibold text-brandblue" title={`${elementLabel(row.el.elementType)}${row.value ? ` — ${row.value}` : ""}`}>
|
||||
{elementLabel(row.el.elementType)}
|
||||
<div className="flex items-center gap-1 truncate pr-2 text-xs font-semibold text-brandblue" title={`${elementLabel(row.el.elementType)}${row.value ? ` — ${row.value}` : ""}`}>
|
||||
<ChevronRightIcon className="h-3 w-3 shrink-0 text-gray-300 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<span className="truncate">{elementLabel(row.el.elementType)}</span>
|
||||
</div>
|
||||
<div className="relative h-5">
|
||||
<div className="absolute -top-1 bottom-[-4px] w-px bg-gray-200" style={{ left: `${nowX}%` }} />
|
||||
<div className={`absolute top-1/2 h-1.5 -translate-y-1/2 rounded-full ${u.bar} opacity-70`} style={{ left: `${x0}%`, width: `${Math.max(1, x1 - x0)}%` }} />
|
||||
<div className={`absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white ${u.due}`} style={{ left: `${x1}%` }} title={`Renew ${row.el.renewalYear}`} />
|
||||
<div className={`absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white ${u.due}`} style={{ left: `${x1}%` }} />
|
||||
{x1 < 86 && (
|
||||
<span className="absolute top-1/2 -translate-y-1/2 pl-2 text-[10px] font-bold tabular-nums text-gray-500" style={{ left: `${x1}%` }}>
|
||||
{row.el.renewalYear}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{assessCount > 0 && (
|
||||
<p className="mt-3 border-t border-gray-100 px-4 pt-3 text-xs text-gray-400">
|
||||
<p className="mt-3 border-t border-gray-100 px-4 pt-3 text-xs text-gray-500">
|
||||
{assessCount} health & safety assessment{assessCount === 1 ? "" : "s"} not shown — they carry a rating, not a renewal date.
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -316,9 +340,106 @@ function Roadmap({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
// ── Shell: summary + view toggle ──────────────────────────────────────────────
|
||||
// ── Aspect drill-down drawer ──────────────────────────────────────────────────
|
||||
function AspectDrawer({ row, currentYear, onClose }: { row: Row; currentYear: number; onClose: () => void }) {
|
||||
const el = row.el;
|
||||
const u = U[row.urgency];
|
||||
const span = row.installed != null && el.renewalYear != null ? el.renewalYear - row.installed : 0;
|
||||
const pct = span > 0 ? Math.max(0, Math.min(100, ((currentYear - row.installed!) / span) * 100)) : 0;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="relative border-b border-gray-100 px-6 pb-5 pt-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
className="absolute right-4 top-4 grid h-8 w-8 place-items-center rounded-lg border border-gray-200 text-gray-500 transition-colors hover:bg-gray-50 hover:text-gray-800"
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-gray-500">{categoryLabelOf(el.elementType)}</p>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<div className={`grid h-10 w-10 shrink-0 place-items-center rounded-xl ${u.medallion}`}>
|
||||
<ElementIcon elementType={el.elementType} className="h-5 w-5" />
|
||||
</div>
|
||||
<Vaul.Title className="font-manrope text-xl font-bold text-brandblue">
|
||||
{elementLabel(el.elementType)}
|
||||
{el.elementInstance > 1 && <span className="ml-1 font-medium text-gray-300">#{el.elementInstance}</span>}
|
||||
</Vaul.Title>
|
||||
</div>
|
||||
<Vaul.Description className="mt-2 text-xs text-gray-500">
|
||||
{el.source} · surveyed {el.date}
|
||||
</Vaul.Description>
|
||||
</div>
|
||||
|
||||
{!row.isAssessment && (
|
||||
<div className="border-b border-gray-100 px-6 py-4">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<p className="text-xs text-gray-500">{statusLine(row, currentYear)}</p>
|
||||
<span className={`font-manrope text-3xl font-black tabular-nums leading-none ${u.year}`}>{el.renewalYear ?? "—"}</span>
|
||||
</div>
|
||||
{span > 0 && (
|
||||
<div className="mt-3">
|
||||
<div className="relative h-1.5 overflow-hidden rounded-full bg-gray-100">
|
||||
<div className={`absolute inset-y-0 left-0 rounded-full ${u.bar}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div className="mt-1.5 flex justify-between text-[11px] font-medium text-gray-500">
|
||||
<span>Installed {row.installed}</span>
|
||||
<span className="tabular-nums">{span}-yr life</span>
|
||||
<span>Renew {el.renewalYear}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<p className="px-6 pb-2 pt-4 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
Aspects · {el.aspects.length}
|
||||
</p>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-[10px] font-semibold uppercase tracking-wide text-gray-500">
|
||||
<th className="px-6 py-2 font-semibold">Aspect</th>
|
||||
<th className="px-3 py-2 font-semibold">Value</th>
|
||||
<th className="px-3 py-2 font-semibold">Inst.</th>
|
||||
<th className="px-6 py-2 font-semibold">Renew</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{el.aspects.map((a, i) => {
|
||||
const au = urgencyOf(a.renewalYear, currentYear);
|
||||
return (
|
||||
<tr key={i} className="border-t border-gray-50 align-top">
|
||||
<td className="whitespace-nowrap px-6 py-3 font-medium text-gray-500">{aspectLabel(a.aspectType)}</td>
|
||||
<td className="px-3 py-3 font-semibold text-brandblue">
|
||||
{a.value ?? <span className="font-normal text-gray-300">—</span>}
|
||||
{a.comments && <div className="mt-0.5 text-xs font-normal text-gray-500">{a.comments}</div>}
|
||||
</td>
|
||||
<td className="px-3 py-3 tabular-nums text-gray-500">
|
||||
{yearOf(a.installDate) ?? <span className="text-gray-300">—</span>}
|
||||
</td>
|
||||
<td className="px-6 py-3 tabular-nums">
|
||||
{a.renewalYear === null ? (
|
||||
<span className="text-gray-300">—</span>
|
||||
) : (
|
||||
<span className={`font-semibold ${U[au].year}`}>{a.renewalYear}</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Shell: summary + view toggle + drawer ─────────────────────────────────────
|
||||
export function ConditionView({ elements, currentYear }: { elements: ResolvedElement[]; currentYear: number }) {
|
||||
const [view, setView] = useState<"worklist" | "roadmap">("worklist");
|
||||
const [selected, setSelected] = useState<Row | null>(null);
|
||||
const rows = elements.map((el) => toRow(el, currentYear));
|
||||
|
||||
const comps = rows.filter((r) => !r.isAssessment);
|
||||
|
|
@ -335,12 +456,12 @@ export function ConditionView({ elements, currentYear }: { elements: ResolvedEle
|
|||
? { n: counts.soon, label: `component${counts.soon === 1 ? "" : "s"} due soon`, tone: U.soon.tone }
|
||||
: { n: counts.ok, label: "in good condition", tone: U.ok.tone };
|
||||
|
||||
const tab = (key: "worklist" | "roadmap", label: string, Icon: React.ElementType) => (
|
||||
const tab = (k: "worklist" | "roadmap", label: string, Icon: React.ElementType) => (
|
||||
<button
|
||||
onClick={() => setView(key)}
|
||||
aria-pressed={view === key}
|
||||
onClick={() => setView(k)}
|
||||
aria-pressed={view === k}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-semibold transition-colors ${
|
||||
view === key ? "bg-white text-brandblue shadow-sm" : "text-gray-500 hover:text-gray-800"
|
||||
view === k ? "bg-white text-brandblue shadow-sm" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
|
|
@ -374,15 +495,29 @@ export function ConditionView({ elements, currentYear }: { elements: ResolvedEle
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* View toggle */}
|
||||
<div className="flex justify-end">
|
||||
{/* Hint + view toggle */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-xs text-gray-500">Select a component to view its aspects</p>
|
||||
<div className="inline-flex rounded-xl border border-gray-200 bg-gray-50 p-1">
|
||||
{tab("worklist", "Priority", Squares2X2Icon)}
|
||||
{tab("roadmap", "Timeline", CalendarDaysIcon)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === "worklist" ? <Worklist rows={rows} currentYear={currentYear} /> : <Roadmap rows={rows} currentYear={currentYear} />}
|
||||
{view === "worklist" ? (
|
||||
<Worklist rows={rows} currentYear={currentYear} onSelect={setSelected} />
|
||||
) : (
|
||||
<Roadmap rows={rows} currentYear={currentYear} onSelect={setSelected} />
|
||||
)}
|
||||
|
||||
<Vaul.Root open={selected !== null} onOpenChange={(open) => !open && setSelected(null)} direction="right">
|
||||
<Vaul.Portal>
|
||||
<Vaul.Overlay className="fixed inset-0 z-50 bg-black/40" />
|
||||
<Vaul.Content className="fixed inset-y-0 right-0 z-50 flex w-[460px] max-w-[100vw] flex-col bg-white outline-none">
|
||||
{selected && <AspectDrawer row={selected} currentYear={currentYear} onClose={() => setSelected(null)} />}
|
||||
</Vaul.Content>
|
||||
</Vaul.Portal>
|
||||
</Vaul.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue