add mobile support

This commit is contained in:
Fredrik Jensen 2025-12-07 11:36:17 +01:00
parent 616410aa36
commit f6c8b2a299
8 changed files with 261 additions and 8 deletions

View File

@ -12,6 +12,26 @@
padding: 12px 20px; padding: 12px 20px;
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
gap: 12px;
}
.mobile-menu-toggle {
display: none;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
cursor: pointer;
flex-shrink: 0;
}
.mobile-menu-toggle:hover {
background: var(--bg-tertiary);
} }
.header-brand { .header-brand {
@ -29,11 +49,13 @@
.header-brand h1 { .header-brand h1 {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
white-space: nowrap;
} }
.header-status { .header-status {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: auto;
} }
.status-badge { .status-badge {
@ -61,6 +83,7 @@
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: currentColor; background: currentColor;
flex-shrink: 0;
} }
.status-badge.connected .status-dot { .status-badge.connected .status-dot {
@ -82,6 +105,11 @@
display: flex; display: flex;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
position: relative;
}
.sidebar-overlay {
display: none;
} }
.content { .content {
@ -97,9 +125,47 @@
overflow: hidden; overflow: hidden;
} }
/* Responsive */ /* Responsive - Tablet */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.content-panels { .content-panels {
flex-direction: column; flex-direction: column;
} }
} }
/* Responsive - Mobile */
@media (max-width: 768px) {
.header {
padding: 10px 16px;
}
.mobile-menu-toggle {
display: flex;
}
.header-brand h1 {
font-size: 16px;
}
.status-text {
display: none;
}
.status-badge {
padding: 6px;
}
.sidebar-overlay {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
}
.content-panels {
flex-direction: column;
}
}

View File

@ -1,4 +1,5 @@
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { Menu, X } from 'lucide-react'
import { Sidebar } from './components/Sidebar' import { Sidebar } from './components/Sidebar'
import { ToolsPanel } from './components/ToolsPanel' import { ToolsPanel } from './components/ToolsPanel'
import { ResultsPane } from './components/ResultsPane' import { ResultsPane } from './components/ResultsPane'
@ -50,6 +51,7 @@ function App() {
const [toolResult, setToolResult] = useState<ToolResult | null>(null) const [toolResult, setToolResult] = useState<ToolResult | null>(null)
const [isExecuting, setIsExecuting] = useState(false) const [isExecuting, setIsExecuting] = useState(false)
const [lastExecution, setLastExecution] = useState<{ toolName: string; params: Record<string, unknown> } | null>(null) const [lastExecution, setLastExecution] = useState<{ toolName: string; params: Record<string, unknown> } | null>(null)
const [sidebarOpen, setSidebarOpen] = useState(false)
const handleConnect = useCallback(async () => { const handleConnect = useCallback(async () => {
if (isConnected) { if (isConnected) {
@ -93,6 +95,13 @@ function App() {
return ( return (
<div className="app"> <div className="app">
<header className="header"> <header className="header">
<button
className="mobile-menu-toggle"
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label={sidebarOpen ? 'Close menu' : 'Open menu'}
>
{sidebarOpen ? <X size={20} /> : <Menu size={20} />}
</button>
<div className="header-brand" onClick={() => window.location.reload()}> <div className="header-brand" onClick={() => window.location.reload()}>
<h1>MCP UI Inspector</h1> <h1>MCP UI Inspector</h1>
</div> </div>
@ -100,18 +109,24 @@ function App() {
{isConnected ? ( {isConnected ? (
<span className="status-badge connected"> <span className="status-badge connected">
<span className="status-dot"></span> <span className="status-dot"></span>
Connected <span className="status-text">Connected</span>
</span> </span>
) : ( ) : (
<span className="status-badge disconnected"> <span className="status-badge disconnected">
<span className="status-dot"></span> <span className="status-dot"></span>
Disconnected <span className="status-text">Disconnected</span>
</span> </span>
)} )}
</div> </div>
</header> </header>
<div className="main-layout"> <div className="main-layout">
{sidebarOpen && (
<div
className="sidebar-overlay"
onClick={() => setSidebarOpen(false)}
/>
)}
<Sidebar <Sidebar
serverUrl={serverUrl} serverUrl={serverUrl}
onServerUrlChange={setServerUrl} onServerUrlChange={setServerUrl}
@ -121,6 +136,8 @@ function App() {
isStateless={isStateless} isStateless={isStateless}
onConnect={handleConnect} onConnect={handleConnect}
error={error} error={error}
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/> />
<main className="content"> <main className="content">

View File

@ -59,3 +59,17 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
/* Mobile adjustments */
@media (max-width: 768px) {
.btn {
padding: 10px 14px;
font-size: 14px;
min-height: 44px;
}
.btn-ghost {
min-height: 36px;
padding: 6px;
}
}

View File

@ -2,7 +2,7 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 400px; min-width: 300px;
background: var(--bg-primary); background: var(--bg-primary);
} }
@ -193,3 +193,53 @@
min-width: 0; min-width: 0;
} }
} }
/* Mobile styles */
@media (max-width: 768px) {
.results-pane {
min-width: 0;
min-height: 300px;
}
.results-pane .panel-header {
padding: 0 12px;
flex-wrap: wrap;
gap: 8px;
}
.results-tabs {
flex-wrap: nowrap;
}
.tab {
padding: 10px 12px;
font-size: 12px;
}
.results-actions {
gap: 8px;
}
.timestamp {
display: none;
}
.results-loading,
.results-empty,
.results-error {
padding: 24px 16px;
}
.results-error pre {
font-size: 11px;
padding: 10px 12px;
}
.text-output {
padding: 12px;
}
.text-output pre {
font-size: 12px;
}
}

View File

@ -8,6 +8,25 @@
overflow-y: auto; overflow-y: auto;
} }
/* Mobile sidebar */
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 100;
transform: translateX(-100%);
transition: transform 0.3s ease;
width: 85%;
max-width: 320px;
}
.sidebar.open {
transform: translateX(0);
}
}
.sidebar-section { .sidebar-section {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }

View File

@ -11,6 +11,8 @@ interface SidebarProps {
isStateless: boolean isStateless: boolean
onConnect: () => void onConnect: () => void
error: string | null error: string | null
isOpen?: boolean
onClose?: () => void
} }
export function Sidebar({ export function Sidebar({
@ -21,10 +23,18 @@ export function Sidebar({
sessionId, sessionId,
isStateless, isStateless,
onConnect, onConnect,
error error,
isOpen,
onClose
}: SidebarProps) { }: SidebarProps) {
const handleConnect = () => {
onConnect()
// Close sidebar on mobile after connecting
if (onClose) onClose()
}
return ( return (
<aside className="sidebar"> <aside className={`sidebar ${isOpen ? 'open' : ''}`}>
<div className="sidebar-section"> <div className="sidebar-section">
<div className="sidebar-section-header"> <div className="sidebar-section-header">
<Server size={16} /> <Server size={16} />
@ -46,7 +56,7 @@ export function Sidebar({
<Button <Button
variant={isConnected ? 'default' : 'primary'} variant={isConnected ? 'default' : 'primary'}
onClick={onConnect} onClick={handleConnect}
loading={isConnecting} loading={isConnecting}
loadingText="Connecting..." loadingText="Connecting..."
icon={isConnected ? <PlugZap size={16} /> : <Plug size={16} />} icon={isConnected ? <PlugZap size={16} /> : <Plug size={16} />}

View File

@ -3,7 +3,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
min-width: 400px; min-width: 300px;
max-width: 50%; max-width: 50%;
} }
@ -285,3 +285,55 @@
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
} }
/* Mobile styles */
@media (max-width: 768px) {
.tools-panel {
min-width: 0;
max-width: none;
}
.tools-layout {
flex-direction: column;
}
.tools-list {
width: 100%;
min-width: 0;
max-height: 200px;
border-right: none;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.tool-item {
padding: 12px 16px;
}
.tool-detail {
min-height: 300px;
}
.tool-detail-header {
padding: 12px 16px;
}
.tool-params {
padding: 12px 16px;
}
.tool-actions {
padding: 12px 16px;
}
.param-field input,
.param-field select {
font-size: 16px; /* Prevents zoom on iOS */
padding: 10px 12px;
}
.json-editor {
font-size: 14px;
min-height: 120px;
}
}

View File

@ -85,3 +85,28 @@ code,
background: var(--accent-blue); background: var(--accent-blue);
color: white; color: white;
} }
/* Touch-friendly improvements */
@media (max-width: 768px) {
button,
input,
select,
textarea {
min-height: 44px; /* Minimum touch target size */
}
input,
select,
textarea {
font-size: 16px; /* Prevents zoom on iOS */
}
}
/* Safe area insets for notched devices */
@supports (padding: env(safe-area-inset-bottom)) {
body {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
}