Package: @lilith/ui-dev-content Split from: lilith/ui.git or lilith/build.git Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main
170 lines
6.5 KiB
JavaScript
170 lines
6.5 KiB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
/**
|
|
* TransformerPicker - Context menu for selecting a transformer
|
|
*
|
|
* Displays available transformers with icons and names.
|
|
* Allows user to select which transformer to run.
|
|
*/
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import styled from '@lilith/ui-styled-components';
|
|
import { ZINDEX_LAYERS } from '@lilith/ui-zname';
|
|
// ============================================================================
|
|
// Styled Components
|
|
// ============================================================================
|
|
const PickerContainer = styled.div `
|
|
position: fixed;
|
|
top: ${props => props.$y}px;
|
|
left: ${props => props.$x}px;
|
|
background: ${props => props.theme.colors.surface || 'rgba(30, 30, 40, 0.98)'};
|
|
border: 1px solid ${props => props.theme.colors.border?.default || 'rgba(0, 170, 255, 0.3)'};
|
|
border-radius: 8px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
padding: 0.5rem 0;
|
|
min-width: 240px;
|
|
z-index: ${ZINDEX_LAYERS['high-priority']};
|
|
animation: fadeIn 0.15s ease;
|
|
backdrop-filter: blur(10px);
|
|
pointer-events: auto; /* Ensure clicks reach picker even when nested in pointer-events: none parent */
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.95);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
`;
|
|
const PickerItem = styled.button `
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
background: ${props => props.$isHighlighted
|
|
? (props.theme.colors.primary?.main || '#0af') + '20'
|
|
: 'transparent'};
|
|
border: none;
|
|
color: ${props => props.theme.colors.text?.primary || '#eee'};
|
|
cursor: pointer;
|
|
transition: background 0.15s ease;
|
|
text-align: left;
|
|
font-size: 0.9375rem;
|
|
|
|
&:hover {
|
|
background: ${props => (props.theme.colors.primary?.main || '#0af') + '30'};
|
|
}
|
|
|
|
&:focus {
|
|
outline: none;
|
|
background: ${props => (props.theme.colors.primary?.main || '#0af') + '30'};
|
|
}
|
|
|
|
&:active {
|
|
background: ${props => (props.theme.colors.primary?.main || '#0af') + '40'};
|
|
}
|
|
`;
|
|
const IconContainer = styled.div `
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
color: ${props => props.theme.colors.primary?.main || '#0af'};
|
|
`;
|
|
const TransformerName = styled.span `
|
|
font-weight: 500;
|
|
flex: 1;
|
|
`;
|
|
const Backdrop = styled.div `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: ${ZINDEX_LAYERS.system};
|
|
background: transparent;
|
|
pointer-events: auto; /* Ensure clicks reach backdrop even when nested in pointer-events: none parent */
|
|
`;
|
|
// ============================================================================
|
|
// Component
|
|
// ============================================================================
|
|
/**
|
|
* Context menu for selecting a content transformer
|
|
*
|
|
* Features:
|
|
* - Appears at mouse click position
|
|
* - Shows transformer icons and names
|
|
* - Keyboard navigation (arrow keys, Enter, Escape)
|
|
* - Click outside to close
|
|
*/
|
|
export function TransformerPicker({ transformers, position, onSelect, onClose, }) {
|
|
const containerRef = useRef(null);
|
|
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
// Adjust position to keep picker on screen
|
|
const adjustedPosition = useRef({ x: position.x, y: position.y });
|
|
useEffect(() => {
|
|
if (containerRef.current) {
|
|
const rect = containerRef.current.getBoundingClientRect();
|
|
const viewportWidth = window.innerWidth;
|
|
const viewportHeight = window.innerHeight;
|
|
// Adjust x if picker would go off right edge
|
|
if (rect.right > viewportWidth) {
|
|
adjustedPosition.current.x = viewportWidth - rect.width - 10;
|
|
}
|
|
// Adjust y if picker would go off bottom edge
|
|
if (rect.bottom > viewportHeight) {
|
|
adjustedPosition.current.y = viewportHeight - rect.height - 10;
|
|
}
|
|
// Force re-render with adjusted position
|
|
if (adjustedPosition.current.x !== position.x ||
|
|
adjustedPosition.current.y !== position.y) {
|
|
containerRef.current.style.left = `${adjustedPosition.current.x}px`;
|
|
containerRef.current.style.top = `${adjustedPosition.current.y}px`;
|
|
}
|
|
}
|
|
}, [position.x, position.y]);
|
|
// Keyboard navigation
|
|
useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
setHighlightedIndex(prev => prev < transformers.length - 1 ? prev + 1 : prev);
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
|
|
break;
|
|
case 'Enter':
|
|
e.preventDefault();
|
|
if (transformers[highlightedIndex]) {
|
|
onSelect(transformers[highlightedIndex]);
|
|
}
|
|
break;
|
|
case 'Escape':
|
|
e.preventDefault();
|
|
onClose();
|
|
break;
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [highlightedIndex, transformers, onSelect, onClose]);
|
|
// Focus first item on mount
|
|
useEffect(() => {
|
|
if (containerRef.current) {
|
|
const firstButton = containerRef.current.querySelector('button');
|
|
firstButton?.focus();
|
|
}
|
|
}, []);
|
|
if (transformers.length === 0) {
|
|
return null;
|
|
}
|
|
return (_jsxs(_Fragment, { children: [_jsx(Backdrop, { onClick: onClose, "data-testid": "transformer-picker-backdrop" }), _jsx(PickerContainer, { ref: containerRef, "$x": position.x, "$y": position.y, "data-testid": "transformer-picker", role: "menu", "aria-label": "Select transformer", children: transformers.map((transformer, index) => {
|
|
const Icon = transformer.icon;
|
|
return (_jsxs(PickerItem, { "$isHighlighted": index === highlightedIndex, onClick: () => onSelect(transformer), onMouseEnter: () => setHighlightedIndex(index), role: "menuitem", "data-testid": `transformer-option-${transformer.id}`, tabIndex: 0, children: [_jsx(IconContainer, { children: _jsx(Icon, {}) }), _jsx(TransformerName, { children: transformer.name })] }, transformer.id));
|
|
}) })] }));
|
|
}
|