132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
import { useEffect, useRef, useState, type ReactNode, type CSSProperties } from 'react';
|
||
import './devtool.css';
|
||
|
||
interface CrosshairProps {
|
||
children: ReactNode;
|
||
/** Optionales Label, das vor den Dimensionen angezeigt wird, z. B. "Header" */
|
||
label?: string;
|
||
/** Beliebige CSS-Farbe für Rahmen/Kreuz, überschreibt --crosshair-color */
|
||
color?: string;
|
||
/** Schraffur-Hintergrund aktivieren (.hatch Utility) */
|
||
hatch?: boolean;
|
||
/** Größe der Schraffur in px, überschreibt --hatch-size */
|
||
hatchSize?: number;
|
||
/** Live-Breite/Höhe oben links einblenden (per ResizeObserver) */
|
||
showDimensions?: boolean;
|
||
className?: string;
|
||
}
|
||
|
||
export function Crosshair({
|
||
children,
|
||
label,
|
||
color,
|
||
hatch = false,
|
||
hatchSize,
|
||
showDimensions = true,
|
||
className = '',
|
||
}: CrosshairProps) {
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
const [size, setSize] = useState({ w: 0, h: 0 });
|
||
|
||
useEffect(() => {
|
||
const el = ref.current;
|
||
if (!el || !showDimensions) return;
|
||
|
||
const ro = new ResizeObserver(([entry]) => {
|
||
const { width, height } = entry.contentRect;
|
||
setSize({ w: Math.round(width), h: Math.round(height) });
|
||
});
|
||
ro.observe(el);
|
||
return () => ro.disconnect();
|
||
}, [showDimensions]);
|
||
|
||
const style: CSSProperties = {
|
||
...(color ? { ['--crosshair-color' as string]: color } : {}),
|
||
...(hatchSize ? { ['--hatch-size' as string]: `${hatchSize}px` } : {}),
|
||
};
|
||
|
||
return (
|
||
<div
|
||
ref={ref}
|
||
className={`crosshair ${hatch ? 'hatch' : ''} ${className}`.trim()}
|
||
style={style}
|
||
>
|
||
{showDimensions && (
|
||
<span className="pointer-events-none absolute left-1 top-1 z-50 rounded bg-black/70 px-1.5 py-0.5 font-mono text-[10px] leading-none text-white">
|
||
{label ? `${label} · ` : ''}
|
||
{size.w}×{size.h}
|
||
</span>
|
||
)}
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
/** Zeigt den berechneten z-index eines Elements als Badge oben rechts an. */
|
||
export function ZIndexBadge({
|
||
children,
|
||
className = '',
|
||
}: {
|
||
children: ReactNode;
|
||
className?: string;
|
||
}) {
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
const [z, setZ] = useState('auto');
|
||
|
||
useEffect(() => {
|
||
const el = ref.current;
|
||
if (!el) return;
|
||
setZ(getComputedStyle(el).zIndex);
|
||
}, []);
|
||
|
||
return (
|
||
<div ref={ref} className={`relative ${className}`.trim()}>
|
||
<span className="pointer-events-none absolute right-1 top-1 z-[9999] rounded bg-black/70 px-1.5 py-0.5 font-mono text-[10px] leading-none text-white">
|
||
z: {z}
|
||
</span>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Definiert die SVG-Filter für die colorblind-* Utilities aus dev-utilities.css.
|
||
* Einmal im App-Root rendern (z. B. direkt unter <body>/<App>) — erzeugt selbst
|
||
* kein sichtbares Element, nur die <filter>-Definitionen im DOM.
|
||
*/
|
||
export function ColorblindFilters() {
|
||
return (
|
||
<svg aria-hidden="true" className="absolute h-0 w-0 overflow-hidden">
|
||
<defs>
|
||
<filter id="protanopia">
|
||
<feColorMatrix
|
||
type="matrix"
|
||
values="0.567 0.433 0 0 0
|
||
0.558 0.442 0 0 0
|
||
0 0.242 0.758 0 0
|
||
0 0 0 1 0"
|
||
/>
|
||
</filter>
|
||
<filter id="deuteranopia">
|
||
<feColorMatrix
|
||
type="matrix"
|
||
values="0.625 0.375 0 0 0
|
||
0.7 0.3 0 0 0
|
||
0 0.3 0.7 0 0
|
||
0 0 0 1 0"
|
||
/>
|
||
</filter>
|
||
<filter id="tritanopia">
|
||
<feColorMatrix
|
||
type="matrix"
|
||
values="0.95 0.05 0 0 0
|
||
0 0.433 0.567 0 0
|
||
0 0.475 0.525 0 0
|
||
0 0 0 1 0"
|
||
/>
|
||
</filter>
|
||
</defs>
|
||
</svg>
|
||
);
|
||
} |