207 lines
6.4 KiB
TypeScript
207 lines
6.4 KiB
TypeScript
import { useMemo } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import type { OverlayLayer } from './ChartPanel.types';
|
|
import { IconEye, IconLock, IconTrash } from './ChartIcons';
|
|
|
|
type Props = {
|
|
open: boolean;
|
|
layers: OverlayLayer[];
|
|
onRequestClose: () => void;
|
|
|
|
onToggleLayerVisible: (layerId: string) => void;
|
|
onToggleLayerLocked: (layerId: string) => void;
|
|
onSetLayerOpacity: (layerId: string, opacity: number) => void;
|
|
|
|
fibPresent: boolean;
|
|
fibSelected: boolean;
|
|
fibVisible: boolean;
|
|
fibLocked: boolean;
|
|
fibOpacity: number;
|
|
onSelectFib: () => void;
|
|
onToggleFibVisible: () => void;
|
|
onToggleFibLocked: () => void;
|
|
onSetFibOpacity: (opacity: number) => void;
|
|
onDeleteFib: () => void;
|
|
};
|
|
|
|
function clamp01(v: number): number {
|
|
if (!Number.isFinite(v)) return 1;
|
|
return Math.max(0, Math.min(1, v));
|
|
}
|
|
|
|
function opacityToPct(opacity: number): number {
|
|
return Math.round(clamp01(opacity) * 100);
|
|
}
|
|
|
|
function pctToOpacity(pct: number): number {
|
|
if (!Number.isFinite(pct)) return 1;
|
|
return clamp01(pct / 100);
|
|
}
|
|
|
|
function IconButton({
|
|
title,
|
|
active,
|
|
disabled,
|
|
onClick,
|
|
children,
|
|
}: {
|
|
title: string;
|
|
active?: boolean;
|
|
disabled?: boolean;
|
|
onClick: () => void;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
className={['layersBtn', active ? 'layersBtn--active' : null].filter(Boolean).join(' ')}
|
|
title={title}
|
|
aria-label={title}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onClick();
|
|
}}
|
|
disabled={disabled}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function OpacitySlider({
|
|
value,
|
|
disabled,
|
|
onChange,
|
|
}: {
|
|
value: number;
|
|
disabled?: boolean;
|
|
onChange: (next: number) => void;
|
|
}) {
|
|
const pct = opacityToPct(value);
|
|
return (
|
|
<div className="layersOpacity">
|
|
<input
|
|
className="layersOpacity__range"
|
|
type="range"
|
|
min={0}
|
|
max={100}
|
|
value={pct}
|
|
onChange={(e) => onChange(pctToOpacity(Number(e.target.value)))}
|
|
disabled={disabled}
|
|
/>
|
|
<div className="layersOpacity__pct">{pct}%</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ChartLayersPanel({
|
|
open,
|
|
layers,
|
|
onRequestClose,
|
|
onToggleLayerVisible,
|
|
onToggleLayerLocked,
|
|
onSetLayerOpacity,
|
|
fibPresent,
|
|
fibSelected,
|
|
fibVisible,
|
|
fibLocked,
|
|
fibOpacity,
|
|
onSelectFib,
|
|
onToggleFibVisible,
|
|
onToggleFibLocked,
|
|
onSetFibOpacity,
|
|
onDeleteFib,
|
|
}: Props) {
|
|
const drawingsLayer = useMemo(() => layers.find((l) => l.id === 'drawings'), [layers]);
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className={['chartLayersBackdrop', open ? 'chartLayersBackdrop--open' : null].filter(Boolean).join(' ')}
|
|
onClick={open ? onRequestClose : undefined}
|
|
/>
|
|
<div className={['chartLayersPanel', open ? 'chartLayersPanel--open' : null].filter(Boolean).join(' ')}>
|
|
<div className="chartLayersPanel__head">
|
|
<div className="chartLayersPanel__title">Layers</div>
|
|
<button type="button" className="chartLayersPanel__close" onClick={onRequestClose} aria-label="Close">
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="chartLayersTable">
|
|
<div className="chartLayersRow chartLayersRow--head">
|
|
<div className="chartLayersCell chartLayersCell--icon" title="Visible">
|
|
<IconEye />
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--icon" title="Lock">
|
|
<IconLock />
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--name">Name</div>
|
|
<div className="chartLayersCell chartLayersCell--opacity">Opacity</div>
|
|
<div className="chartLayersCell chartLayersCell--actions">Actions</div>
|
|
</div>
|
|
|
|
{layers.map((layer) => (
|
|
<div key={layer.id} className="chartLayersRow chartLayersRow--layer">
|
|
<div className="chartLayersCell chartLayersCell--icon">
|
|
<IconButton title="Toggle visible" active={layer.visible} onClick={() => onToggleLayerVisible(layer.id)}>
|
|
<IconEye />
|
|
</IconButton>
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--icon">
|
|
<IconButton title="Toggle lock" active={layer.locked} onClick={() => onToggleLayerLocked(layer.id)}>
|
|
<IconLock />
|
|
</IconButton>
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--name">
|
|
<div className="layersName layersName--layer">
|
|
{layer.name}
|
|
{layer.id === 'drawings' ? <span className="layersName__meta">{fibPresent ? ' (1)' : ' (0)'}</span> : null}
|
|
</div>
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--opacity">
|
|
<OpacitySlider value={layer.opacity} onChange={(next) => onSetLayerOpacity(layer.id, next)} />
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--actions" />
|
|
</div>
|
|
))}
|
|
|
|
{drawingsLayer && fibPresent ? (
|
|
<div
|
|
className={['chartLayersRow', 'chartLayersRow--object', fibSelected ? 'chartLayersRow--selected' : null]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
onClick={onSelectFib}
|
|
role="button"
|
|
tabIndex={0}
|
|
>
|
|
<div className="chartLayersCell chartLayersCell--icon">
|
|
<IconButton title="Toggle visible" active={fibVisible} onClick={onToggleFibVisible}>
|
|
<IconEye />
|
|
</IconButton>
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--icon">
|
|
<IconButton title="Toggle lock" active={fibLocked} onClick={onToggleFibLocked}>
|
|
<IconLock />
|
|
</IconButton>
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--name">
|
|
<div className="layersName layersName--object">Fib Retracement</div>
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--opacity">
|
|
<OpacitySlider value={fibOpacity} onChange={onSetFibOpacity} />
|
|
</div>
|
|
<div className="chartLayersCell chartLayersCell--actions">
|
|
<IconButton title="Delete fib" onClick={onDeleteFib}>
|
|
<IconTrash />
|
|
</IconButton>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|