Transfer from geelen/use-mcp
This commit is contained in:
parent
d1d7803d9a
commit
de192f4de0
8 changed files with 9512 additions and 136 deletions
37
examples/inspector/src/App.tsx
Normal file
37
examples/inspector/src/App.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import { McpServers } from './components/McpServers.js'
|
||||
import { OAuthCallback } from './components/OAuthCallback.js'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/oauth/callback" element={<OAuthCallback />} />
|
||||
<Route path="/" element={
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">MCP Inspector</h1>
|
||||
<p className="text-gray-600">
|
||||
Minimal demo showcasing the{' '}
|
||||
<a
|
||||
href="https://github.com/modelcontextprotocol/use-mcp"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline font-medium transition-colors"
|
||||
>
|
||||
use-mcp
|
||||
</a>{' '}
|
||||
React hook
|
||||
</p>
|
||||
</div>
|
||||
<McpServers />
|
||||
</div>
|
||||
</div>
|
||||
} />
|
||||
</Routes>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
557
examples/inspector/src/components/McpServers.tsx
Normal file
557
examples/inspector/src/components/McpServers.tsx
Normal file
|
|
@ -0,0 +1,557 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useMcp, type Tool } from 'use-mcp/react'
|
||||
import { Info, X, ChevronRight, ChevronDown } from 'lucide-react'
|
||||
|
||||
|
||||
// MCP Connection wrapper that only renders when active
|
||||
function McpConnection({
|
||||
serverUrl,
|
||||
onConnectionUpdate,
|
||||
}: {
|
||||
serverUrl: string
|
||||
onConnectionUpdate: (data: any) => void
|
||||
}) {
|
||||
// Use the MCP hook with the server URL
|
||||
const connection = useMcp({
|
||||
url: serverUrl,
|
||||
debug: true,
|
||||
autoRetry: false,
|
||||
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
|
||||
})
|
||||
|
||||
// Update parent component with connection data
|
||||
useEffect(() => {
|
||||
onConnectionUpdate(connection)
|
||||
}, [
|
||||
connection.state,
|
||||
connection.tools,
|
||||
connection.error,
|
||||
connection.log.length,
|
||||
connection.authUrl,
|
||||
])
|
||||
|
||||
// Return null as this is just a hook wrapper
|
||||
return null
|
||||
}
|
||||
|
||||
export function McpServers({
|
||||
onToolsUpdate,
|
||||
}: {
|
||||
onToolsUpdate?: (tools: Tool[]) => void
|
||||
}) {
|
||||
const [serverUrl, setServerUrl] = useState(() => {
|
||||
return sessionStorage.getItem('mcpServerUrl') || ''
|
||||
})
|
||||
const [isActive, setIsActive] = useState(false)
|
||||
|
||||
const [connectionData, setConnectionData] = useState<any>({
|
||||
state: 'not-connected',
|
||||
tools: [],
|
||||
error: undefined,
|
||||
log: [],
|
||||
authUrl: undefined,
|
||||
retry: () => {},
|
||||
disconnect: () => {},
|
||||
authenticate: () => Promise.resolve(undefined),
|
||||
callTool: (_name: string, _args?: Record<string, unknown>) =>
|
||||
Promise.resolve(undefined),
|
||||
clearStorage: () => {},
|
||||
})
|
||||
const [toolForms, setToolForms] = useState<Record<string, Record<string, any>>>({})
|
||||
const [toolExecutionLogs, setToolExecutionLogs] = useState<Record<string, string>>({})
|
||||
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>({})
|
||||
const logRef = useRef<HTMLDivElement>(null)
|
||||
const executionLogRefs = useRef<Record<string, HTMLTextAreaElement | null>>({})
|
||||
|
||||
// Extract connection properties
|
||||
const { state, tools, log, authUrl, disconnect, authenticate } =
|
||||
connectionData
|
||||
|
||||
// Notify parent component when tools change
|
||||
useEffect(() => {
|
||||
if (onToolsUpdate && tools.length > 0) {
|
||||
onToolsUpdate(
|
||||
tools.map((t: Tool) => ({
|
||||
...t,
|
||||
callTool: (args: Record<string, unknown>) =>
|
||||
connectionData.callTool(t.name, args),
|
||||
})),
|
||||
)
|
||||
}
|
||||
}, [tools, onToolsUpdate])
|
||||
|
||||
// Handle connection
|
||||
const handleConnect = () => {
|
||||
if (!serverUrl.trim()) return
|
||||
setIsActive(true)
|
||||
}
|
||||
|
||||
// Handle disconnection
|
||||
const handleDisconnect = () => {
|
||||
disconnect()
|
||||
setIsActive(false)
|
||||
setConnectionData({
|
||||
state: 'not-connected',
|
||||
tools: [],
|
||||
error: undefined,
|
||||
log: [],
|
||||
authUrl: undefined,
|
||||
retry: () => {},
|
||||
disconnect: () => {},
|
||||
authenticate: () => Promise.resolve(undefined),
|
||||
callTool: (_name: string, _args?: Record<string, unknown>) =>
|
||||
Promise.resolve(undefined),
|
||||
clearStorage: () => {},
|
||||
})
|
||||
}
|
||||
|
||||
// Handle authentication if popup was blocked
|
||||
const handleManualAuth = async () => {
|
||||
try {
|
||||
await authenticate()
|
||||
} catch (err) {
|
||||
console.error('Authentication error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll log to bottom
|
||||
useEffect(() => {
|
||||
if (logRef.current) {
|
||||
logRef.current.scrollTop = logRef.current.scrollHeight
|
||||
}
|
||||
}, [log])
|
||||
|
||||
// Generate status badge based on connection state
|
||||
const getStatusBadge = () => {
|
||||
const baseClasses = 'px-2 py-1 rounded-full text-xs font-medium'
|
||||
|
||||
switch (state) {
|
||||
case 'discovering':
|
||||
return (
|
||||
<span className={`${baseClasses} bg-blue-100 text-blue-800`}>
|
||||
Discovering
|
||||
</span>
|
||||
)
|
||||
case 'authenticating':
|
||||
return (
|
||||
<span className={`${baseClasses} bg-purple-100 text-purple-800`}>
|
||||
Authenticating
|
||||
</span>
|
||||
)
|
||||
case 'connecting':
|
||||
return (
|
||||
<span className={`${baseClasses} bg-yellow-100 text-yellow-800`}>
|
||||
Connecting
|
||||
</span>
|
||||
)
|
||||
case 'loading':
|
||||
return (
|
||||
<span className={`${baseClasses} bg-orange-100 text-orange-800`}>
|
||||
Loading
|
||||
</span>
|
||||
)
|
||||
case 'ready':
|
||||
return (
|
||||
<span className={`${baseClasses} bg-green-100 text-green-800`}>
|
||||
Connected
|
||||
</span>
|
||||
)
|
||||
case 'failed':
|
||||
return (
|
||||
<span className={`${baseClasses} bg-red-100 text-red-800`}>
|
||||
Failed
|
||||
</span>
|
||||
)
|
||||
case 'not-connected':
|
||||
default:
|
||||
return (
|
||||
<span className={`${baseClasses} bg-gray-100 text-gray-800`}>
|
||||
Not Connected
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Log the tools to console when they change (for debugging)
|
||||
useEffect(() => {
|
||||
if (tools.length > 0) {
|
||||
console.log('MCP Tools available:', tools)
|
||||
}
|
||||
}, [tools])
|
||||
|
||||
// Initialize form data when tools change
|
||||
useEffect(() => {
|
||||
const newForms: Record<string, Record<string, any>> = {}
|
||||
tools.forEach((tool: Tool) => {
|
||||
if (tool.inputSchema && tool.inputSchema.properties) {
|
||||
const formData: Record<string, any> = {}
|
||||
Object.entries(tool.inputSchema.properties).forEach(([key, schema]: [string, any]) => {
|
||||
// Set default values based on type
|
||||
if (schema.type === 'number' || schema.type === 'integer') {
|
||||
formData[key] = schema.default || 0
|
||||
} else if (schema.type === 'boolean') {
|
||||
formData[key] = schema.default || false
|
||||
} else {
|
||||
formData[key] = schema.default || ''
|
||||
}
|
||||
})
|
||||
newForms[tool.name] = formData
|
||||
}
|
||||
})
|
||||
setToolForms(newForms)
|
||||
}, [tools])
|
||||
|
||||
// Handle form input changes
|
||||
const handleFormChange = (toolName: string, fieldName: string, value: any) => {
|
||||
setToolForms(prev => ({
|
||||
...prev,
|
||||
[toolName]: {
|
||||
...prev[toolName],
|
||||
[fieldName]: value
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Helper function to clean and update execution log
|
||||
const updateExecutionLog = (toolName: string, newContent: string) => {
|
||||
setToolExecutionLogs(prev => {
|
||||
const currentLog = prev[toolName] || ''
|
||||
const updatedLog = currentLog + newContent
|
||||
// Remove blank lines and rejoin
|
||||
const cleanedLog = updatedLog
|
||||
.split('\n')
|
||||
.filter(line => line.trim() !== '')
|
||||
.join('\n')
|
||||
return {
|
||||
...prev,
|
||||
[toolName]: cleanedLog + '\n'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Handle tool execution
|
||||
const handleRunTool = async (tool: Tool) => {
|
||||
const args = toolForms[tool.name] || {}
|
||||
const argsStr = JSON.stringify(args)
|
||||
|
||||
// Add execution start message
|
||||
const startMessage = `Calling ${tool.name}(${argsStr})\n`
|
||||
updateExecutionLog(tool.name, startMessage)
|
||||
|
||||
try {
|
||||
const result = await connectionData.callTool(tool.name, args)
|
||||
const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
||||
updateExecutionLog(tool.name, `${resultStr}\n`)
|
||||
} catch (error) {
|
||||
updateExecutionLog(tool.name, `Error: ${error}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll execution logs to bottom when they change
|
||||
useEffect(() => {
|
||||
Object.keys(toolExecutionLogs).forEach(toolName => {
|
||||
const textarea = executionLogRefs.current[toolName]
|
||||
if (textarea) {
|
||||
textarea.scrollTop = textarea.scrollHeight
|
||||
}
|
||||
})
|
||||
}, [toolExecutionLogs])
|
||||
|
||||
// Clear execution log for specific tool
|
||||
const clearExecutionLog = (toolName: string) => {
|
||||
setToolExecutionLogs(prev => ({
|
||||
...prev,
|
||||
[toolName]: ''
|
||||
}))
|
||||
}
|
||||
|
||||
// Toggle tool expanded state
|
||||
const toggleTool = (toolName: string) => {
|
||||
setExpandedTools(prev => ({
|
||||
...prev,
|
||||
[toolName]: !prev[toolName]
|
||||
}))
|
||||
}
|
||||
|
||||
// Render form field based on schema
|
||||
const renderFormField = (toolName: string, fieldName: string, schema: any, isRequired: boolean) => {
|
||||
const value = toolForms[toolName]?.[fieldName] || ''
|
||||
|
||||
if (schema.type === 'number' || schema.type === 'integer') {
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
className="w-32 p-2 pr-6 border border-gray-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-300 placeholder-gray-300"
|
||||
value={value}
|
||||
step={schema.type === 'integer' ? 1 : 'any'}
|
||||
required={isRequired}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value === '' ? '' :
|
||||
(schema.type === 'integer' ? parseInt(e.target.value) || 0 : parseFloat(e.target.value) || 0)
|
||||
handleFormChange(toolName, fieldName, newValue)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else if (schema.type === 'boolean') {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-blue-600 rounded focus:ring-blue-300"
|
||||
checked={value}
|
||||
onChange={(e) => handleFormChange(toolName, fieldName, e.target.checked)}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
// String or other text input
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
className="w-full p-2 border border-gray-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-300 placeholder-gray-300"
|
||||
value={value}
|
||||
required={isRequired}
|
||||
placeholder={schema.description || ''}
|
||||
onChange={(e) => handleFormChange(toolName, fieldName, e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-lg bg-white p-4 border border-zinc-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold">MCP Servers</span>
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-500 text-xs mt-1 mb-3">
|
||||
Connect to Model Context Protocol (MCP) servers to access additional AI
|
||||
capabilities.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 p-2 border border-gray-200 rounded text-sm placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-300"
|
||||
placeholder="Enter MCP server URL"
|
||||
value={serverUrl}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value
|
||||
setServerUrl(newValue)
|
||||
sessionStorage.setItem('mcpServerUrl', newValue)
|
||||
}}
|
||||
disabled={isActive && state !== 'failed'}
|
||||
/>
|
||||
|
||||
{state === 'ready' ||
|
||||
(isActive && state !== 'not-connected' && state !== 'failed') ? (
|
||||
<button
|
||||
className="px-4 py-2 bg-orange-100 hover:bg-orange-200 text-orange-900 rounded text-sm font-medium whitespace-nowrap"
|
||||
onClick={handleDisconnect}
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white rounded py-2 px-4 text-sm font-medium disabled:opacity-50 whitespace-nowrap"
|
||||
onClick={handleConnect}
|
||||
disabled={isActive || !serverUrl.trim()}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authentication Link if needed */}
|
||||
{authUrl && (
|
||||
<div className="p-3 bg-orange-50 border border-orange-200 rounded">
|
||||
<p className="text-xs mb-2">
|
||||
Authentication required. Please click the link below:
|
||||
</p>
|
||||
<a
|
||||
href={authUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-orange-700 hover:text-orange-800 underline"
|
||||
onClick={handleManualAuth}
|
||||
>
|
||||
Authenticate in new window
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Available Tools section - always present */}
|
||||
<div>
|
||||
<h3 className="font-medium text-sm mb-3">
|
||||
Available Tools ({tools.length})
|
||||
</h3>
|
||||
|
||||
{tools.length === 0 ? (
|
||||
<div className="border border-gray-200 rounded p-4 bg-gray-50 text-center text-gray-500 text-sm">
|
||||
No tools available. Connect to an MCP server to see available tools.
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-gray-200 rounded p-4 bg-gray-50 space-y-2">
|
||||
{tools.map((tool: Tool, index: number) => {
|
||||
const isExpanded = expandedTools[tool.name] || false
|
||||
|
||||
return (
|
||||
<div key={index} className="bg-white rounded border border-gray-100 shadow-sm">
|
||||
{/* Tool header - always visible */}
|
||||
<div
|
||||
className="flex items-center gap-2 p-3 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||
onClick={() => toggleTool(tool.name)}
|
||||
>
|
||||
{/* Toggle arrow */}
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={16} className="text-gray-500 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight size={16} className="text-gray-500 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
{/* Tool name */}
|
||||
<h4 className="font-bold text-base text-black flex-shrink-0">
|
||||
{tool.name}
|
||||
</h4>
|
||||
|
||||
{/* Tool description when collapsed */}
|
||||
{!isExpanded && tool.description && (
|
||||
<p className="text-gray-600 text-sm truncate ml-2">
|
||||
{tool.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expanded content */}
|
||||
{isExpanded && (
|
||||
<div className="px-3 pb-3 border-t border-gray-100">
|
||||
{tool.description && (
|
||||
<p className="text-gray-600 mb-4 text-sm leading-relaxed mt-3">
|
||||
{tool.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Form for tool parameters */}
|
||||
{tool.inputSchema && tool.inputSchema.properties && (
|
||||
<div className="flex flex-wrap gap-3 mb-4">
|
||||
{Object.entries(tool.inputSchema.properties).map(([fieldName, schema]: [string, any]) => {
|
||||
const isRequired = Array.isArray(tool.inputSchema.required) && tool.inputSchema.required.includes(fieldName)
|
||||
const isTextInput = schema.type !== 'number' && schema.type !== 'integer' && schema.type !== 'boolean'
|
||||
return (
|
||||
<div key={fieldName} className={`space-y-1 ${isTextInput ? 'w-full' : ''}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs font-medium text-gray-700">
|
||||
{fieldName}
|
||||
</label>
|
||||
{schema.description && (
|
||||
<div className="relative group">
|
||||
<Info size={12} className="text-gray-400 cursor-help" />
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-10">
|
||||
{schema.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{renderFormField(tool.name, fieldName, schema, isRequired)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Run button */}
|
||||
<button
|
||||
onClick={() => handleRunTool(tool)}
|
||||
disabled={state !== 'ready'}
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded py-2 px-4 text-sm font-medium mb-4"
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
|
||||
{/* Per-tool execution log - only show if there's content */}
|
||||
{toolExecutionLogs[tool.name] && (
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h5 className="font-medium text-sm text-gray-700">Execution Log</h5>
|
||||
<button
|
||||
onClick={() => clearExecutionLog(tool.name)}
|
||||
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<X size={12} />
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
ref={(el) => { executionLogRefs.current[tool.name] = el }}
|
||||
value={toolExecutionLogs[tool.name]}
|
||||
readOnly
|
||||
className="w-full h-32 p-2 border border-gray-200 rounded text-[10px] font-mono bg-gray-50 resize-none placeholder-gray-300"
|
||||
placeholder="Tool execution results will appear here..."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Debug Log */}
|
||||
<div>
|
||||
<label className="font-medium text-xs block mb-2">Debug Log</label>
|
||||
<div
|
||||
ref={logRef}
|
||||
className="border border-gray-200 rounded p-2 bg-gray-50 h-32 overflow-y-auto font-mono text-xs"
|
||||
>
|
||||
{log.length > 0 ? (
|
||||
log.map((entry: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`py-0.5 ${
|
||||
entry.level === 'debug'
|
||||
? 'text-gray-500'
|
||||
: entry.level === 'info'
|
||||
? 'text-blue-600'
|
||||
: entry.level === 'warn'
|
||||
? 'text-orange-600'
|
||||
: 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
[{entry.level}] {entry.message}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-gray-400">No log entries yet</div>
|
||||
)}
|
||||
</div>
|
||||
{connectionData?.state !== 'not-connected' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
connectionData?.clearStorage()
|
||||
if (isActive) {
|
||||
handleDisconnect()
|
||||
}
|
||||
}}
|
||||
className="text-xs text-orange-600 hover:text-orange-800 hover:underline mt-2"
|
||||
>
|
||||
Clear stored authentication
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Only render the actual MCP connection when active */}
|
||||
{isActive && (
|
||||
<McpConnection
|
||||
serverUrl={serverUrl}
|
||||
onConnectionUpdate={setConnectionData}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
18
examples/inspector/src/components/OAuthCallback.tsx
Normal file
18
examples/inspector/src/components/OAuthCallback.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { useEffect } from 'react'
|
||||
import { onMcpAuthorization } from 'use-mcp'
|
||||
|
||||
export function OAuthCallback() {
|
||||
useEffect(() => {
|
||||
onMcpAuthorization()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Authenticating...</h1>
|
||||
<p className="text-gray-600 mb-2">Please wait while we complete your authentication.</p>
|
||||
<p className="text-sm text-gray-500">This window should close automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
examples/inspector/src/index.css
Normal file
3
examples/inspector/src/index.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@config "../tailwind.config.js";
|
||||
10
examples/inspector/src/main.tsx
Normal file
10
examples/inspector/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
119
examples/inspector/src/styles/github.css
Normal file
119
examples/inspector/src/styles/github.css
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
.light {
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
code.hljs {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
/*!
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/
|
||||
.hljs {
|
||||
color: #24292e;
|
||||
background: #ffffff;
|
||||
}
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable.language_ {
|
||||
/* prettylights-syntax-keyword */
|
||||
color: #d73a49;
|
||||
}
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-title.function_ {
|
||||
/* prettylights-syntax-entity */
|
||||
color: #6f42c1;
|
||||
}
|
||||
.hljs-attr,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-operator,
|
||||
.hljs-variable,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id {
|
||||
/* prettylights-syntax-constant */
|
||||
color: #005cc5;
|
||||
}
|
||||
.hljs-regexp,
|
||||
.hljs-string,
|
||||
.hljs-meta .hljs-string {
|
||||
/* prettylights-syntax-string */
|
||||
color: #032f62;
|
||||
}
|
||||
.hljs-built_in,
|
||||
.hljs-symbol {
|
||||
/* prettylights-syntax-variable */
|
||||
color: #e36209;
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-code,
|
||||
.hljs-formula {
|
||||
/* prettylights-syntax-comment */
|
||||
color: #6a737d;
|
||||
}
|
||||
.hljs-name,
|
||||
.hljs-quote,
|
||||
.hljs-selector-tag,
|
||||
.hljs-selector-pseudo {
|
||||
/* prettylights-syntax-entity-tag */
|
||||
color: #22863a;
|
||||
}
|
||||
.hljs-subst {
|
||||
/* prettylights-syntax-storage-modifier-import */
|
||||
color: #24292e;
|
||||
}
|
||||
.hljs-section {
|
||||
/* prettylights-syntax-markup-heading */
|
||||
color: #005cc5;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-bullet {
|
||||
/* prettylights-syntax-markup-list */
|
||||
color: #735c0f;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
/* prettylights-syntax-markup-italic */
|
||||
color: #24292e;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
/* prettylights-syntax-markup-bold */
|
||||
color: #24292e;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-addition {
|
||||
/* prettylights-syntax-markup-inserted */
|
||||
color: #22863a;
|
||||
background-color: #f0fff4;
|
||||
}
|
||||
.hljs-deletion {
|
||||
/* prettylights-syntax-markup-deleted */
|
||||
color: #b31d28;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
.hljs-char.escape_,
|
||||
.hljs-link,
|
||||
.hljs-params,
|
||||
.hljs-property,
|
||||
.hljs-punctuation,
|
||||
.hljs-tag {
|
||||
/* purposely ignored */
|
||||
}
|
||||
}
|
||||
37
examples/inspector/src/styles/scrollbar.css
Normal file
37
examples/inspector/src/styles/scrollbar.css
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* Light theme scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Dark theme scrollbar */
|
||||
.dark ::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: #27272a; /* zinc-800 */
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #52525b; /* zinc-600 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: #71717a; /* zinc-500 */
|
||||
}
|
||||
9
examples/inspector/src/vite-env.d.ts
vendored
Normal file
9
examples/inspector/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
apiKeyModalResolve?: (value: boolean) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
Loading…
Reference in a new issue