Feature/v2/smartfolders rebuild (#6480)

Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
This commit is contained in:
Reece Browne
2026-06-10 11:18:14 +01:00
committed by GitHub
co-authored by aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
parent e0fc5061de
commit e2536daeb8
49 changed files with 9166 additions and 103 deletions
@@ -0,0 +1,278 @@
import React, { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { Text, ActionIcon, ScrollArea } from "@mantine/core";
import CloseIcon from "@mui/icons-material/Close";
import {
CardModalPhase,
CARD_MODAL_TIMINGS,
} from "@app/hooks/useCardModalAnimation";
interface CardExpansionModalProps {
phase: CardModalPhase;
cardRect: DOMRect | null;
textExpanded: boolean;
onClose: () => void;
icon: React.ReactNode;
count: number;
labelSingular: string;
labelPlural: string;
children: React.ReactNode;
footer?: React.ReactNode;
toolbar?: React.ReactNode;
widthRem?: number;
heightRem?: number;
/** Replace ScrollArea with a flex-fill container so children can expand to full body height */
fillHeight?: boolean;
}
const MODAL_W_REM = 56;
const MODAL_H_REM = 38;
const HEADER_H_REM = 3.5;
const MODAL_TOP_FRACTION = 0.12;
const EASING = "cubic-bezier(0.22,1,0.36,1)";
export function CardExpansionModal({
phase,
cardRect,
textExpanded,
onClose,
icon,
count,
labelSingular,
labelPlural,
children,
footer,
toolbar,
widthRem,
heightRem,
fillHeight,
}: CardExpansionModalProps) {
const { t } = useTranslation();
const [viewportW, setViewportW] = useState(window.innerWidth);
const [viewportH, setViewportH] = useState(window.innerHeight);
useEffect(() => {
const handler = () => {
setViewportW(window.innerWidth);
setViewportH(window.innerHeight);
};
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
if (phase === "closed" || !cardRect) return null;
const rootFontSize = parseFloat(
getComputedStyle(document.documentElement).fontSize,
);
const modalW = Math.min(
(widthRem ?? MODAL_W_REM) * rootFontSize,
viewportW * 0.9,
);
const modalH = Math.min(
(heightRem ?? MODAL_H_REM) * rootFontSize,
viewportH * 0.85,
);
const headerH = HEADER_H_REM * rootFontSize;
const finalLeft = (viewportW - modalW) / 2;
const finalTop = Math.min(
viewportH * MODAL_TOP_FRACTION,
viewportH - modalH - 16,
);
const isAtCard = phase === "entering" || phase === "closing-header";
const isAtHeader = phase === "header-open";
const cardH = isAtCard ? cardRect.height : isAtHeader ? headerH : modalH;
const getTransition = () => {
const s = CARD_MODAL_TIMINGS;
if (phase === "header-open")
return `top ${s.headerStretch}ms ${EASING}, left ${s.headerStretch}ms ${EASING}, width ${s.headerStretch}ms ${EASING}, height ${s.headerStretch}ms ${EASING}`;
if (phase === "open") return `height ${s.bodyDrop}ms ${EASING}`;
if (phase === "closing-body") return `height ${s.closeBody}ms ease-in`;
if (phase === "closing-header")
return `top ${s.closeStretch}ms ${EASING}, left ${s.closeStretch}ms ${EASING}, width ${s.closeStretch}ms ${EASING}, height ${s.closeStretch}ms ${EASING}, opacity ${s.closeStretch}ms ease`;
return "none";
};
const backdropOpacity =
phase === "entering" || phase === "closing-header" ? 0 : 1;
const cardOpacity = phase === "closing-header" ? 0 : 1;
const showBody = phase === "open" || phase === "closing-body";
return createPortal(
<div style={{ position: "fixed", inset: 0, zIndex: 300 }}>
<div
onClick={onClose}
style={{
position: "absolute",
inset: 0,
backgroundColor: "rgba(0,0,0,0.55)",
opacity: backdropOpacity,
transition: "opacity 220ms ease",
willChange: "opacity",
}}
/>
<div
style={{
position: "fixed",
top: isAtCard ? cardRect.top : finalTop,
left: isAtCard ? cardRect.left : finalLeft,
width: isAtCard ? cardRect.width : modalW,
height: cardH,
opacity: cardOpacity,
transition: getTransition(),
willChange: "top, left, width, height, opacity",
borderRadius: "var(--mantine-radius-md)",
overflow: "hidden",
backgroundColor: "var(--bg-toolbar)",
display: "flex",
flexDirection: "column",
boxShadow: "0 1.5rem 3rem rgba(0,0,0,0.3)",
}}
>
{/* Header */}
<div
style={{
position: "relative",
height: headerH,
flexShrink: 0,
borderBottom: "0.0625rem solid var(--border-subtle)",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div
style={{
position: "absolute",
left: "1rem",
display: "flex",
alignItems: "center",
}}
>
{icon}
</div>
<Text
component="span"
fw={800}
style={{ fontSize: "1.375rem", lineHeight: 1, margin: 0 }}
>
{count}
</Text>
<div
style={{
maxWidth: textExpanded ? "16rem" : "0",
opacity: textExpanded ? 1 : 0,
overflow: "hidden",
whiteSpace: "nowrap",
display: "flex",
alignItems: "center",
transition: `max-width 100ms ${EASING}, opacity 80ms ease`,
}}
>
<Text
component="span"
c="dimmed"
style={{
fontSize: "1rem",
paddingLeft: "0.5rem",
lineHeight: 1,
margin: 0,
}}
>
{count === 1 ? labelSingular : labelPlural}
</Text>
</div>
<ActionIcon
variant="subtle"
size="lg"
color="gray"
onClick={onClose}
aria-label={t("close", "Close")}
style={{
position: "absolute",
top: "0.25rem",
right: "0.375rem",
}}
>
<CloseIcon style={{ fontSize: "1.25rem" }} />
</ActionIcon>
</div>
</div>
{showBody && (
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
minHeight: 0,
backgroundColor: "var(--bg-toolbar)",
}}
>
{toolbar && (
<div
style={{
flexShrink: 0,
borderBottom: "0.0625rem solid var(--border-subtle)",
}}
>
{toolbar}
</div>
)}
{fillHeight ? (
<div
style={{
flex: 1,
minHeight: 0,
padding: "0.75rem 1rem",
display: "flex",
flexDirection: "column",
}}
>
{children}
</div>
) : (
<ScrollArea style={{ flex: 1, minHeight: 0 }}>
<div
style={{
padding: "0.75rem 1rem",
backgroundColor: "var(--bg-toolbar)",
}}
>
{children}
</div>
</ScrollArea>
)}
{footer && (
<div
style={{
padding: "0.75rem 1rem",
borderTop: "0.0625rem solid var(--border-subtle)",
flexShrink: 0,
}}
>
{footer}
</div>
)}
</div>
)}
</div>
</div>,
document.body,
);
}