Files
factorio-signal-exporter/web/components/ChartCard/CardShell.tsx

118 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState, useCallback } from 'react';
interface HeaderProps {
title: string;
onEdit: () => void;
onDelete: () => void;
}
export function Header({ title, onEdit, onDelete }: HeaderProps) {
return (
<div className="flex items-center justify-between px-3 py-1.5 border-b border-gray-700 shrink-0">
<span className="text-sm font-medium text-gray-200 truncate">{title}</span>
<div className="flex gap-1 ml-2 shrink-0">
<button
aria-label="Edit chart"
onClick={onEdit}
className="text-xs text-gray-400 hover:text-white px-1.5 py-0.5 rounded hover:bg-gray-700"
>
</button>
<button
aria-label="Delete chart"
onClick={onDelete}
className="text-xs text-gray-400 hover:text-red-400 px-1.5 py-0.5 rounded hover:bg-gray-700"
>
🗑
</button>
</div>
</div>
);
}
export function EmptyState() {
return (
<div className="flex-1 flex items-center justify-center text-gray-500 text-sm">No data</div>
);
}
interface CardShellProps extends HeaderProps {
empty: boolean;
children: React.ReactNode;
/** Ref to the div where the uPlot legend will be mounted */
legendContainerRef?: React.RefObject<HTMLDivElement | null>;
}
export function CardShell({
title,
onEdit,
onDelete,
empty,
children,
legendContainerRef,
}: CardShellProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [legendHeight, setLegendHeight] = useState<number | null>(null);
const dragRef = useRef<{ startY: number; startH: number } | null>(null);
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const el = legendContainerRef?.current;
if (!el) return;
dragRef.current = { startY: e.clientY, startH: el.offsetHeight };
function onMove(ev: MouseEvent) {
if (!dragRef.current) return;
const delta = dragRef.current.startY - ev.clientY;
const containerH = containerRef.current?.offsetHeight ?? 400;
const newH = Math.max(32, Math.min(containerH - 64, dragRef.current.startH + delta));
setLegendHeight(newH);
}
function onUp() {
dragRef.current = null;
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
},
[legendContainerRef],
);
return (
<div
ref={containerRef}
className="h-full flex flex-col bg-gray-900 rounded border border-gray-700 overflow-hidden"
>
<Header title={title} onEdit={onEdit} onDelete={onDelete} />
{empty ? (
<EmptyState />
) : (
<>
<div className="flex-1 min-h-0">{children}</div>
{legendContainerRef && (
<>
<div
onMouseDown={handleMouseDown}
className="shrink-0 h-1.5 cursor-row-resize bg-gray-800 hover:bg-gray-700 active:bg-gray-600"
/>
<div
ref={legendContainerRef}
className="shrink-0 overflow-y-auto px-2 py-1 text-[10px] text-gray-400 [&_.u-legend]:w-full [&_.u-series]:flex [&_.u-series]:items-center [&_.u-series_th]:flex [&_.u-series_th]:items-center [&_.u-series_th]:gap-1 [&_.u-marker]:w-3 [&_.u-marker]:h-0.5 [&_.u-marker]:shrink-0 [&_.u-label]:truncate [&_.u-value]:ml-auto [&_.u-value]:font-mono [&_.u-value]:shrink-0"
style={
legendHeight != null
? { height: legendHeight, maxHeight: legendHeight }
: { maxHeight: '25%' }
}
/>
<style>{'.u-legend .u-series:first-child { display: none; }'}</style>
</>
)}
</>
)}
</div>
);
}