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;
background: var(--bg-secondary);
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 {
@ -29,11 +49,13 @@
.header-brand h1 {
font-size: 18px;
font-weight: 600;
white-space: nowrap;
}
.header-status {
display: flex;
align-items: center;
margin-left: auto;
}
.status-badge {
@ -61,6 +83,7 @@
height: 8px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
.status-badge.connected .status-dot {
@ -82,6 +105,11 @@
display: flex;
flex: 1;
overflow: hidden;
position: relative;
}
.sidebar-overlay {
display: none;
}
.content {
@ -97,9 +125,47 @@
overflow: hidden;
}
/* Responsive */
/* Responsive - Tablet */
@media (max-width: 1200px) {
.content-panels {
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 { Menu, X } from 'lucide-react'
import { Sidebar } from './components/Sidebar'
import { ToolsPanel } from './components/ToolsPanel'
import { ResultsPane } from './components/ResultsPane'
@ -50,6 +51,7 @@ function App() {
const [toolResult, setToolResult] = useState<ToolResult | null>(null)
const [isExecuting, setIsExecuting] = useState(false)
const [lastExecution, setLastExecution] = useState<{ toolName: string; params: Record<string, unknown> } | null>(null)
const [sidebarOpen, setSidebarOpen] = useState(false)
const handleConnect = useCallback(async () => {
if (isConnected) {
@ -93,6 +95,13 @@ function App() {
return (
<div className="app">
<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()}>
<h1>MCP UI Inspector</h1>
</div>
@ -100,18 +109,24 @@ function App() {
{isConnected ? (
<span className="status-badge connected">
<span className="status-dot"></span>
Connected
<span className="status-text">Connected</span>
</span>
) : (
<span className="status-badge disconnected">
<span className="status-dot"></span>
Disconnected
<span className="status-text">Disconnected</span>
</span>
)}
</div>
</header>
<div className="main-layout">
{sidebarOpen && (
<div
className="sidebar-overlay"
onClick={() => setSidebarOpen(false)}
/>
)}
<Sidebar
serverUrl={serverUrl}
onServerUrlChange={setServerUrl}
@ -121,6 +136,8 @@ function App() {
isStateless={isStateless}
onConnect={handleConnect}
error={error}
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/>
<main className="content">

View File

@ -59,3 +59,17 @@
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;
display: flex;
flex-direction: column;
min-width: 400px;
min-width: 300px;
background: var(--bg-primary);
}
@ -193,3 +193,53 @@
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;
}
/* 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 {
border-bottom: 1px solid var(--border-color);
}

View File

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

View File

@ -3,7 +3,7 @@
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
min-width: 400px;
min-width: 300px;
max-width: 50%;
}
@ -285,3 +285,55 @@
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);
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);
}
}