UNPKG

polen

Version:

A framework for delightful GraphQL developer portals

275 lines 13.9 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { React as ReactHooks } from '#dep/react/index'; import { Box } from '@radix-ui/themes'; import { GraphQLErrorBoundary } from './components/GraphQLErrorBoundary.js'; import { GraphQLTokenPopover } from './components/GraphQLTokenPopover.js'; import { usePopoverState } from './hooks/use-popover-state.js'; import { parseGraphQLWithTreeSitter } from './lib/parser.js'; /** * Main component that renders an interactive GraphQL code block * * This component: * 1. Parses the GraphQL code into tokens using tree-sitter * 2. Renders each token with appropriate styling * 3. Adds interactivity to certain token types (types and fields) * 4. Shows loading/error states during parsing */ /** * Internal GraphQL Interactive implementation * Wrapped by error boundary in the main export */ const GraphQLInteractiveImpl = ({ codeblock, schema, showWarningIfNoSchema = true, }) => { // State to hold the parsed tokens const [tokens, setTokens] = ReactHooks.useState(null); // Loading state while parser initializes and processes the code const [isLoading, setIsLoading] = ReactHooks.useState(true); // Error state if parsing fails const [error, setError] = ReactHooks.useState(null); // Retry attempt counter const [retryCount, setRetryCount] = ReactHooks.useState(0); // Popover state management - must be called at top level for hooks rules const popoverState = usePopoverState({ showDelay: 300, hideDelay: 100, allowMultiplePins: true, }); // Memoize token parsing to avoid re-computation on unrelated renders const parseTokens = ReactHooks.useCallback(async () => { try { setIsLoading(true); setError(null); // Parse the code into tokens with semantic analysis const parsedTokens = await parseGraphQLWithTreeSitter(codeblock.code, codeblock.annotations, schema); setTokens(parsedTokens); setRetryCount(0); // Reset retry count on success } catch (err) { // Provide detailed error information to users const errorMessage = err instanceof Error ? err.message : 'Unknown parsing error'; setError(errorMessage); setTokens([]); // Set empty tokens on error for fallback rendering } finally { setIsLoading(false); } }, [codeblock.code, codeblock.annotations, schema]); // Retry function for users const handleRetry = ReactHooks.useCallback(() => { setRetryCount(prev => prev + 1); parseTokens(); }, [parseTokens]); // Parse the GraphQL code whenever dependencies change ReactHooks.useEffect(() => { parseTokens(); }, [parseTokens]); // Render loading state // Shows the code with reduced opacity and a loading indicator if (isLoading) { return (_jsxs("div", { className: 'graphql-loading', children: [_jsx("pre", { style: { opacity: 0.5 }, children: _jsx("code", { children: codeblock.code }) }), _jsx("div", { style: { position: 'absolute', top: '8px', right: '8px', fontSize: '12px', color: '#666', backgroundColor: '#f0f0f0', padding: '2px 6px', borderRadius: '3px', }, children: "Loading tree-sitter..." })] })); } // Render error state with retry option if (error) { return (_jsxs(Box, { className: 'graphql-error', p: '4', style: { borderRadius: 'var(--radius-2)', backgroundColor: 'var(--gray-2)', position: 'relative', borderLeft: '3px solid var(--red-9)', }, children: [_jsx("pre", { style: { margin: 0, whiteSpace: 'pre' }, children: _jsx("code", { children: codeblock.code }) }), _jsxs("div", { style: { color: 'var(--red-11)', fontSize: '12px', marginTop: '8px', padding: '8px', backgroundColor: 'var(--red-a3)', borderRadius: '3px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', }, children: [_jsxs("span", { children: ["Interactive parsing failed: ", error] }), retryCount < 3 && (_jsxs("button", { onClick: handleRetry, style: { backgroundColor: 'var(--red-9)', color: 'white', border: 'none', padding: '4px 8px', borderRadius: '3px', fontSize: '11px', cursor: 'pointer', }, children: ["Retry (", retryCount + 1, "/3)"] }))] })] })); } // Fallback if no tokens were parsed or parsing failed if (!tokens || tokens.length === 0) { return (_jsxs(Box, { className: 'graphql-fallback', p: '4', style: { borderRadius: 'var(--radius-2)', backgroundColor: 'var(--gray-2)', position: 'relative', }, children: [_jsx("pre", { style: { margin: 0, whiteSpace: 'pre' }, children: _jsx("code", { children: codeblock.code }) }), error && (_jsx("div", { style: { position: 'absolute', top: '8px', right: '8px', fontSize: '12px', color: 'var(--red-11)', backgroundColor: 'var(--red-a3)', padding: '2px 6px', borderRadius: '3px', maxWidth: '200px', }, title: error, children: "Interactive features unavailable" }))] })); } // Main render: Show the parsed and interactive code return (_jsxs(Box, { className: 'graphql-interactive', p: '4', position: 'relative', style: { borderRadius: 'var(--radius-2)', backgroundColor: 'var(--gray-2)', overflowX: 'auto', maxWidth: '100%', }, children: [_jsx("pre", { style: { margin: 0, whiteSpace: 'pre' }, children: _jsx("code", { children: tokens.map((token, index) => { const tokenId = `${token.start}-${token.end}-${index}`; return (_jsx(TokenComponent, { token: token, tokenId: tokenId, popoverState: popoverState, schema: schema }, tokenId)); }) }) }), !schema && showWarningIfNoSchema && (_jsxs("div", { style: { position: 'absolute', top: '8px', right: '8px', fontSize: '12px', color: 'var(--amber-11)', backgroundColor: 'var(--amber-a3)', padding: '2px 6px', borderRadius: '3px', display: 'flex', alignItems: 'center', gap: '4px', }, title: 'Interactive features are not available because no GraphQL schema is configured', children: [_jsxs("svg", { width: '12', height: '12', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', children: [_jsx("path", { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }), _jsx("line", { x1: '12', y1: '9', x2: '12', y2: '13' }), _jsx("line", { x1: '12', y1: '17', x2: '12.01', y2: '17' })] }), "No schema configured"] }))] })); }; /** * Component that renders a single token with interactive features * * This component handles: * - Applying syntax highlighting based on token type * - Hover effects for interactive tokens * - Click handlers for navigation * - Visual feedback for CodeHike annotations */ const TokenComponent = ({ token, tokenId, popoverState, schema }) => { // Track hover state for interactive tokens const [isHovered, setIsHovered] = ReactHooks.useState(false); // Handle clicks on interactive tokens - memoized to prevent unnecessary re-renders const handleClick = ReactHooks.useCallback((e) => { if (token.polen.isInteractive()) { e.preventDefault(); e.stopPropagation(); // Don't allow pinning for invalid fields if (token.semantic && 'kind' in token.semantic && token.semantic.kind === 'InvalidField') { return; } // Toggle popover pin state only - no navigation popoverState.onTogglePin(tokenId); } }, [token, tokenId, popoverState]); // Show hover effects when mouse enters an interactive token - memoized const handleMouseEnter = ReactHooks.useCallback(() => { if (token.polen.isInteractive()) { setIsHovered(true); popoverState.onHoverStart(tokenId); } }, [token, tokenId, popoverState]); // Hide hover effects when mouse leaves - memoized const handleMouseLeave = ReactHooks.useCallback(() => { setIsHovered(false); popoverState.onHoverEnd(tokenId); }, [tokenId, popoverState]); // Get the appropriate CSS class from the token const baseClass = token.highlighter.getCssClass(); // Map class names to inline styles const getBaseStyle = () => { switch (baseClass) { case 'graphql-keyword': return { color: 'var(--red-11)', fontWeight: 'bold' }; case 'graphql-type-interactive': return { color: 'var(--blue-11)', fontWeight: 500 }; case 'graphql-field-interactive': return { color: 'var(--violet-11)' }; case 'graphql-field-error': return { color: 'var(--red-11)', }; case 'graphql-error-hint': return { color: 'var(--red-11)', fontSize: '0.9em', fontStyle: 'italic', opacity: 0.5, }; case 'graphql-comment': return { color: 'var(--gray-11)', fontStyle: 'italic', opacity: 0.6, }; case 'graphql-operation': return { color: 'var(--violet-11)', fontStyle: 'italic' }; case 'graphql-fragment': return { color: 'var(--violet-11)', fontStyle: 'italic' }; case 'graphql-variable': return { color: 'var(--orange-11)' }; case 'graphql-argument': return { color: 'var(--gray-12)' }; case 'graphql-string': return { color: 'var(--blue-11)' }; case 'graphql-number': return { color: 'var(--blue-11)' }; case 'graphql-punctuation': return { color: 'var(--gray-11)', opacity: 0.5 }; default: return { color: 'var(--gray-12)' }; } }; // Check if this is an invalid field const isInvalidField = token.semantic && 'kind' in token.semantic && token.semantic.kind === 'InvalidField'; // Build the style object for this token const style = { ...getBaseStyle(), // Interactive tokens get special styling (except invalid fields) ...(token.polen.isInteractive() && !isInvalidField && { cursor: 'pointer', textDecoration: isHovered ? 'underline' : 'none', backgroundColor: isHovered ? 'var(--accent-a3)' : 'transparent', }), // Invalid fields get different hover styling - no cursor change, no underline ...(isInvalidField && { cursor: 'default', textDecoration: 'underline wavy var(--red-a5)', textUnderlineOffset: '2px', // Subtle background change on hover to show it's interactive for popover backgroundColor: isHovered ? 'var(--red-a2)' : 'transparent', }), // Tokens with CodeHike annotations get highlighted ...(token.codeHike.annotations.length > 0 && { position: 'relative', backgroundColor: 'var(--yellow-a3)', }), }; // Build the span element const tokenSpan = (_jsx("span", { className: baseClass, style: style, "data-token-class": baseClass, "data-interactive": token.polen.isInteractive(), children: token.text })); // Wrap in popover if token has semantic information return (_jsx(GraphQLTokenPopover, { token: token, open: popoverState.isOpen(tokenId), pinned: popoverState.isPinned(tokenId), onTriggerHover: handleMouseEnter, onTriggerLeave: handleMouseLeave, onTriggerClick: handleClick, onContentHover: () => popoverState.onPopoverHover(tokenId), onContentLeave: () => popoverState.onPopoverLeave(tokenId), onClose: () => popoverState.unpin(tokenId), children: tokenSpan })); }; /** * Main GraphQL Interactive component with error boundary protection * * This is the component that should be used in user code. It wraps the * internal implementation with an error boundary that provides graceful * fallback to static code rendering if interactive features fail. */ export const GraphQLInteractive = (props) => { return (_jsx(GraphQLErrorBoundary, { fallbackCode: props.codeblock.code, onError: (error, errorInfo) => { // Log error for debugging (only in development) if (process.env['NODE_ENV'] === 'development') { console.error('GraphQL Interactive Error Boundary:', error, errorInfo); } }, children: _jsx(GraphQLInteractiveImpl, { ...props }) })); }; //# sourceMappingURL=GraphQLInteractive.js.map