refactor(ui): mobile 375px responsiveness for pages and config sections

- Config page: vertical tabs → horizontal on mobile via useIsMobile()
- Agent/Channel/Team detail: scrollable tab overflow, responsive card padding
- Config sections: grid-cols-2/3/4 → responsive sm: breakpoints (18 files)
- Gateway/Quota sections: responsive grids, overflow-x-auto on data tables
- Shares/Links/Skills: stack forms on mobile, responsive fixed widths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
viettranxandClaude Opus 4.6 committed 2026-03-05 12:13:59 +07:00
1 parent 4120226f24
commit 07d8b4efa6
19 files changed
+143 -133

No files matched your search

@@ -109,9 +109,9 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) {
</div>
{/* Tabs */}
<div className="rounded-xl border bg-card p-4 shadow-sm">
<div className="rounded-xl border bg-card p-3 shadow-sm sm:p-4">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsList className="w-full justify-start overflow-x-auto">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="config">Config</TabsTrigger>
<TabsTrigger value="files">Files</TabsTrigger>
@@ -53,7 +53,7 @@ export function AgentSharesTab({ agentId }: AgentSharesTabProps) {
{/* Add share form */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 text-sm font-medium">Grant Access</h3>
<div className="flex items-end gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
<div className="flex-1 space-y-1.5">
<Label htmlFor="shareUserId">User ID</Label>
<Input
@@ -66,7 +66,7 @@ export function AgentSharesTab({ agentId }: AgentSharesTabProps) {
}}
/>
</div>
<div className="w-36 space-y-1.5">
<div className="w-full space-y-1.5 sm:w-36">
<Label>Role</Label>
<Select value={newRole} onValueChange={setNewRole}>
<SelectTrigger>
@@ -100,8 +100,8 @@ export function AgentSharesTab({ agentId }: AgentSharesTabProps) {
</p>
</div>
) : (
<div className="rounded-lg border">
<div className="grid grid-cols-[1fr_100px_48px] items-center gap-2 border-b bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
<div className="overflow-x-auto rounded-lg border">
<div className="grid min-w-[300px] grid-cols-[1fr_100px_48px] items-center gap-2 border-b bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
<span>User</span>
<span>Role</span>
<span />
@@ -109,7 +109,7 @@ export function AgentSharesTab({ agentId }: AgentSharesTabProps) {
{shares.map((share) => (
<div
key={share.user_id}
className="grid grid-cols-[1fr_100px_48px] items-center gap-2 border-b px-4 py-3 last:border-0"
className="grid min-w-[300px] grid-cols-[1fr_100px_48px] items-center gap-2 border-b px-4 py-3 last:border-0"
>
<div>
<span className="text-sm font-medium">{share.user_id}</span>
@@ -69,7 +69,7 @@ export function AgentSkillsTab({ agentId }: AgentSkillsTabProps) {
<p className="text-sm text-muted-foreground">
{skills.filter((s) => s.granted).length} of {skills.length} skills granted
</p>
<SearchInput value={search} onChange={setSearch} placeholder="Filter skills..." className="w-64" />
<SearchInput value={search} onChange={setSearch} placeholder="Filter skills..." className="w-full sm:w-64" />
</div>
<div className="divide-y rounded-lg border">
@@ -18,7 +18,7 @@ export function CompactionSection({ enabled, value, onToggle, onChange }: Compac
enabled={enabled}
onToggle={onToggle}
>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Minimum tokens reserved for the LLM response. Higher values give more room for output but reduce available context for history.">Reserve Tokens Floor</InfoLabel>
<Input
@@ -39,7 +39,7 @@ export function CompactionSection({ enabled, value, onToggle, onChange }: Compac
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Minimum number of messages in history before compaction can trigger, regardless of token usage.">Min Messages</InfoLabel>
<Input
@@ -27,7 +27,7 @@ export function ConfigSection({
<Switch checked={enabled} onCheckedChange={onToggle} />
</div>
{enabled ? (
<div className="rounded-lg border p-4 space-y-4">{children}</div>
<div className="rounded-lg border p-3 space-y-4 sm:p-4">{children}</div>
) : (
<p className="text-xs text-muted-foreground italic">
Using global defaults from config.json
@@ -25,7 +25,7 @@ export function ContextPruningSection({ enabled, value, onToggle, onChange }: Co
enabled={enabled}
onToggle={onToggle}
>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Pruning strategy. 'cache-ttl' trims old tool results based on their position in the conversation.">Mode</InfoLabel>
<Select
@@ -53,7 +53,7 @@ export function ContextPruningSection({ enabled, value, onToggle, onChange }: Co
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Context usage ratio (0-1) at which soft trimming begins. E.g. 0.3 means trimming starts when context is 30% full.">Soft Trim Ratio (0-1)</InfoLabel>
<Input
@@ -90,7 +90,7 @@ export function ContextPruningSection({ enabled, value, onToggle, onChange }: Co
{/* Soft Trim */}
<div className="space-y-3 rounded-md border border-dashed p-3">
<h4 className="text-xs font-medium text-muted-foreground">Soft Trim</h4>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="space-y-2">
<InfoLabel tip="Tool results longer than this will be soft-trimmed, keeping only head and tail portions.">Max Chars</InfoLabel>
<Input
@@ -38,7 +38,7 @@ export function MemorySection({ enabled, value, onToggle, onChange }: MemorySect
providerPlaceholder="(auto)"
modelPlaceholder="text-embedding-3-small"
/>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Maximum number of memory entries returned per search query.">Max Results</InfoLabel>
<Input
@@ -58,7 +58,7 @@ export function MemorySection({ enabled, value, onToggle, onChange }: MemorySect
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Weight for vector (semantic) similarity in hybrid search scoring. Higher values prioritize meaning over keywords.">Vector Weight</InfoLabel>
<Input
@@ -67,7 +67,7 @@ export function QualityGatesSection({ enabled, value, onToggle, onChange }: Qual
<Trash2 className="h-3.5 w-3.5 text-muted-foreground" />
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Validation method. 'Agent' delegates to a reviewer agent, 'Command' runs a shell command and checks exit code.">Type</InfoLabel>
<Select
@@ -107,7 +107,7 @@ export function QualityGatesSection({ enabled, value, onToggle, onChange }: Qual
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="flex items-center gap-2">
<Switch
checked={gate.block_on_failure}
@@ -127,7 +127,7 @@ export function QualityGatesSection({ enabled, value, onToggle, onChange }: Qual
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Maximum time in seconds to wait for the quality gate check to complete before timing out.">Timeout (seconds)</InfoLabel>
<Input
@@ -25,7 +25,7 @@ export function SandboxSection({ enabled, value, onToggle, onChange }: SandboxSe
enabled={enabled}
onToggle={onToggle}
>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="'off' disables sandboxing, 'non-main' sandboxes only sub-agents, 'all' sandboxes every execution including the main agent.">Mode</InfoLabel>
<Select
@@ -65,7 +65,7 @@ export function SandboxSection({ enabled, value, onToggle, onChange }: SandboxSe
onChange={(e) => onChange({ ...value, image: e.target.value || undefined })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Container lifecycle scope. 'session' = one container per chat session, 'agent' = shared across sessions, 'shared' = shared across all agents.">Scope</InfoLabel>
<Select
@@ -90,7 +90,7 @@ export function SandboxSection({ enabled, value, onToggle, onChange }: SandboxSe
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Maximum memory allocation for the sandbox container in megabytes.">Memory (MB)</InfoLabel>
<Input
@@ -37,7 +37,7 @@ export function SubagentsSection({ enabled, value, onToggle, onChange }: Subagen
enabled={enabled}
onToggle={onToggle}
>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Maximum number of sub-agents that can run simultaneously for this agent.">Max Concurrent</InfoLabel>
<Input
@@ -62,7 +62,7 @@ export function SubagentsSection({ enabled, value, onToggle, onChange }: Subagen
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Maximum number of sub-agents a single parent agent can spawn in one session.">Max Children Per Agent</InfoLabel>
<Input
@@ -92,7 +92,7 @@ export function IdentitySection({
Short description of this agent's expertise. Used for delegation discovery.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>Status</Label>
<Select value={status} onValueChange={onStatusChange}>
@@ -45,7 +45,7 @@ export function LlmConfigSection({
providerTip="LLM provider name. Must match a configured provider."
modelTip="Model ID to use."
/>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="contextWindow">Context Window</Label>
<Input
@@ -66,7 +66,7 @@ export function LinkCreateForm({ agentOptions, onSubmit }: LinkCreateFormProps)
<div className="rounded-lg border p-4">
<h3 className="mb-3 text-sm font-medium">Create Link</h3>
<div className="space-y-3">
<div className="flex items-end gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
<div className="flex-1 space-y-1.5">
<Label>Target Agent</Label>
<Combobox
@@ -76,7 +76,7 @@ export function LinkCreateForm({ agentOptions, onSubmit }: LinkCreateFormProps)
placeholder="Select or search agent..."
/>
</div>
<div className="w-44 space-y-1.5">
<div className="w-full space-y-1.5 sm:w-44">
<Label>Direction</Label>
<Select value={direction} onValueChange={setDirection}>
<SelectTrigger>
@@ -97,7 +97,7 @@ export function LinkCreateForm({ agentOptions, onSubmit }: LinkCreateFormProps)
<p className="text-xs text-muted-foreground">
{DIRECTION_OPTIONS.find((o) => o.value === direction)?.desc}
</p>
<div className="flex items-end gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
<div className="flex-1 space-y-1.5">
<Label>Description</Label>
<Input
@@ -106,7 +106,7 @@ export function LinkCreateForm({ agentOptions, onSubmit }: LinkCreateFormProps)
placeholder="Optional description of the link..."
/>
</div>
<div className="w-32 space-y-1.5">
<div className="w-full space-y-1.5 sm:w-32">
<Label>Max Concurrent</Label>
<Input
type="number"
@@ -44,8 +44,8 @@ export function LinkList({
}
return (
<div className="rounded-lg border">
<div className="grid grid-cols-[1fr_100px_60px_60px_80px] items-center gap-2 border-b bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
<div className="overflow-x-auto rounded-lg border">
<div className="grid min-w-[500px] grid-cols-[1fr_100px_60px_60px_80px] items-center gap-2 border-b bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
<span>Target</span>
<span>Direction</span>
<span>Status</span>
@@ -55,7 +55,7 @@ export function LinkList({
{links.map((link) => (
<div
key={link.id}
className="grid grid-cols-[1fr_100px_60px_60px_80px] items-center gap-2 border-b px-4 py-3 last:border-0"
className="grid min-w-[500px] grid-cols-[1fr_100px_60px_60px_80px] items-center gap-2 border-b px-4 py-3 last:border-0"
>
<div className="min-w-0">
<div className="flex items-center gap-1.5">
@@ -93,9 +93,9 @@ export function ChannelDetailPage({ instanceId, onBack }: ChannelDetailPageProps
</div>
{/* Tabs */}
<div className="rounded-xl border bg-card p-4 shadow-sm">
<div className="rounded-xl border bg-card p-3 shadow-sm sm:p-4">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsList className="w-full justify-start overflow-x-auto">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="credentials">Credentials</TabsTrigger>
<TabsTrigger value="config">Config</TabsTrigger>
+12 -4
View File
@@ -12,6 +12,7 @@ import { DetailSkeleton } from "@/components/shared/loading-skeleton";
import { useConfig } from "./hooks/use-config";
import { useMinLoading } from "@/hooks/use-min-loading";
import { useDeferredLoading } from "@/hooks/use-deferred-loading";
import { useIsMobile } from "@/hooks/use-media-query";
import { ROUTES } from "@/lib/constants";
import { GatewaySection } from "./sections/gateway-section";
import { ProvidersSection } from "./sections/providers-section";
@@ -27,6 +28,7 @@ import { QuotaSection } from "./sections/quota-section";
export function ConfigPage() {
const { config, hash, configPath, loading, saving, error, refresh, applyRaw, patch } = useConfig();
const isMobile = useIsMobile();
const spinning = useMinLoading(loading);
const showSkeleton = useDeferredLoading(loading && !config);
const [rawText, setRawText] = useState("");
@@ -117,16 +119,22 @@ export function ConfigPage() {
</span>
</div>
<Tabs orientation="vertical" defaultValue="general" className="mt-4 items-start">
<TabsList variant="line" className="w-44 shrink-0 sticky top-6 rounded-lg border bg-card p-3 shadow-sm">
<Tabs orientation={isMobile ? "horizontal" : "vertical"} defaultValue="general" className="mt-4 items-start">
<TabsList
variant={isMobile ? "default" : "line"}
className={isMobile
? "w-full overflow-x-auto"
: "w-44 shrink-0 sticky top-6 rounded-lg border bg-card p-3 shadow-sm"
}
>
<TabsTrigger value="general">General</TabsTrigger>
{isManaged && <TabsTrigger value="quota">Quota</TabsTrigger>}
<TabsTrigger value="agents">Agents</TabsTrigger>
<TabsTrigger value="tools">Tools</TabsTrigger>
<TabsTrigger value="connections">Connections</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
<div className="my-2 h-px w-full bg-border" />
<TabsTrigger value="raw">Raw Editor</TabsTrigger>
{!isMobile && <div className="my-2 h-px w-full bg-border" />}
<TabsTrigger value="raw">Raw</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-4">
@@ -63,7 +63,7 @@ export function GatewaySection({ data, onSave, saving }: Props) {
<CardDescription>WebSocket & HTTP server settings, security</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="grid gap-1.5">
<InfoLabel tip="IP address to bind the server. Use 0.0.0.0 to accept connections from any interface.">Host</InfoLabel>
<Input
@@ -96,7 +96,7 @@ export function GatewaySection({ data, onSave, saving }: Props) {
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="grid gap-1.5">
<InfoLabel tip="Sender IDs with admin privileges. These users bypass rate limits and can manage configuration.">Owner IDs</InfoLabel>
<TagInput
@@ -115,7 +115,7 @@ export function GatewaySection({ data, onSave, saving }: Props) {
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div className="grid gap-1.5">
<InfoLabel tip="Maximum characters per inbound message. Messages exceeding this are rejected.">Max Message Chars</InfoLabel>
<Input
@@ -134,99 +134,101 @@ function OverridesTable({
<div className="space-y-2">
<InfoLabel tip={tip}>{label}</InfoLabel>
{keys.map((key, i) => (
<div key={i} className="flex items-end gap-2">
<div className="grid gap-1.5 min-w-[180px]">
{i === 0 && (
<span className="text-xs text-muted-foreground">Key</span>
)}
{options ? (
<Select
value={key}
onValueChange={(v) => updateKey(key, v)}
>
<SelectTrigger>
<SelectValue placeholder={keyPlaceholder} />
</SelectTrigger>
<SelectContent>
{/* Current value always shown */}
{key && (
<SelectItem value={key}>
{options.find((o) => o.value === key)?.label ?? key}
</SelectItem>
)}
{availableOptions
?.filter((o) => o.value !== key)
.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
<div key={i} className="overflow-x-auto">
<div className="flex items-end gap-2 min-w-[420px]">
<div className="grid gap-1.5 min-w-[180px]">
{i === 0 && (
<span className="text-xs text-muted-foreground">Key</span>
)}
{options ? (
<Select
value={key}
onValueChange={(v) => updateKey(key, v)}
>
<SelectTrigger>
<SelectValue placeholder={keyPlaceholder} />
</SelectTrigger>
<SelectContent>
{/* Current value always shown */}
{key && (
<SelectItem value={key}>
{options.find((o) => o.value === key)?.label ?? key}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
)}
{availableOptions
?.filter((o) => o.value !== key)
.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
placeholder={keyPlaceholder}
value={key}
onChange={(e) => updateKey(key, e.target.value)}
/>
)}
</div>
<div className="grid gap-1.5 w-20">
{i === 0 && (
<span className="text-xs text-muted-foreground">Hour</span>
)}
<Input
placeholder={keyPlaceholder}
value={key}
onChange={(e) => updateKey(key, e.target.value)}
type="number"
min={0}
value={entries[key]?.hour ?? 0}
onChange={(e) =>
updateWindow(key, {
...entries[key],
hour: Number(e.target.value),
})
}
/>
)}
</div>
<div className="grid gap-1.5 w-20">
{i === 0 && (
<span className="text-xs text-muted-foreground">Day</span>
)}
<Input
type="number"
min={0}
value={entries[key]?.day ?? 0}
onChange={(e) =>
updateWindow(key, {
...entries[key],
day: Number(e.target.value),
})
}
/>
</div>
<div className="grid gap-1.5 w-20">
{i === 0 && (
<span className="text-xs text-muted-foreground">Week</span>
)}
<Input
type="number"
min={0}
value={entries[key]?.week ?? 0}
onChange={(e) =>
updateWindow(key, {
...entries[key],
week: Number(e.target.value),
})
}
/>
</div>
<Button
variant="ghost"
size="icon"
className="shrink-0"
onClick={() => removeRow(key)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="grid gap-1.5 w-20">
{i === 0 && (
<span className="text-xs text-muted-foreground">Hour</span>
)}
<Input
type="number"
min={0}
value={entries[key]?.hour ?? 0}
onChange={(e) =>
updateWindow(key, {
...entries[key],
hour: Number(e.target.value),
})
}
/>
</div>
<div className="grid gap-1.5 w-20">
{i === 0 && (
<span className="text-xs text-muted-foreground">Day</span>
)}
<Input
type="number"
min={0}
value={entries[key]?.day ?? 0}
onChange={(e) =>
updateWindow(key, {
...entries[key],
day: Number(e.target.value),
})
}
/>
</div>
<div className="grid gap-1.5 w-20">
{i === 0 && (
<span className="text-xs text-muted-foreground">Week</span>
)}
<Input
type="number"
min={0}
value={entries[key]?.week ?? 0}
onChange={(e) =>
updateWindow(key, {
...entries[key],
week: Number(e.target.value),
})
}
/>
</div>
<Button
variant="ghost"
size="icon"
className="shrink-0"
onClick={() => removeRow(key)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
{options ? (
+2 -2
View File
@@ -105,9 +105,9 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) {
</div>
{/* Tabs */}
<div className="rounded-xl border bg-card p-4 shadow-sm">
<div className="rounded-xl border bg-card p-3 shadow-sm sm:p-4">
<Tabs defaultValue="members">
<TabsList>
<TabsList className="w-full justify-start overflow-x-auto">
<TabsTrigger value="members">Members</TabsTrigger>
<TabsTrigger value="tasks">Tasks</TabsTrigger>
<TabsTrigger value="delegations">Delegations</TabsTrigger>