feat: mobile tool workshop with list-detail pattern

- Add mobile viewport detection to ToolWorkshopPage
- Implement mobile list view with MobileListView component
- Implement mobile detail view with MobileDetailView component
- Implement mobile edit view with MobileEditView component
- Add MobileFAB for creating new tool types
- Fix IconName type issues in mobile components
- TypeScript check passes, build succeeds
This commit is contained in:
Alex Blank
2026-05-25 12:18:24 +02:00
parent 437ad840ef
commit e8d5b16acc
16 changed files with 1356 additions and 3853 deletions
+262
View File
@@ -1,5 +1,10 @@
import { useCallback, useEffect, useState } from "react";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { MobileListView } from "../components/mobile-list-view";
import { MobileDetailView } from "../components/mobile-detail-view";
import { MobileEditView } from "../components/mobile-edit-view";
import { MobileFAB } from "../components/mobile-fab";
import {
createToolType,
deleteToolType,
@@ -31,7 +36,11 @@ import {
type RightPanelTab = "details" | "configs" | "folders";
type Status = "loading" | "ready" | "error";
type MobileView = "list" | "detail" | "edit";
export const ToolWorkshopPage = () => {
const isMobile = useMobileViewport();
const [mobileView, setMobileView] = useState<MobileView>("list");
const [status, setStatus] = useState<Status>("loading");
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [configs, setConfigs] = useState<ToolConfig[]>([]);
@@ -507,6 +516,259 @@ export const ToolWorkshopPage = () => {
);
}
if (isMobile) {
if (mobileView === "list") {
return (
<div className="mobile-page">
<div className="mobile-page-header">
<h1>Tool Workshop</h1>
<span className="muted">{toolTypes.length} tool types</span>
</div>
<MobileListView
items={toolTypes.map((t) => ({
id: t.id,
title: t.display_name,
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
}))}
onItemClick={(id) => {
setSelectedToolTypeId(id);
setIsCreating(false);
setMobileView("detail");
}}
emptyMessage="No tool types yet"
/>
<MobileFAB onClick={() => {
setSelectedToolTypeId(null);
setIsCreating(true);
resetToolTypeForm();
setMobileView("edit");
}} />
</div>
);
}
if (mobileView === "detail" && selectedToolType) {
return (
<MobileDetailView
title={selectedToolType.display_name}
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type} · ${selectedToolType.interface_type === "web" ? `Port ${selectedToolType.default_port}` : "Terminal"}`}
fields={[
{ label: "Name", value: selectedToolType.name },
{ label: "Display Name", value: selectedToolType.display_name },
{ label: "Description", value: selectedToolType.description },
{ label: "Category", value: selectedToolType.category },
{ label: "Interface Type", value: selectedToolType.interface_type },
{ label: "Requires Port", value: selectedToolType.requires_port, type: "boolean" },
{ label: "Default Port", value: selectedToolType.default_port },
{ label: "Definition Type", value: selectedToolType.definition_type },
{ label: "Startup Command", value: selectedToolType.startup_command },
{ label: "Readiness Command", value: selectedToolType.readiness_probe?.command ?? null },
{ label: "Readiness Timeout", value: selectedToolType.readiness_probe?.timeout ?? null },
{ label: "Readiness Interval", value: selectedToolType.readiness_probe?.interval ?? null },
{ label: "Required Variables", value: selectedToolType.required_variables?.join(", ") ?? null },
{ label: "Compose Template", value: selectedToolType.compose_template, type: "code" },
{ label: "Dockerfile Template", value: selectedToolType.dockerfile_template, type: "code" },
]}
onEdit={() => {
populateToolTypeForm(selectedToolType);
setIsCreating(false);
setMobileView("edit");
}}
onDelete={() => {
handleDeleteToolType(selectedToolType.id);
setMobileView("list");
}}
onBack={() => {
setSelectedToolTypeId(null);
setMobileView("list");
}}
/>
);
}
if (mobileView === "edit") {
return (
<MobileEditView
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
onCancel={() => {
if (toolTypeDirty) {
if (!window.confirm("You have unsaved changes. Discard them?")) {
return;
}
}
setMobileView(isCreating ? "list" : "detail");
}}
onSave={() => {
// Create a synthetic form event to call handleToolTypeSubmit
const syntheticEvent = { preventDefault: () => {} } as React.FormEvent;
void handleToolTypeSubmit(syntheticEvent);
if (!toolTypeError) {
setMobileView("list");
}
}}
isSaving={false}
>
{/* Tool Type Form Fields */}
<div className="mobile-form-group">
<label className="mobile-form-label">Name *</label>
<input
type="text"
value={toolTypeForm.name}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, name: e.target.value })}
className="mobile-form-input"
placeholder="e.g., my-tool"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Display Name *</label>
<input
type="text"
value={toolTypeForm.display_name}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, display_name: e.target.value })}
className="mobile-form-input"
placeholder="e.g., My Tool"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Description</label>
<textarea
value={toolTypeForm.description}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, description: e.target.value })}
className="mobile-form-textarea"
placeholder="What does this tool do?"
rows={3}
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Category</label>
<input
type="text"
value={toolTypeForm.category}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, category: e.target.value })}
className="mobile-form-input"
placeholder="e.g., development"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Interface Type</label>
<select
value={toolTypeForm.interface_type}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, interface_type: e.target.value as "web" | "terminal" })}
className="mobile-form-select"
>
<option value="web">Web</option>
<option value="terminal">Terminal</option>
</select>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Requires Port</label>
<input
type="checkbox"
checked={toolTypeForm.requires_port}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, requires_port: e.target.checked })}
className="mobile-form-checkbox"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Default Port</label>
<input
type="text"
value={toolTypeForm.default_port}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, default_port: e.target.value })}
className="mobile-form-input"
placeholder="e.g., 8080"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Definition Type</label>
<select
value={toolTypeForm.definition_type}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, definition_type: e.target.value as "compose" | "dockerfile" })}
className="mobile-form-select"
>
<option value="compose">Compose</option>
<option value="dockerfile">Dockerfile</option>
</select>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Startup Command</label>
<input
type="text"
value={toolTypeForm.startup_command}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, startup_command: e.target.value })}
className="mobile-form-input"
placeholder="Command to run on startup"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Readiness Command</label>
<input
type="text"
value={toolTypeForm.readiness_command}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, readiness_command: e.target.value })}
className="mobile-form-input"
placeholder="e.g., curl -f http://localhost:8080/health"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Readiness Timeout</label>
<input
type="text"
value={toolTypeForm.readiness_timeout}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, readiness_timeout: e.target.value })}
className="mobile-form-input"
placeholder="30"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Readiness Interval</label>
<input
type="text"
value={toolTypeForm.readiness_interval}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, readiness_interval: e.target.value })}
className="mobile-form-input"
placeholder="2"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label">Required Variables</label>
<input
type="text"
value={toolTypeForm.required_variables}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, required_variables: e.target.value })}
className="mobile-form-input"
placeholder="VAR1, VAR2, VAR3"
/>
</div>
{toolTypeForm.definition_type === "compose" && (
<div className="mobile-form-group">
<label className="mobile-form-label">Compose Template</label>
<textarea
value={toolTypeForm.compose_template}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, compose_template: e.target.value })}
className="mobile-form-textarea mobile-form-code"
placeholder="version: '3'"
rows={10}
/>
</div>
)}
{toolTypeForm.definition_type === "dockerfile" && (
<div className="mobile-form-group">
<label className="mobile-form-label">Dockerfile Template</label>
<textarea
value={toolTypeForm.dockerfile_template}
onChange={(e) => setToolTypeForm({ ...toolTypeForm, dockerfile_template: e.target.value })}
className="mobile-form-textarea mobile-form-code"
placeholder="FROM ubuntu:22.04"
rows={10}
/>
</div>
)}
</MobileEditView>
);
}
}
return (
<div className="container" style={{ display: "flex", height: "calc(100vh - 4rem)", gap: 0, padding: 0 }}>
{/* Left Sidebar - Tool List */}