ui-dev-content/dist/components/TransformerPicker.js
autocommit 8b284e01b9 chore: initial package split from monorepo
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
2026-04-20 01:11:45 -07:00

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));
}) })] }));
}