polen 0.10.0-next.12 → 0.10.0-next.13

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.
Files changed (42) hide show
  1. package/build/lib/graphql-document/components/CopyButton.d.ts +19 -0
  2. package/build/lib/graphql-document/components/CopyButton.d.ts.map +1 -0
  3. package/build/lib/graphql-document/components/CopyButton.js +43 -0
  4. package/build/lib/graphql-document/components/CopyButton.js.map +1 -0
  5. package/build/lib/graphql-document/components/GraphQLDocument.d.ts +0 -4
  6. package/build/lib/graphql-document/components/GraphQLDocument.d.ts.map +1 -1
  7. package/build/lib/graphql-document/components/GraphQLDocument.js +31 -74
  8. package/build/lib/graphql-document/components/GraphQLDocument.js.map +1 -1
  9. package/build/lib/graphql-document/components/GraphQLIdentifierPopover.d.ts +33 -0
  10. package/build/lib/graphql-document/components/GraphQLIdentifierPopover.d.ts.map +1 -0
  11. package/build/lib/graphql-document/components/GraphQLIdentifierPopover.js +48 -0
  12. package/build/lib/graphql-document/components/GraphQLIdentifierPopover.js.map +1 -0
  13. package/build/lib/graphql-document/components/IdentifierLink.d.ts +15 -13
  14. package/build/lib/graphql-document/components/IdentifierLink.d.ts.map +1 -1
  15. package/build/lib/graphql-document/components/IdentifierLink.js +51 -117
  16. package/build/lib/graphql-document/components/IdentifierLink.js.map +1 -1
  17. package/build/lib/graphql-document/components/graphql-document-styles.d.ts +5 -0
  18. package/build/lib/graphql-document/components/graphql-document-styles.d.ts.map +1 -0
  19. package/build/lib/graphql-document/components/graphql-document-styles.js +167 -0
  20. package/build/lib/graphql-document/components/graphql-document-styles.js.map +1 -0
  21. package/build/lib/graphql-document/components/index.d.ts +2 -1
  22. package/build/lib/graphql-document/components/index.d.ts.map +1 -1
  23. package/build/lib/graphql-document/components/index.js +2 -1
  24. package/build/lib/graphql-document/components/index.js.map +1 -1
  25. package/build/lib/graphql-document/hooks/use-tooltip-state.d.ts +43 -0
  26. package/build/lib/graphql-document/hooks/use-tooltip-state.d.ts.map +1 -0
  27. package/build/lib/graphql-document/hooks/use-tooltip-state.js +132 -0
  28. package/build/lib/graphql-document/hooks/use-tooltip-state.js.map +1 -0
  29. package/package.json +2 -1
  30. package/src/lib/graphql-document/components/CopyButton.tsx +76 -0
  31. package/src/lib/graphql-document/components/GraphQLDocument.tsx +52 -86
  32. package/src/lib/graphql-document/components/GraphQLIdentifierPopover.tsx +197 -0
  33. package/src/lib/graphql-document/components/IdentifierLink.tsx +105 -166
  34. package/src/lib/graphql-document/components/graphql-document-styles.ts +167 -0
  35. package/src/lib/graphql-document/components/index.ts +2 -1
  36. package/src/lib/graphql-document/hooks/use-tooltip-state.test.ts +76 -0
  37. package/src/lib/graphql-document/hooks/use-tooltip-state.ts +191 -0
  38. package/build/lib/graphql-document/components/HoverTooltip.d.ts +0 -35
  39. package/build/lib/graphql-document/components/HoverTooltip.d.ts.map +0 -1
  40. package/build/lib/graphql-document/components/HoverTooltip.js +0 -132
  41. package/build/lib/graphql-document/components/HoverTooltip.js.map +0 -1
  42. package/src/lib/graphql-document/components/HoverTooltip.tsx +0 -282
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Copy button component for GraphQL documents
3
+ */
4
+
5
+ import type { React } from '#dep/react/index'
6
+ import { React as ReactHooks } from '#dep/react/index'
7
+ import { CheckIcon, CopyIcon } from '@radix-ui/react-icons'
8
+ import { Button } from '@radix-ui/themes'
9
+
10
+ export interface CopyButtonProps {
11
+ /** The text to copy */
12
+ text: string
13
+ /** Optional className */
14
+ className?: string
15
+ /** Size variant */
16
+ size?: '1' | '2' | '3'
17
+ }
18
+
19
+ /**
20
+ * Copy button for GraphQL code blocks
21
+ *
22
+ * Shows a copy icon that changes to a checkmark when clicked
23
+ */
24
+ export const CopyButton: React.FC<CopyButtonProps> = ({
25
+ text,
26
+ className = '',
27
+ size = '1',
28
+ }) => {
29
+ const [copied, setCopied] = ReactHooks.useState(false)
30
+ const timeoutRef = ReactHooks.useRef<NodeJS.Timeout | null>(null)
31
+
32
+ const handleCopy = ReactHooks.useCallback(async (e: React.MouseEvent) => {
33
+ e.preventDefault()
34
+ e.stopPropagation()
35
+
36
+ try {
37
+ await navigator.clipboard.writeText(text)
38
+ setCopied(true)
39
+
40
+ // Clear any existing timeout
41
+ if (timeoutRef.current) {
42
+ clearTimeout(timeoutRef.current)
43
+ }
44
+
45
+ // Reset after 2 seconds
46
+ timeoutRef.current = setTimeout(() => {
47
+ setCopied(false)
48
+ timeoutRef.current = null
49
+ }, 2000)
50
+ } catch (err) {
51
+ console.error('Failed to copy text:', err)
52
+ }
53
+ }, [text])
54
+
55
+ // Cleanup timeout on unmount
56
+ ReactHooks.useEffect(() => {
57
+ return () => {
58
+ if (timeoutRef.current) {
59
+ clearTimeout(timeoutRef.current)
60
+ }
61
+ }
62
+ }, [])
63
+
64
+ return (
65
+ <Button
66
+ size={size}
67
+ variant='ghost'
68
+ className={`graphql-copy-button ${className}`}
69
+ onClick={handleCopy}
70
+ aria-label={copied ? 'Copied!' : 'Copy code'}
71
+ data-copied={copied}
72
+ >
73
+ {copied ? <CheckIcon width='16' height='16' /> : <CopyIcon width='16' height='16' />}
74
+ </Button>
75
+ )
76
+ }
@@ -1,14 +1,15 @@
1
1
  import type { React } from '#dep/react/index'
2
+ import { React as ReactHooks } from '#dep/react/index'
2
3
  import type { GraphQLSchema } from 'graphql'
3
- import { useEffect, useMemo, useRef, useState } from 'react'
4
4
  import { useNavigate } from 'react-router'
5
5
  import { analyze } from '../analysis.ts'
6
+ import { useTooltipState } from '../hooks/use-tooltip-state.ts'
6
7
  import { createSimplePositionCalculator } from '../positioning-simple.ts'
7
8
  import { createPolenSchemaResolver } from '../schema-integration.ts'
8
9
  import type { Identifier } from '../types.ts'
9
- import { hoverTooltipStyles } from './HoverTooltip.tsx'
10
+ import { CopyButton } from './CopyButton.tsx'
11
+ import { graphqlDocumentStyles } from './graphql-document-styles.ts'
10
12
  import { IdentifierLink } from './IdentifierLink.tsx'
11
- import { identifierLinkStyles } from './IdentifierLink.tsx'
12
13
 
13
14
  /**
14
15
  * Options for the GraphQL document component
@@ -58,58 +59,50 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
58
59
  onNavigate,
59
60
  validate = true,
60
61
  className = '',
61
- } = options
62
+ } = options || {}
62
63
 
63
64
  const navigate = useNavigate()
64
65
  const handleNavigate = onNavigate || ((url: string) => navigate(url))
65
66
 
66
67
  // Container ref for positioning calculations
67
- const containerRef = useRef<HTMLDivElement>(null)
68
- const [isReady, setIsReady] = useState(false)
69
- const [openTooltipId, setOpenTooltipId] = useState<string | null>(null)
70
-
71
- // Handle click outside to close tooltips
72
- useEffect(() => {
73
- if (!openTooltipId) return
74
-
75
- const handleClickOutside = (event: MouseEvent) => {
76
- // Check if click is outside the tooltip and identifier links
77
- const target = event.target as HTMLElement
78
-
79
- // Don't close if clicking on an identifier link or tooltip
80
- if (
81
- target.closest('.graphql-identifier-overlay')
82
- || target.closest('.graphql-hover-tooltip')
83
- ) {
84
- return
68
+ const containerRef = ReactHooks.useRef<HTMLDivElement>(null)
69
+ const [isReady, setIsReady] = ReactHooks.useState(false)
70
+
71
+ // Use tooltip state management
72
+ const tooltipState = useTooltipState({
73
+ showDelay: 300,
74
+ hideDelay: 200, // Increased for smoother experience
75
+ allowMultiplePins: true,
76
+ })
77
+
78
+ // Handle escape key to unpin all
79
+ ReactHooks.useEffect(() => {
80
+ const handleKeyDown = (event: KeyboardEvent) => {
81
+ if (event.key === 'Escape') {
82
+ tooltipState.unpinAll()
85
83
  }
86
-
87
- // Close the tooltip
88
- setOpenTooltipId(null)
89
84
  }
90
85
 
91
- // Add event listener
92
- document.addEventListener('mousedown', handleClickOutside)
93
-
86
+ document.addEventListener('keydown', handleKeyDown)
94
87
  return () => {
95
- document.removeEventListener('mousedown', handleClickOutside)
88
+ document.removeEventListener('keydown', handleKeyDown)
96
89
  }
97
- }, [openTooltipId])
90
+ }, [tooltipState])
98
91
 
99
92
  // Layer 1: Parse and analyze
100
- const analysisResult = useMemo(() => {
93
+ const analysisResult = ReactHooks.useMemo(() => {
101
94
  if (plain) return null
102
95
  const result = analyze(children, { schema })
103
96
  return result
104
97
  }, [children, plain, schema])
105
98
 
106
99
  // Layer 2: Schema resolution
107
- const resolver = useMemo(() => {
100
+ const resolver = ReactHooks.useMemo(() => {
108
101
  if (!schema || plain) return null
109
102
  return createPolenSchemaResolver(schema)
110
103
  }, [schema, plain])
111
104
 
112
- const resolutions = useMemo(() => {
105
+ const resolutions = ReactHooks.useMemo(() => {
113
106
  if (!analysisResult || !resolver) {
114
107
  return new Map()
115
108
  }
@@ -125,15 +118,17 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
125
118
  }, [analysisResult, resolver])
126
119
 
127
120
  // Layer 3: Position calculation
128
- const positionCalculator = useMemo(() => {
121
+ const positionCalculator = ReactHooks.useMemo(() => {
129
122
  if (plain) return null
130
123
  return createSimplePositionCalculator()
131
124
  }, [plain])
132
125
 
133
- const [positions, setPositions] = useState<Map<string, { position: any; identifier: Identifier }>>(new Map())
126
+ const [positions, setPositions] = ReactHooks.useState<Map<string, { position: any; identifier: Identifier }>>(
127
+ new Map(),
128
+ )
134
129
 
135
130
  // Prepare code block and calculate positions after render
136
- useEffect(() => {
131
+ ReactHooks.useEffect(() => {
137
132
  if (!containerRef.current || !analysisResult || !positionCalculator || plain) {
138
133
  return
139
134
  }
@@ -162,7 +157,7 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
162
157
  }, [analysisResult, positionCalculator, plain, highlightedHtml])
163
158
 
164
159
  // Handle resize events
165
- useEffect(() => {
160
+ ReactHooks.useEffect(() => {
166
161
  if (!containerRef.current || !positionCalculator || plain) return
167
162
 
168
163
  const handleResize = () => {
@@ -180,20 +175,19 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
180
175
  }, [positionCalculator, plain])
181
176
 
182
177
  // Validation errors
183
- const validationErrors = useMemo(() => {
178
+ const validationErrors = ReactHooks.useMemo(() => {
184
179
  if (!validate || !analysisResult || !schema) return []
185
180
  return analysisResult.errors
186
181
  }, [validate, analysisResult, schema])
187
182
 
188
183
  return (
189
184
  <>
190
- {/* Inject styles */}
191
- <style dangerouslySetInnerHTML={{ __html: identifierLinkStyles + '\n' + hoverTooltipStyles }} />
192
-
185
+ <style dangerouslySetInnerHTML={{ __html: graphqlDocumentStyles }} />
193
186
  <div
194
187
  ref={containerRef}
195
- className={`graphql-document ${className} ${debug ? 'graphql-debug-mode' : ''}`}
196
- style={{ position: 'relative' }}
188
+ className={`graphql-document ${className} ${debug ? 'graphql-debug-mode' : ''} ${
189
+ !isReady && !plain ? 'graphql-loading' : ''
190
+ }`}
197
191
  >
198
192
  {/* Base syntax highlighting */}
199
193
  {highlightedHtml ? <div dangerouslySetInnerHTML={{ __html: highlightedHtml }} /> : (
@@ -202,6 +196,15 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
202
196
  </pre>
203
197
  )}
204
198
 
199
+ {/* Copy button */}
200
+ {!plain && (
201
+ <CopyButton
202
+ text={children}
203
+ className='graphql-document-copy'
204
+ size='2'
205
+ />
206
+ )}
207
+
205
208
  {/* Interactive overlay layer */}
206
209
  {!plain && isReady && (
207
210
  <div className='graphql-interaction-layer' style={{ pointerEvents: 'none' }}>
@@ -219,8 +222,12 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
219
222
  position={position}
220
223
  onNavigate={handleNavigate}
221
224
  debug={debug}
222
- isOpen={openTooltipId === id}
223
- onToggle={(open) => setOpenTooltipId(open ? id : null)}
225
+ isOpen={tooltipState.isOpen(id)}
226
+ isPinned={tooltipState.isPinned(id)}
227
+ onHoverStart={() => tooltipState.onHoverStart(id)}
228
+ onHoverEnd={() => tooltipState.onHoverEnd(id)}
229
+ onTogglePin={() => tooltipState.onTogglePin(id)}
230
+ onTooltipHover={() => tooltipState.onTooltipHover(id)}
224
231
  />
225
232
  )
226
233
  })}
@@ -241,44 +248,3 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
241
248
  </>
242
249
  )
243
250
  }
244
-
245
- /**
246
- * Default styles for the GraphQL document component
247
- */
248
- export const graphqlDocumentStyles = `
249
- .graphql-document {
250
- position: relative;
251
- }
252
-
253
- .graphql-interaction-layer {
254
- position: absolute;
255
- top: 0;
256
- left: 0;
257
- right: 0;
258
- bottom: 0;
259
- pointer-events: none;
260
- }
261
-
262
- .graphql-interaction-layer > * {
263
- pointer-events: auto;
264
- }
265
-
266
- .graphql-validation-errors {
267
- margin-top: 1rem;
268
- padding: 0.5rem;
269
- background-color: var(--red-2);
270
- border: 1px solid var(--red-6);
271
- border-radius: 4px;
272
- }
273
-
274
- .graphql-error {
275
- color: var(--red-11);
276
- font-size: 0.875rem;
277
- margin: 0.25rem 0;
278
- }
279
-
280
- .graphql-debug-mode [data-graphql-id] {
281
- background-color: rgba(59, 130, 246, 0.1);
282
- outline: 1px solid rgba(59, 130, 246, 0.3);
283
- }
284
- `
@@ -0,0 +1,197 @@
1
+ /**
2
+ * GraphQL Identifier Popover using Radix Themes
3
+ *
4
+ * Displays rich information about GraphQL identifiers on hover/click
5
+ */
6
+
7
+ import type { React } from '#dep/react/index'
8
+ import { Cross2Icon } from '@radix-ui/react-icons'
9
+ import { Badge, Box, Card, Flex, IconButton, Link, Popover, Text } from '@radix-ui/themes'
10
+ import type { Documentation } from '../schema-integration.ts'
11
+ import type { Identifier } from '../types.ts'
12
+
13
+ export interface GraphQLIdentifierPopoverProps {
14
+ /** The identifier being shown */
15
+ identifier: Identifier
16
+ /** Documentation from schema */
17
+ documentation: Documentation
18
+ /** Whether this identifier has an error */
19
+ hasError?: boolean
20
+ /** Reference URL for "View docs" link */
21
+ referenceUrl: string
22
+ /** Whether popover is open */
23
+ open: boolean
24
+ /** Whether popover is pinned */
25
+ isPinned: boolean
26
+ /** Callback when open state changes */
27
+ onOpenChange: (open: boolean) => void
28
+ /** Callback to navigate to docs */
29
+ onNavigate?: (url: string) => void
30
+ /** The trigger element */
31
+ children: React.ReactNode
32
+ }
33
+
34
+ /**
35
+ * Popover content for GraphQL identifiers
36
+ */
37
+ export const GraphQLIdentifierPopover: React.FC<GraphQLIdentifierPopoverProps> = ({
38
+ identifier,
39
+ documentation,
40
+ hasError = false,
41
+ referenceUrl,
42
+ open,
43
+ isPinned,
44
+ onOpenChange,
45
+ onNavigate,
46
+ children,
47
+ }) => {
48
+ // Determine badge color based on identifier kind
49
+ const getBadgeColor = () => {
50
+ switch (identifier.kind) {
51
+ case 'Type':
52
+ return 'blue'
53
+ case 'Field':
54
+ return 'green'
55
+ case 'Argument':
56
+ return 'orange'
57
+ case 'Variable':
58
+ return 'purple'
59
+ case 'Directive':
60
+ return 'amber'
61
+ case 'Fragment':
62
+ return 'cyan'
63
+ default:
64
+ return 'gray'
65
+ }
66
+ }
67
+
68
+ return (
69
+ <Popover.Root open={open} onOpenChange={onOpenChange}>
70
+ <Popover.Trigger>
71
+ {children}
72
+ </Popover.Trigger>
73
+
74
+ <Popover.Content
75
+ className='graphql-identifier-popover'
76
+ style={{ maxWidth: 400 }}
77
+ onInteractOutside={(e) => {
78
+ // Prevent closing when clicking inside popover if pinned
79
+ if (isPinned) {
80
+ e.preventDefault()
81
+ }
82
+ }}
83
+ >
84
+ <Flex direction='column' gap='2'>
85
+ {/* Header with name, kind, and close button */}
86
+ <Flex justify='between' align='center'>
87
+ <Flex align='center' gap='2'>
88
+ <Text size='2' weight='bold'>
89
+ {identifier.name}
90
+ </Text>
91
+ <Badge color={getBadgeColor()} size='1'>
92
+ {identifier.kind}
93
+ </Badge>
94
+ </Flex>
95
+ {isPinned && (
96
+ <IconButton
97
+ size='1'
98
+ variant='ghost'
99
+ onClick={() => onOpenChange(false)}
100
+ aria-label='Close popover'
101
+ >
102
+ <Cross2Icon />
103
+ </IconButton>
104
+ )}
105
+ </Flex>
106
+
107
+ {/* Type signature */}
108
+ <Box>
109
+ <Text size='1' color='gray'>
110
+ Type: <Text as='span' size='1' style={{ fontFamily: 'monospace' }}>{documentation.typeInfo}</Text>
111
+ </Text>
112
+ </Box>
113
+
114
+ {/* Description */}
115
+ {documentation.description && (
116
+ <Box>
117
+ <Text size='1'>
118
+ {documentation.description}
119
+ </Text>
120
+ </Box>
121
+ )}
122
+
123
+ {/* Default value for arguments */}
124
+ {documentation.defaultValue && (
125
+ <Box>
126
+ <Text size='1' color='gray'>
127
+ Default:{' '}
128
+ <Text as='span' size='1' style={{ fontFamily: 'monospace' }}>{documentation.defaultValue}</Text>
129
+ </Text>
130
+ </Box>
131
+ )}
132
+
133
+ {/* Deprecation warning */}
134
+ {documentation.deprecated && (
135
+ <Box
136
+ style={{
137
+ padding: '8px',
138
+ backgroundColor: 'var(--amber-2)',
139
+ borderRadius: '4px',
140
+ border: '1px solid var(--amber-6)',
141
+ }}
142
+ >
143
+ <Text size='1' color='amber'>
144
+ ⚠️ Deprecated: {documentation.deprecated.reason}
145
+ </Text>
146
+ {documentation.deprecated.replacement && (
147
+ <Text size='1' color='amber'>
148
+ Use {documentation.deprecated.replacement} instead.
149
+ </Text>
150
+ )}
151
+ </Box>
152
+ )}
153
+
154
+ {/* Error message */}
155
+ {hasError && (
156
+ <Box
157
+ style={{
158
+ padding: '8px',
159
+ backgroundColor: 'var(--red-2)',
160
+ borderRadius: '4px',
161
+ border: '1px solid var(--red-6)',
162
+ }}
163
+ >
164
+ <Text size='1' color='red'>
165
+ ❌ {identifier.kind} not found in schema
166
+ </Text>
167
+ </Box>
168
+ )}
169
+
170
+ {/* Schema path */}
171
+ <Box>
172
+ <Text size='1' color='gray'>
173
+ Path: {identifier.schemaPath.join(' → ')}
174
+ </Text>
175
+ </Box>
176
+
177
+ {/* View docs link */}
178
+ {onNavigate && !hasError && (
179
+ <Box>
180
+ <Link
181
+ size='1'
182
+ href={referenceUrl}
183
+ onClick={(e: React.MouseEvent) => {
184
+ e.preventDefault()
185
+ onNavigate(referenceUrl)
186
+ onOpenChange(false)
187
+ }}
188
+ >
189
+ View full documentation →
190
+ </Link>
191
+ </Box>
192
+ )}
193
+ </Flex>
194
+ </Popover.Content>
195
+ </Popover.Root>
196
+ )
197
+ }