polen 0.10.0-next.21 → 0.10.0-next.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/api/config/configurator.d.ts +62 -11
- package/build/api/config/configurator.d.ts.map +1 -1
- package/build/api/config/configurator.js +9 -0
- package/build/api/config/configurator.js.map +1 -1
- package/build/api/vite/plugins/core.d.ts.map +1 -1
- package/build/api/vite/plugins/core.js +1 -0
- package/build/api/vite/plugins/core.js.map +1 -1
- package/build/project-data.d.ts +1 -0
- package/build/project-data.d.ts.map +1 -1
- package/build/sandbox.js +40 -17
- package/build/sandbox.js.map +1 -1
- package/build/template/components/CodeBlock.d.ts.map +1 -1
- package/build/template/components/CodeBlock.js +3 -5
- package/build/template/components/CodeBlock.js.map +1 -1
- package/build/template/components/Field.js +1 -1
- package/build/template/components/Field.js.map +1 -1
- package/build/template/components/GraphQLInteractive/GraphQLInteractive.d.ts +31 -0
- package/build/template/components/GraphQLInteractive/GraphQLInteractive.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/GraphQLInteractive.js +275 -0
- package/build/template/components/GraphQLInteractive/GraphQLInteractive.js.map +1 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.d.ts +39 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.js +51 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.js.map +1 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.d.ts +33 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.js +242 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.js.map +1 -0
- package/build/template/components/GraphQLInteractive/hooks/use-popover-state.d.ts +45 -0
- package/build/template/components/GraphQLInteractive/hooks/use-popover-state.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/hooks/use-popover-state.js +176 -0
- package/build/template/components/GraphQLInteractive/hooks/use-popover-state.js.map +1 -0
- package/build/template/components/GraphQLInteractive/index.d.ts +2 -0
- package/build/template/components/GraphQLInteractive/index.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/index.js +2 -0
- package/build/template/components/GraphQLInteractive/index.js.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/graphql-node-types.d.ts +52 -0
- package/build/template/components/GraphQLInteractive/lib/graphql-node-types.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/graphql-node-types.js +34 -0
- package/build/template/components/GraphQLInteractive/lib/graphql-node-types.js.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/parser.d.ts +71 -0
- package/build/template/components/GraphQLInteractive/lib/parser.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/parser.js +836 -0
- package/build/template/components/GraphQLInteractive/lib/parser.js.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/semantic-nodes.d.ts +98 -0
- package/build/template/components/GraphQLInteractive/lib/semantic-nodes.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/semantic-nodes.js +31 -0
- package/build/template/components/GraphQLInteractive/lib/semantic-nodes.js.map +1 -0
- package/build/template/components/content/$$.d.ts +0 -1
- package/build/template/components/content/$$.d.ts.map +1 -1
- package/build/template/components/content/$$.js +0 -1
- package/build/template/components/content/$$.js.map +1 -1
- package/package.json +5 -21
- package/src/api/config/configurator.ts +72 -11
- package/src/api/vite/plugins/core.ts +1 -0
- package/src/lib/kit-temp.test.ts +9 -9
- package/src/project-data.ts +1 -0
- package/src/sandbox.ts +40 -17
- package/src/template/components/CodeBlock.tsx +6 -9
- package/src/template/components/Field.tsx +1 -1
- package/src/template/components/GraphQLInteractive/GraphQLInteractive.tsx +464 -0
- package/src/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.tsx +96 -0
- package/src/template/components/GraphQLInteractive/components/GraphQLTokenPopover.tsx +492 -0
- package/src/template/components/GraphQLInteractive/hooks/use-popover-state.ts +244 -0
- package/src/template/components/GraphQLInteractive/index.ts +1 -0
- package/src/template/components/GraphQLInteractive/lib/graphql-node-types.ts +217 -0
- package/src/template/components/GraphQLInteractive/lib/parser.ts +1075 -0
- package/src/template/components/GraphQLInteractive/lib/semantic-nodes.ts +154 -0
- package/src/template/components/GraphQLInteractive/tests/parser-comment.test.ts +33 -0
- package/src/template/components/GraphQLInteractive/tests/parser-error-hint.test.ts +102 -0
- package/src/template/components/GraphQLInteractive/tests/parser.test.ts +131 -0
- package/src/template/components/content/$$.ts +0 -1
- package/build/template/components/content/GraphQLDocumentWithSchema.d.ts +0 -8
- package/build/template/components/content/GraphQLDocumentWithSchema.d.ts.map +0 -1
- package/build/template/components/content/GraphQLDocumentWithSchema.js +0 -13
- package/build/template/components/content/GraphQLDocumentWithSchema.js.map +0 -1
- package/build/template/components/content/GraphQLDocumentWrapper.d.ts +0 -7
- package/build/template/components/content/GraphQLDocumentWrapper.d.ts.map +0 -1
- package/build/template/components/content/GraphQLDocumentWrapper.js +0 -48
- package/build/template/components/content/GraphQLDocumentWrapper.js.map +0 -1
- package/src/template/components/content/GraphQLDocumentWithSchema.tsx +0 -13
- package/src/template/components/content/GraphQLDocumentWrapper.tsx +0 -72
@@ -0,0 +1,464 @@
|
|
1
|
+
/**
|
2
|
+
* Interactive GraphQL code block with tree-sitter parsing
|
3
|
+
*
|
4
|
+
* This component replaces CodeHike's default rendering for GraphQL code blocks
|
5
|
+
* that have the "interactive" meta flag. It provides:
|
6
|
+
* - Syntax highlighting using tree-sitter
|
7
|
+
* - Hover tooltips showing type information (when schema is available)
|
8
|
+
* - Click navigation to reference documentation
|
9
|
+
* - Integration with CodeHike's annotation system
|
10
|
+
*/
|
11
|
+
|
12
|
+
import type { React } from '#dep/react/index'
|
13
|
+
import { React as ReactHooks } from '#dep/react/index'
|
14
|
+
import { Box } from '@radix-ui/themes'
|
15
|
+
import type { HighlightedCode } from 'codehike/code'
|
16
|
+
import type { GraphQLSchema } from 'graphql'
|
17
|
+
import { GraphQLErrorBoundary } from './components/GraphQLErrorBoundary.js'
|
18
|
+
import { GraphQLTokenPopover } from './components/GraphQLTokenPopover.js'
|
19
|
+
import { usePopoverState } from './hooks/use-popover-state.js'
|
20
|
+
import { type GraphQLToken, parseGraphQLWithTreeSitter } from './lib/parser.js'
|
21
|
+
|
22
|
+
interface GraphQLInteractiveProps {
|
23
|
+
/** The code block from CodeHike with code and annotations */
|
24
|
+
codeblock: HighlightedCode
|
25
|
+
|
26
|
+
/** The GraphQL schema for providing type information and validation */
|
27
|
+
schema?: GraphQLSchema
|
28
|
+
|
29
|
+
/** Whether to show a warning indicator when schema is missing */
|
30
|
+
showWarningIfNoSchema?: boolean
|
31
|
+
}
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Main component that renders an interactive GraphQL code block
|
35
|
+
*
|
36
|
+
* This component:
|
37
|
+
* 1. Parses the GraphQL code into tokens using tree-sitter
|
38
|
+
* 2. Renders each token with appropriate styling
|
39
|
+
* 3. Adds interactivity to certain token types (types and fields)
|
40
|
+
* 4. Shows loading/error states during parsing
|
41
|
+
*/
|
42
|
+
/**
|
43
|
+
* Internal GraphQL Interactive implementation
|
44
|
+
* Wrapped by error boundary in the main export
|
45
|
+
*/
|
46
|
+
const GraphQLInteractiveImpl: React.FC<GraphQLInteractiveProps> = ({
|
47
|
+
codeblock,
|
48
|
+
schema,
|
49
|
+
showWarningIfNoSchema = true,
|
50
|
+
}) => {
|
51
|
+
// State to hold the parsed tokens
|
52
|
+
const [tokens, setTokens] = ReactHooks.useState<GraphQLToken[] | null>(null)
|
53
|
+
|
54
|
+
// Loading state while parser initializes and processes the code
|
55
|
+
const [isLoading, setIsLoading] = ReactHooks.useState(true)
|
56
|
+
|
57
|
+
// Error state if parsing fails
|
58
|
+
const [error, setError] = ReactHooks.useState<string | null>(null)
|
59
|
+
|
60
|
+
// Retry attempt counter
|
61
|
+
const [retryCount, setRetryCount] = ReactHooks.useState(0)
|
62
|
+
|
63
|
+
// Popover state management - must be called at top level for hooks rules
|
64
|
+
const popoverState = usePopoverState({
|
65
|
+
showDelay: 300,
|
66
|
+
hideDelay: 100,
|
67
|
+
allowMultiplePins: true,
|
68
|
+
})
|
69
|
+
|
70
|
+
// Memoize token parsing to avoid re-computation on unrelated renders
|
71
|
+
const parseTokens = ReactHooks.useCallback(async () => {
|
72
|
+
try {
|
73
|
+
setIsLoading(true)
|
74
|
+
setError(null)
|
75
|
+
|
76
|
+
// Parse the code into tokens with semantic analysis
|
77
|
+
const parsedTokens = await parseGraphQLWithTreeSitter(
|
78
|
+
codeblock.code,
|
79
|
+
codeblock.annotations,
|
80
|
+
schema, // Pass the schema for semantic analysis
|
81
|
+
)
|
82
|
+
|
83
|
+
setTokens(parsedTokens)
|
84
|
+
setRetryCount(0) // Reset retry count on success
|
85
|
+
} catch (err) {
|
86
|
+
// Provide detailed error information to users
|
87
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown parsing error'
|
88
|
+
setError(errorMessage)
|
89
|
+
setTokens([]) // Set empty tokens on error for fallback rendering
|
90
|
+
} finally {
|
91
|
+
setIsLoading(false)
|
92
|
+
}
|
93
|
+
}, [codeblock.code, codeblock.annotations, schema])
|
94
|
+
|
95
|
+
// Retry function for users
|
96
|
+
const handleRetry = ReactHooks.useCallback(() => {
|
97
|
+
setRetryCount(prev => prev + 1)
|
98
|
+
parseTokens()
|
99
|
+
}, [parseTokens])
|
100
|
+
|
101
|
+
// Parse the GraphQL code whenever dependencies change
|
102
|
+
ReactHooks.useEffect(() => {
|
103
|
+
parseTokens()
|
104
|
+
}, [parseTokens])
|
105
|
+
|
106
|
+
// Render loading state
|
107
|
+
// Shows the code with reduced opacity and a loading indicator
|
108
|
+
if (isLoading) {
|
109
|
+
return (
|
110
|
+
<div className='graphql-loading'>
|
111
|
+
<pre style={{ opacity: 0.5 }}>
|
112
|
+
<code>{codeblock.code}</code>
|
113
|
+
</pre>
|
114
|
+
<div
|
115
|
+
style={{
|
116
|
+
position: 'absolute',
|
117
|
+
top: '8px',
|
118
|
+
right: '8px',
|
119
|
+
fontSize: '12px',
|
120
|
+
color: '#666',
|
121
|
+
backgroundColor: '#f0f0f0',
|
122
|
+
padding: '2px 6px',
|
123
|
+
borderRadius: '3px',
|
124
|
+
}}
|
125
|
+
>
|
126
|
+
Loading tree-sitter...
|
127
|
+
</div>
|
128
|
+
</div>
|
129
|
+
)
|
130
|
+
}
|
131
|
+
|
132
|
+
// Render error state with retry option
|
133
|
+
if (error) {
|
134
|
+
return (
|
135
|
+
<Box
|
136
|
+
className='graphql-error'
|
137
|
+
p={'4'}
|
138
|
+
style={{
|
139
|
+
borderRadius: 'var(--radius-2)',
|
140
|
+
backgroundColor: 'var(--gray-2)',
|
141
|
+
position: 'relative',
|
142
|
+
borderLeft: '3px solid var(--red-9)',
|
143
|
+
}}
|
144
|
+
>
|
145
|
+
<pre style={{ margin: 0, whiteSpace: 'pre' }}>
|
146
|
+
<code>{codeblock.code}</code>
|
147
|
+
</pre>
|
148
|
+
<div
|
149
|
+
style={{
|
150
|
+
color: 'var(--red-11)',
|
151
|
+
fontSize: '12px',
|
152
|
+
marginTop: '8px',
|
153
|
+
padding: '8px',
|
154
|
+
backgroundColor: 'var(--red-a3)',
|
155
|
+
borderRadius: '3px',
|
156
|
+
display: 'flex',
|
157
|
+
justifyContent: 'space-between',
|
158
|
+
alignItems: 'center',
|
159
|
+
}}
|
160
|
+
>
|
161
|
+
<span>Interactive parsing failed: {error}</span>
|
162
|
+
{retryCount < 3 && (
|
163
|
+
<button
|
164
|
+
onClick={handleRetry}
|
165
|
+
style={{
|
166
|
+
backgroundColor: 'var(--red-9)',
|
167
|
+
color: 'white',
|
168
|
+
border: 'none',
|
169
|
+
padding: '4px 8px',
|
170
|
+
borderRadius: '3px',
|
171
|
+
fontSize: '11px',
|
172
|
+
cursor: 'pointer',
|
173
|
+
}}
|
174
|
+
>
|
175
|
+
Retry ({retryCount + 1}/3)
|
176
|
+
</button>
|
177
|
+
)}
|
178
|
+
</div>
|
179
|
+
</Box>
|
180
|
+
)
|
181
|
+
}
|
182
|
+
|
183
|
+
// Fallback if no tokens were parsed or parsing failed
|
184
|
+
if (!tokens || tokens.length === 0) {
|
185
|
+
return (
|
186
|
+
<Box
|
187
|
+
className='graphql-fallback'
|
188
|
+
p={'4'}
|
189
|
+
style={{
|
190
|
+
borderRadius: 'var(--radius-2)',
|
191
|
+
backgroundColor: 'var(--gray-2)',
|
192
|
+
position: 'relative',
|
193
|
+
}}
|
194
|
+
>
|
195
|
+
<pre style={{ margin: 0, whiteSpace: 'pre' }}>
|
196
|
+
<code>{codeblock.code}</code>
|
197
|
+
</pre>
|
198
|
+
{error && (
|
199
|
+
<div
|
200
|
+
style={{
|
201
|
+
position: 'absolute',
|
202
|
+
top: '8px',
|
203
|
+
right: '8px',
|
204
|
+
fontSize: '12px',
|
205
|
+
color: 'var(--red-11)',
|
206
|
+
backgroundColor: 'var(--red-a3)',
|
207
|
+
padding: '2px 6px',
|
208
|
+
borderRadius: '3px',
|
209
|
+
maxWidth: '200px',
|
210
|
+
}}
|
211
|
+
title={error}
|
212
|
+
>
|
213
|
+
Interactive features unavailable
|
214
|
+
</div>
|
215
|
+
)}
|
216
|
+
</Box>
|
217
|
+
)
|
218
|
+
}
|
219
|
+
|
220
|
+
// Main render: Show the parsed and interactive code
|
221
|
+
return (
|
222
|
+
<Box
|
223
|
+
className='graphql-interactive'
|
224
|
+
p={'4'}
|
225
|
+
position={'relative'}
|
226
|
+
style={{
|
227
|
+
borderRadius: 'var(--radius-2)',
|
228
|
+
backgroundColor: 'var(--gray-2)',
|
229
|
+
overflowX: 'auto',
|
230
|
+
maxWidth: '100%',
|
231
|
+
}}
|
232
|
+
>
|
233
|
+
{/* Render each token as a separate span with appropriate styling */}
|
234
|
+
<pre style={{ margin: 0, whiteSpace: 'pre' }}>
|
235
|
+
<code>
|
236
|
+
{tokens.map((token, index) => {
|
237
|
+
const tokenId = `${token.start}-${token.end}-${index}`
|
238
|
+
return (
|
239
|
+
<TokenComponent
|
240
|
+
key={tokenId}
|
241
|
+
token={token}
|
242
|
+
tokenId={tokenId}
|
243
|
+
popoverState={popoverState}
|
244
|
+
schema={schema}
|
245
|
+
/>
|
246
|
+
)
|
247
|
+
})}
|
248
|
+
</code>
|
249
|
+
</pre>
|
250
|
+
{!schema && showWarningIfNoSchema && (
|
251
|
+
<div
|
252
|
+
style={{
|
253
|
+
position: 'absolute',
|
254
|
+
top: '8px',
|
255
|
+
right: '8px',
|
256
|
+
fontSize: '12px',
|
257
|
+
color: 'var(--amber-11)',
|
258
|
+
backgroundColor: 'var(--amber-a3)',
|
259
|
+
padding: '2px 6px',
|
260
|
+
borderRadius: '3px',
|
261
|
+
display: 'flex',
|
262
|
+
alignItems: 'center',
|
263
|
+
gap: '4px',
|
264
|
+
}}
|
265
|
+
title='Interactive features are not available because no GraphQL schema is configured'
|
266
|
+
>
|
267
|
+
<svg width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2'>
|
268
|
+
<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' />
|
269
|
+
<line x1='12' y1='9' x2='12' y2='13' />
|
270
|
+
<line x1='12' y1='17' x2='12.01' y2='17' />
|
271
|
+
</svg>
|
272
|
+
No schema configured
|
273
|
+
</div>
|
274
|
+
)}
|
275
|
+
</Box>
|
276
|
+
)
|
277
|
+
}
|
278
|
+
|
279
|
+
interface TokenComponentProps {
|
280
|
+
/** The token to render */
|
281
|
+
token: GraphQLToken
|
282
|
+
|
283
|
+
/** Unique ID for this token */
|
284
|
+
tokenId: string
|
285
|
+
|
286
|
+
/** Popover state manager */
|
287
|
+
popoverState: ReturnType<typeof usePopoverState>
|
288
|
+
|
289
|
+
/** The GraphQL schema for type information */
|
290
|
+
schema?: GraphQLSchema
|
291
|
+
}
|
292
|
+
|
293
|
+
/**
|
294
|
+
* Component that renders a single token with interactive features
|
295
|
+
*
|
296
|
+
* This component handles:
|
297
|
+
* - Applying syntax highlighting based on token type
|
298
|
+
* - Hover effects for interactive tokens
|
299
|
+
* - Click handlers for navigation
|
300
|
+
* - Visual feedback for CodeHike annotations
|
301
|
+
*/
|
302
|
+
const TokenComponent: React.FC<TokenComponentProps> = ({ token, tokenId, popoverState, schema }) => {
|
303
|
+
// Track hover state for interactive tokens
|
304
|
+
const [isHovered, setIsHovered] = ReactHooks.useState(false)
|
305
|
+
|
306
|
+
// Handle clicks on interactive tokens - memoized to prevent unnecessary re-renders
|
307
|
+
const handleClick = ReactHooks.useCallback((e: React.MouseEvent) => {
|
308
|
+
if (token.polen.isInteractive()) {
|
309
|
+
e.preventDefault()
|
310
|
+
e.stopPropagation()
|
311
|
+
|
312
|
+
// Don't allow pinning for invalid fields
|
313
|
+
if (token.semantic && 'kind' in token.semantic && token.semantic.kind === 'InvalidField') {
|
314
|
+
return
|
315
|
+
}
|
316
|
+
|
317
|
+
// Toggle popover pin state only - no navigation
|
318
|
+
popoverState.onTogglePin(tokenId)
|
319
|
+
}
|
320
|
+
}, [token, tokenId, popoverState])
|
321
|
+
|
322
|
+
// Show hover effects when mouse enters an interactive token - memoized
|
323
|
+
const handleMouseEnter = ReactHooks.useCallback(() => {
|
324
|
+
if (token.polen.isInteractive()) {
|
325
|
+
setIsHovered(true)
|
326
|
+
popoverState.onHoverStart(tokenId)
|
327
|
+
}
|
328
|
+
}, [token, tokenId, popoverState])
|
329
|
+
|
330
|
+
// Hide hover effects when mouse leaves - memoized
|
331
|
+
const handleMouseLeave = ReactHooks.useCallback(() => {
|
332
|
+
setIsHovered(false)
|
333
|
+
popoverState.onHoverEnd(tokenId)
|
334
|
+
}, [tokenId, popoverState])
|
335
|
+
|
336
|
+
// Get the appropriate CSS class from the token
|
337
|
+
const baseClass = token.highlighter.getCssClass()
|
338
|
+
|
339
|
+
// Map class names to inline styles
|
340
|
+
const getBaseStyle = (): React.CSSProperties => {
|
341
|
+
switch (baseClass) {
|
342
|
+
case 'graphql-keyword':
|
343
|
+
return { color: 'var(--red-11)', fontWeight: 'bold' }
|
344
|
+
case 'graphql-type-interactive':
|
345
|
+
return { color: 'var(--blue-11)', fontWeight: 500 }
|
346
|
+
case 'graphql-field-interactive':
|
347
|
+
return { color: 'var(--violet-11)' }
|
348
|
+
case 'graphql-field-error':
|
349
|
+
return {
|
350
|
+
color: 'var(--red-11)',
|
351
|
+
}
|
352
|
+
case 'graphql-error-hint':
|
353
|
+
return {
|
354
|
+
color: 'var(--red-11)',
|
355
|
+
fontSize: '0.9em',
|
356
|
+
fontStyle: 'italic',
|
357
|
+
opacity: 0.5,
|
358
|
+
}
|
359
|
+
case 'graphql-comment':
|
360
|
+
return {
|
361
|
+
color: 'var(--gray-11)',
|
362
|
+
fontStyle: 'italic',
|
363
|
+
opacity: 0.6,
|
364
|
+
}
|
365
|
+
case 'graphql-operation':
|
366
|
+
return { color: 'var(--violet-11)', fontStyle: 'italic' }
|
367
|
+
case 'graphql-fragment':
|
368
|
+
return { color: 'var(--violet-11)', fontStyle: 'italic' }
|
369
|
+
case 'graphql-variable':
|
370
|
+
return { color: 'var(--orange-11)' }
|
371
|
+
case 'graphql-argument':
|
372
|
+
return { color: 'var(--gray-12)' }
|
373
|
+
case 'graphql-string':
|
374
|
+
return { color: 'var(--blue-11)' }
|
375
|
+
case 'graphql-number':
|
376
|
+
return { color: 'var(--blue-11)' }
|
377
|
+
case 'graphql-punctuation':
|
378
|
+
return { color: 'var(--gray-11)', opacity: 0.5 }
|
379
|
+
default:
|
380
|
+
return { color: 'var(--gray-12)' }
|
381
|
+
}
|
382
|
+
}
|
383
|
+
|
384
|
+
// Check if this is an invalid field
|
385
|
+
const isInvalidField = token.semantic && 'kind' in token.semantic && token.semantic.kind === 'InvalidField'
|
386
|
+
|
387
|
+
// Build the style object for this token
|
388
|
+
const style: React.CSSProperties = {
|
389
|
+
...getBaseStyle(),
|
390
|
+
// Interactive tokens get special styling (except invalid fields)
|
391
|
+
...(token.polen.isInteractive() && !isInvalidField && {
|
392
|
+
cursor: 'pointer',
|
393
|
+
textDecoration: isHovered ? 'underline' : 'none',
|
394
|
+
backgroundColor: isHovered ? 'var(--accent-a3)' : 'transparent',
|
395
|
+
}),
|
396
|
+
|
397
|
+
// Invalid fields get different hover styling - no cursor change, no underline
|
398
|
+
...(isInvalidField && {
|
399
|
+
cursor: 'default',
|
400
|
+
textDecoration: 'underline wavy var(--red-a5)',
|
401
|
+
textUnderlineOffset: '2px',
|
402
|
+
// Subtle background change on hover to show it's interactive for popover
|
403
|
+
backgroundColor: isHovered ? 'var(--red-a2)' : 'transparent',
|
404
|
+
}),
|
405
|
+
|
406
|
+
// Tokens with CodeHike annotations get highlighted
|
407
|
+
...(token.codeHike.annotations.length > 0 && {
|
408
|
+
position: 'relative',
|
409
|
+
backgroundColor: 'var(--yellow-a3)',
|
410
|
+
}),
|
411
|
+
}
|
412
|
+
|
413
|
+
// Build the span element
|
414
|
+
const tokenSpan = (
|
415
|
+
<span
|
416
|
+
className={baseClass}
|
417
|
+
style={style}
|
418
|
+
data-token-class={baseClass}
|
419
|
+
data-interactive={token.polen.isInteractive()}
|
420
|
+
>
|
421
|
+
{token.text}
|
422
|
+
</span>
|
423
|
+
)
|
424
|
+
|
425
|
+
// Wrap in popover if token has semantic information
|
426
|
+
return (
|
427
|
+
<GraphQLTokenPopover
|
428
|
+
token={token}
|
429
|
+
open={popoverState.isOpen(tokenId)}
|
430
|
+
pinned={popoverState.isPinned(tokenId)}
|
431
|
+
onTriggerHover={handleMouseEnter}
|
432
|
+
onTriggerLeave={handleMouseLeave}
|
433
|
+
onTriggerClick={handleClick}
|
434
|
+
onContentHover={() => popoverState.onPopoverHover(tokenId)}
|
435
|
+
onContentLeave={() => popoverState.onPopoverLeave(tokenId)}
|
436
|
+
onClose={() => popoverState.unpin(tokenId)}
|
437
|
+
>
|
438
|
+
{tokenSpan}
|
439
|
+
</GraphQLTokenPopover>
|
440
|
+
)
|
441
|
+
}
|
442
|
+
|
443
|
+
/**
|
444
|
+
* Main GraphQL Interactive component with error boundary protection
|
445
|
+
*
|
446
|
+
* This is the component that should be used in user code. It wraps the
|
447
|
+
* internal implementation with an error boundary that provides graceful
|
448
|
+
* fallback to static code rendering if interactive features fail.
|
449
|
+
*/
|
450
|
+
export const GraphQLInteractive: React.FC<GraphQLInteractiveProps> = (props) => {
|
451
|
+
return (
|
452
|
+
<GraphQLErrorBoundary
|
453
|
+
fallbackCode={props.codeblock.code}
|
454
|
+
onError={(error, errorInfo) => {
|
455
|
+
// Log error for debugging (only in development)
|
456
|
+
if (process.env['NODE_ENV'] === 'development') {
|
457
|
+
console.error('GraphQL Interactive Error Boundary:', error, errorInfo)
|
458
|
+
}
|
459
|
+
}}
|
460
|
+
>
|
461
|
+
<GraphQLInteractiveImpl {...props} />
|
462
|
+
</GraphQLErrorBoundary>
|
463
|
+
)
|
464
|
+
}
|
@@ -0,0 +1,96 @@
|
|
1
|
+
/**
|
2
|
+
* Error boundary for GraphQL Interactive components
|
3
|
+
*
|
4
|
+
* Provides graceful fallback rendering when the interactive GraphQL parser
|
5
|
+
* encounters errors. Falls back to static syntax highlighting.
|
6
|
+
*/
|
7
|
+
|
8
|
+
import type { React } from '#dep/react/index'
|
9
|
+
import { React as ReactHooks } from '#dep/react/index'
|
10
|
+
import { Box } from '@radix-ui/themes'
|
11
|
+
|
12
|
+
interface GraphQLErrorBoundaryProps {
|
13
|
+
/** Child components to protect */
|
14
|
+
children: React.ReactNode
|
15
|
+
/** Fallback code to display if interactive parsing fails */
|
16
|
+
fallbackCode: string
|
17
|
+
/** Optional callback when errors occur */
|
18
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void
|
19
|
+
}
|
20
|
+
|
21
|
+
interface GraphQLErrorBoundaryState {
|
22
|
+
hasError: boolean
|
23
|
+
error?: Error
|
24
|
+
}
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Error boundary that catches React errors in GraphQL Interactive components
|
28
|
+
* and provides a fallback static code display.
|
29
|
+
*
|
30
|
+
* @example
|
31
|
+
* ```tsx
|
32
|
+
* <GraphQLErrorBoundary fallbackCode={codeblock.code}>
|
33
|
+
* <GraphQLInteractive codeblock={codeblock} schema={schema} />
|
34
|
+
* </GraphQLErrorBoundary>
|
35
|
+
* ```
|
36
|
+
*/
|
37
|
+
export class GraphQLErrorBoundary extends ReactHooks.Component<
|
38
|
+
GraphQLErrorBoundaryProps,
|
39
|
+
GraphQLErrorBoundaryState
|
40
|
+
> {
|
41
|
+
constructor(props: GraphQLErrorBoundaryProps) {
|
42
|
+
super(props)
|
43
|
+
this.state = { hasError: false }
|
44
|
+
}
|
45
|
+
|
46
|
+
static getDerivedStateFromError(error: Error): GraphQLErrorBoundaryState {
|
47
|
+
// Update state so the next render will show the fallback UI
|
48
|
+
return { hasError: true, error }
|
49
|
+
}
|
50
|
+
|
51
|
+
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
52
|
+
// Log the error or send to error reporting service
|
53
|
+
console.error('GraphQL Interactive Error:', error, errorInfo)
|
54
|
+
|
55
|
+
// Call optional error handler
|
56
|
+
this.props.onError?.(error, errorInfo)
|
57
|
+
}
|
58
|
+
|
59
|
+
override render() {
|
60
|
+
if (this.state.hasError) {
|
61
|
+
return (
|
62
|
+
<Box
|
63
|
+
className='graphql-error-fallback'
|
64
|
+
p={'4'}
|
65
|
+
style={{
|
66
|
+
borderRadius: 'var(--radius-2)',
|
67
|
+
backgroundColor: 'var(--gray-2)',
|
68
|
+
position: 'relative',
|
69
|
+
borderLeft: '3px solid var(--red-9)',
|
70
|
+
}}
|
71
|
+
>
|
72
|
+
<pre style={{ margin: 0, whiteSpace: 'pre' }}>
|
73
|
+
<code>{this.props.fallbackCode}</code>
|
74
|
+
</pre>
|
75
|
+
<div
|
76
|
+
style={{
|
77
|
+
position: 'absolute',
|
78
|
+
top: '8px',
|
79
|
+
right: '8px',
|
80
|
+
fontSize: '12px',
|
81
|
+
color: 'var(--red-11)',
|
82
|
+
backgroundColor: 'var(--red-a3)',
|
83
|
+
padding: '2px 6px',
|
84
|
+
borderRadius: '3px',
|
85
|
+
}}
|
86
|
+
title={this.state.error?.message || 'Interactive features failed to load'}
|
87
|
+
>
|
88
|
+
Interactive mode unavailable
|
89
|
+
</div>
|
90
|
+
</Box>
|
91
|
+
)
|
92
|
+
}
|
93
|
+
|
94
|
+
return this.props.children
|
95
|
+
}
|
96
|
+
}
|