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:
@@ -0,0 +1,95 @@
|
||||
import { Icon } from "./icon";
|
||||
|
||||
interface Field {
|
||||
label: string;
|
||||
value: string | number | boolean | null;
|
||||
type?: "text" | "code" | "json" | "boolean";
|
||||
}
|
||||
|
||||
interface MobileDetailViewProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
fields: Field[];
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
fields,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onBack,
|
||||
}) => {
|
||||
const renderValue = (field: Field) => {
|
||||
if (field.value === null || field.value === undefined) {
|
||||
return <span className="text-muted">Not set</span>;
|
||||
}
|
||||
|
||||
if (field.type === "boolean") {
|
||||
return field.value ? (
|
||||
<span className="badge badge-success">Yes</span>
|
||||
) : (
|
||||
<span className="badge badge-secondary">No</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "code" || field.type === "json") {
|
||||
return (
|
||||
<pre className="mobile-detail-code">
|
||||
{typeof field.value === "string" ? field.value : JSON.stringify(field.value, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{String(field.value)}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mobile-detail-view">
|
||||
<header className="mobile-detail-header">
|
||||
<button
|
||||
className="mobile-detail-back"
|
||||
onClick={onBack}
|
||||
type="button"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<Icon name="arrow-left" size="md" />
|
||||
</button>
|
||||
<div className="mobile-detail-header-content">
|
||||
<h1 className="mobile-detail-title">{title}</h1>
|
||||
{subtitle && <p className="mobile-detail-subtitle">{subtitle}</p>}
|
||||
</div>
|
||||
<div className="mobile-detail-actions">
|
||||
<button
|
||||
className="mobile-detail-action"
|
||||
onClick={onEdit}
|
||||
type="button"
|
||||
aria-label="Edit"
|
||||
>
|
||||
<Icon name="edit" size="sm" />
|
||||
</button>
|
||||
<button
|
||||
className="mobile-detail-action mobile-detail-action-danger"
|
||||
onClick={onDelete}
|
||||
type="button"
|
||||
aria-label="Delete"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mobile-detail-fields">
|
||||
{fields.map((field, index) => (
|
||||
<div key={index} className="mobile-detail-field">
|
||||
<label className="mobile-detail-field-label">{field.label}</label>
|
||||
<div className="mobile-detail-field-value">{renderValue(field)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useState } from "react";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
interface FormField {
|
||||
name: string;
|
||||
label: string;
|
||||
type: "text" | "textarea" | "number" | "select" | "checkbox" | "code";
|
||||
value: string | number | boolean;
|
||||
options?: { value: string; label: string }[];
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
interface MobileEditViewProps {
|
||||
title: string;
|
||||
fields?: FormField[];
|
||||
onSave: (data: Record<string, string | number | boolean>) => void;
|
||||
onCancel: () => void;
|
||||
isSaving?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const MobileEditView: React.FC<MobileEditViewProps> = ({
|
||||
title,
|
||||
fields,
|
||||
onSave,
|
||||
onCancel,
|
||||
isSaving = false,
|
||||
children,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<Record<string, string | number | boolean>>(
|
||||
() => {
|
||||
const initial: Record<string, string | number | boolean> = {};
|
||||
fields?.forEach((field) => {
|
||||
initial[field.name] = field.value;
|
||||
});
|
||||
return initial;
|
||||
}
|
||||
);
|
||||
|
||||
const handleChange = (name: string, value: string | number | boolean) => {
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mobile-edit-view">
|
||||
<header className="mobile-edit-header">
|
||||
<button
|
||||
className="mobile-edit-cancel"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<h1 className="mobile-edit-title">{title}</h1>
|
||||
<button
|
||||
className="mobile-edit-save"
|
||||
onClick={() => onSave(formData)}
|
||||
type="button"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<form className="mobile-edit-form" onSubmit={handleSubmit}>
|
||||
{children || fields?.map((field) => (
|
||||
<div key={field.name} className="mobile-edit-field">
|
||||
<label className="mobile-edit-field-label" htmlFor={field.name}>
|
||||
{field.label}
|
||||
{field.required && <span className="required">*</span>}
|
||||
</label>
|
||||
|
||||
{field.type === "textarea" && (
|
||||
<textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={String(formData[field.name] ?? "")}
|
||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
rows={field.rows || 4}
|
||||
className="mobile-edit-input mobile-edit-textarea"
|
||||
/>
|
||||
)}
|
||||
|
||||
{field.type === "select" && (
|
||||
<select
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={String(formData[field.name] ?? "")}
|
||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||
required={field.required}
|
||||
className="mobile-edit-input"
|
||||
>
|
||||
{field.options?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
|
||||
{field.type === "checkbox" && (
|
||||
<label className="mobile-edit-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
checked={Boolean(formData[field.name])}
|
||||
onChange={(e) => handleChange(field.name, e.target.checked)}
|
||||
/>
|
||||
<span>{field.label}</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{field.type === "code" && (
|
||||
<textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={String(formData[field.name] ?? "")}
|
||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
rows={field.rows || 8}
|
||||
className="mobile-edit-input mobile-edit-code"
|
||||
style={{ fontFamily: "monospace" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(field.type === "text" || field.type === "number") && (
|
||||
<input
|
||||
type={field.type === "number" ? "number" : "text"}
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={String(formData[field.name] ?? "")}
|
||||
onChange={(e) =>
|
||||
handleChange(
|
||||
field.name,
|
||||
field.type === "number"
|
||||
? Number(e.target.value)
|
||||
: e.target.value
|
||||
)
|
||||
}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
className="mobile-edit-input"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Icon } from "./icon";
|
||||
|
||||
interface MobileFABProps {
|
||||
onClick: () => void;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const MobileFAB: React.FC<MobileFABProps> = ({
|
||||
onClick,
|
||||
label = "Create new",
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className="mobile-fab"
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon name="add" size="md" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Icon } from "./icon";
|
||||
import type { IconName } from "../utils/icons";
|
||||
|
||||
interface MobileListItem {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
icon?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface MobileListViewProps {
|
||||
items: MobileListItem[];
|
||||
onItemClick: (id: string) => void;
|
||||
onItemDelete?: (id: string) => void;
|
||||
onItemDuplicate?: (id: string) => void;
|
||||
emptyMessage?: string;
|
||||
searchPlaceholder?: string;
|
||||
onSearch?: (query: string) => void;
|
||||
}
|
||||
|
||||
export const MobileListView: React.FC<MobileListViewProps> = ({
|
||||
items,
|
||||
onItemClick,
|
||||
onItemDelete,
|
||||
onItemDuplicate,
|
||||
emptyMessage = "No items found",
|
||||
searchPlaceholder = "Search...",
|
||||
onSearch,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mobile-list-view">
|
||||
{onSearch && (
|
||||
<div className="mobile-list-search">
|
||||
<input
|
||||
type="search"
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
className="mobile-list-search-input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="mobile-list-empty">
|
||||
<Icon name="folder" size="lg" />
|
||||
<p>{emptyMessage}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mobile-list-items">
|
||||
{items.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
className="mobile-list-item"
|
||||
onClick={() => onItemClick(item.id)}
|
||||
type="button"
|
||||
>
|
||||
{item.icon && (
|
||||
<div className="mobile-list-item-icon">
|
||||
<Icon name={item.icon as IconName} size="md" />
|
||||
</div>
|
||||
)}
|
||||
<div className="mobile-list-item-content">
|
||||
<div className="mobile-list-item-title">{item.title}</div>
|
||||
{item.subtitle && (
|
||||
<div className="mobile-list-item-subtitle">{item.subtitle}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mobile-list-item-actions" style={{ transform: "rotate(180deg)" }}>
|
||||
<Icon name="arrow-left" size="sm" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +1,87 @@
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
import { Icon } from "./icon";
|
||||
import { ToolsBottomSheet } from "./tools-bottom-sheet";
|
||||
import type { IconName } from "../utils/icons";
|
||||
|
||||
interface MobileNavProps {
|
||||
sessionCount?: number;
|
||||
}
|
||||
|
||||
const MOBILE_NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [
|
||||
interface NavItem {
|
||||
to: string;
|
||||
label: string;
|
||||
icon: IconName;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
const MOBILE_NAV_ITEMS: NavItem[] = [
|
||||
{ to: "/", label: "Home", icon: "dashboard" },
|
||||
{ to: "/projects", label: "Projects", icon: "projects" },
|
||||
{ to: "/sessions", label: "Sessions", icon: "terminal" },
|
||||
{ to: "/tool-workshop", label: "Tools", icon: "settings" },
|
||||
{ to: "/tools", label: "Tools", icon: "settings", isGroup: true },
|
||||
{ to: "/settings", label: "Settings", icon: "settings" },
|
||||
];
|
||||
|
||||
export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
|
||||
const location = useLocation();
|
||||
const [toolsSheetOpen, setToolsSheetOpen] = useState(false);
|
||||
|
||||
const isToolsActive =
|
||||
location.pathname === "/tool-workshop" ||
|
||||
location.pathname === "/config-profiles";
|
||||
|
||||
const handleNavClick = (item: NavItem) => {
|
||||
if (item.isGroup) {
|
||||
setToolsSheetOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="mobile-nav" role="navigation" aria-label="Mobile navigation">
|
||||
{MOBILE_NAV_ITEMS.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
`mobile-nav-item ${isActive ? "active" : ""}`
|
||||
<>
|
||||
<nav className="mobile-nav" role="navigation" aria-label="Mobile navigation">
|
||||
{MOBILE_NAV_ITEMS.map((item) => {
|
||||
if (item.isGroup) {
|
||||
return (
|
||||
<button
|
||||
key={item.to}
|
||||
className={`mobile-nav-item ${isToolsActive ? "active" : ""}`}
|
||||
onClick={() => handleNavClick(item)}
|
||||
type="button"
|
||||
>
|
||||
<div className="mobile-nav-icon-wrapper">
|
||||
<Icon name={item.icon} size="md" />
|
||||
</div>
|
||||
<span className="mobile-nav-label">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<div className="mobile-nav-icon-wrapper">
|
||||
<Icon name={item.icon} size="md" />
|
||||
{item.to === "/sessions" && sessionCount ? (
|
||||
<span className="mobile-nav-badge">{sessionCount}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="mobile-nav-label">{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
`mobile-nav-item ${isActive ? "active" : ""}`
|
||||
}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<div className="mobile-nav-icon-wrapper">
|
||||
<Icon name={item.icon} size="md" />
|
||||
{item.to === "/sessions" && sessionCount ? (
|
||||
<span className="mobile-nav-badge">{sessionCount}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="mobile-nav-label">{item.label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<ToolsBottomSheet
|
||||
isOpen={toolsSheetOpen}
|
||||
onClose={() => setToolsSheetOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
interface ToolsBottomSheetProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TOOLS_ITEMS = [
|
||||
{ to: "/tool-workshop", label: "Tool Workshop" },
|
||||
{ to: "/config-profiles", label: "Config Profiles" },
|
||||
];
|
||||
|
||||
export const ToolsBottomSheet: React.FC<ToolsBottomSheetProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSelect = (to: string) => {
|
||||
onClose();
|
||||
navigate(to);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mobile-bottom-sheet-overlay"
|
||||
onClick={onClose}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
className="mobile-bottom-sheet"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-label="Tools menu"
|
||||
>
|
||||
<div className="mobile-bottom-sheet-header">
|
||||
<div className="mobile-bottom-sheet-handle" />
|
||||
<h3 className="mobile-bottom-sheet-title">Tools</h3>
|
||||
</div>
|
||||
<div className="mobile-bottom-sheet-content">
|
||||
{TOOLS_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.to}
|
||||
className={`mobile-bottom-sheet-item ${
|
||||
location.pathname === item.to ? "active" : ""
|
||||
}`}
|
||||
onClick={() => handleSelect(item.to)}
|
||||
type="button"
|
||||
>
|
||||
<span className="mobile-bottom-sheet-item-label">{item.label}</span>
|
||||
{location.pathname === item.to && <Icon name="success" size="sm" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 */}
|
||||
|
||||
+262
-3831
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-25
|
||||
@@ -0,0 +1,75 @@
|
||||
## Context
|
||||
|
||||
The current mobile experience for complex configuration and workspace pages is inadequate:
|
||||
|
||||
- **Tool Workshop** (desktop split-pane): List on left, detail/edit form on right. On mobile, both panels are cramped and unusable.
|
||||
- **Config Profiles** (desktop split-pane): Same issue as Tool Workshop, plus it's completely inaccessible from mobile navigation.
|
||||
- **Repo Workspace**: Shows file tree, editor, git toolbar, and terminal all competing for space. On mobile, nothing is usable.
|
||||
|
||||
Desktop layouts work well and should remain unchanged. This design focuses exclusively on mobile-first responsive alternatives.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Make Tool Workshop and Config Profiles fully usable on mobile
|
||||
- Add Config Profiles to mobile navigation (grouped under "Tools")
|
||||
- Redesign Repo Workspace for mobile with clear primary/secondary view hierarchy
|
||||
- Maintain desktop experience exactly as-is
|
||||
- Use consistent patterns across all mobile configuration pages
|
||||
|
||||
**Non-Goals:**
|
||||
- No changes to desktop layouts or navigation
|
||||
- No API or database changes
|
||||
- No changes to existing components' desktop behavior
|
||||
- Not a full redesign of the web app (focused on these 3 pages)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. List→Detail Navigation Pattern
|
||||
**Decision:** Use iOS Settings-style list→detail navigation for Tool Workshop and Config Profiles.
|
||||
**Rationale:** Native mobile pattern users already understand. Works well for browsing and editing items. Allows full-screen forms without cramped split-panes.
|
||||
**Alternative considered:** Bottom sheets for editing - rejected because complex forms need full-screen space.
|
||||
|
||||
### 2. Bottom Sheet for Tools Group
|
||||
**Decision:** Tapping "Tools" in mobile nav opens a bottom sheet with "Tool Workshop" and "Config Profiles" options.
|
||||
**Rationale:** Keeps bottom nav to 5 items (standard mobile pattern). Groups related functionality logically. Bottom sheet is fast and discoverable.
|
||||
**Alternative considered:** Separate nav items - rejected because 6 items is too many for bottom nav.
|
||||
|
||||
### 3. Read-First Detail View
|
||||
**Decision:** Detail view shows read-only information first, with an "Edit" button to enter edit mode.
|
||||
**Rationale:** Prevents accidental edits. Allows quick scanning of configuration without entering edit mode. Matches mobile app patterns (view then edit).
|
||||
**Alternative considered:** Direct edit mode - rejected because users often just want to view, not edit.
|
||||
|
||||
### 4. File Tree as Primary Repo Workspace View
|
||||
**Decision:** Mobile Repo Workspace shows file tree first, with bottom tabs to switch to Editor, Git, or Terminal.
|
||||
**Rationale:** File tree is the natural starting point for navigation. Bottom tabs provide quick access to other views without losing context.
|
||||
**Alternative considered:** Tabbed interface at top - rejected because bottom tabs are more thumb-friendly on mobile.
|
||||
|
||||
### 5. Full-Screen Edit Mode
|
||||
**Decision:** Edit forms open as full-screen pages with back navigation, not modals.
|
||||
**Rationale:** Complex forms with many fields need maximum screen real estate. Back navigation is a clear mental model.
|
||||
**Alternative considered:** Modal overlays - rejected because they feel cramped on mobile for long forms.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**Risk:** Users may find the list→detail pattern slower than split-pane for quick edits.
|
||||
→ **Mitigation:** Optimize for the most common flow (view first, edit only when needed). Add quick actions (delete, duplicate) directly in list view.
|
||||
|
||||
**Risk:** Switching between Editor, Git, and Terminal in Repo Workspace may feel disconnected.
|
||||
→ **Mitigation:** Keep the current file/repository context across tab switches. Show repository name persistently.
|
||||
|
||||
**Risk:** Two different UX patterns (desktop split-pane vs mobile list→detail) may confuse users who switch devices.
|
||||
→ **Mitigation:** This is standard responsive design practice. Both patterns are well-established in their respective contexts.
|
||||
|
||||
**Trade-off:** Mobile pages require more taps to accomplish the same tasks.
|
||||
→ **Acceptance:** This is inherent to mobile form factors. The trade-off is acceptable for improved usability.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
No migration needed - this is purely additive UI work. Steps:
|
||||
1. Create new mobile-specific components
|
||||
2. Update page components to render mobile layouts conditionally
|
||||
3. Update mobile navigation
|
||||
4. Test on mobile devices
|
||||
|
||||
Rollback: Remove conditional mobile rendering, revert to desktop-only layouts.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
The current mobile experience for complex pages (Tool Workshop, Config Profiles, Repo Workspace) is broken or unusable. Tool Workshop and Config Profiles use desktop split-pane layouts that don't work on small screens, and Config Profiles isn't even accessible from mobile navigation. The Repo Workspace shows a file tree and editor side-by-side, making both unusable on phones. We need mobile-first designs for these critical configuration and workspace pages.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Mobile Navigation**: Group "Tool Workshop" and "Config Profiles" under a single "Tools" bottom nav item that opens a bottom sheet with sub-options
|
||||
- **Mobile Tool Workshop**: Replace split-pane with list→detail navigation pattern (iOS Settings style)
|
||||
- **Mobile Config Profiles**: Replace split-pane with list→detail navigation pattern with read-first detail view
|
||||
- **Mobile Repo Workspace**: File tree as primary view with bottom tabs for Editor, Git, and Terminal
|
||||
- **Desktop**: No changes to existing layouts or navigation
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `mobile-tools-navigation`: Bottom sheet navigation grouping Tool Workshop and Config Profiles
|
||||
- `mobile-list-detail`: List→detail navigation pattern for complex configuration pages
|
||||
- `mobile-repo-workspace`: File-tree-primary workspace layout with tabbed secondary views
|
||||
|
||||
### Modified Capabilities
|
||||
- `mobile-navigation`: Add "Tools" group with bottom sheet sub-navigation
|
||||
|
||||
## Impact
|
||||
|
||||
- Frontend: New mobile-specific components and page layouts
|
||||
- Navigation: Mobile bottom nav structure changes
|
||||
- No API changes required
|
||||
- No database changes required
|
||||
@@ -0,0 +1,64 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Mobile list view for configuration items
|
||||
The mobile view SHALL display configuration items (tool types, config profiles) as a scrollable list of cards.
|
||||
|
||||
#### Scenario: Viewing tool types list
|
||||
- **WHEN** user navigates to Tool Workshop on mobile
|
||||
- **THEN** a list of tool type cards is displayed, each showing name and brief description
|
||||
|
||||
#### Scenario: Viewing config profiles list
|
||||
- **WHEN** user navigates to Config Profiles on mobile
|
||||
- **THEN** a list of profile cards is displayed, each showing name and description
|
||||
|
||||
#### Scenario: Empty state
|
||||
- **WHEN** the list has no items
|
||||
- **THEN** an empty state message is shown with a "Create" button
|
||||
|
||||
### Requirement: Item detail view
|
||||
Tapping a list item SHALL navigate to a detail view showing all configuration fields in read-only format.
|
||||
|
||||
#### Scenario: Viewing tool type details
|
||||
- **WHEN** user taps a tool type in the list
|
||||
- **THEN** a detail page opens showing all tool type fields (name, description, port, template, etc.)
|
||||
|
||||
#### Scenario: Viewing config profile details
|
||||
- **WHEN** user taps a config profile in the list
|
||||
- **THEN** a detail page opens showing all profile fields (env vars, mounts, includes, etc.)
|
||||
|
||||
### Requirement: Detail-to-edit navigation
|
||||
The detail view SHALL provide an "Edit" button that navigates to a full-screen edit form.
|
||||
|
||||
#### Scenario: Entering edit mode
|
||||
- **WHEN** user taps "Edit" on the detail view
|
||||
- **THEN** a full-screen edit form opens with all fields editable
|
||||
|
||||
#### Scenario: Saving changes
|
||||
- **WHEN** user modifies fields and taps "Save"
|
||||
- **THEN** changes are saved and the view returns to the detail page with updated data
|
||||
|
||||
#### Scenario: Canceling edit
|
||||
- **WHEN** user taps "Cancel" or back button
|
||||
- **THEN** changes are discarded and the view returns to the detail page
|
||||
|
||||
### Requirement: List item actions
|
||||
Each list item SHALL support swipe-to-delete and a quick actions menu.
|
||||
|
||||
#### Scenario: Deleting item
|
||||
- **WHEN** user swipes left on a list item and taps "Delete"
|
||||
- **THEN** a confirmation dialog appears, and upon confirmation the item is deleted
|
||||
|
||||
#### Scenario: Quick actions
|
||||
- **WHEN** user taps a "More" button on a list item
|
||||
- **THEN** an action sheet appears with options: Edit, Duplicate, Delete
|
||||
|
||||
### Requirement: Create new item
|
||||
A floating action button (FAB) on the list view SHALL open a creation form.
|
||||
|
||||
#### Scenario: Creating new item
|
||||
- **WHEN** user taps the FAB (+) on the list view
|
||||
- **THEN** a full-screen creation form opens
|
||||
|
||||
#### Scenario: Saving new item
|
||||
- **WHEN** user fills the form and taps "Save"
|
||||
- **THEN** the item is created and the view returns to the list with the new item visible
|
||||
@@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Bottom navigation grouping
|
||||
The mobile bottom navigation SHALL support grouping related pages under a single navigation item that opens a sub-menu.
|
||||
|
||||
#### Scenario: Tools group navigation
|
||||
- **WHEN** user views the mobile bottom navigation
|
||||
- **THEN** a "Tools" item is visible that groups Tool Workshop and Config Profiles
|
||||
|
||||
#### Scenario: Opening grouped menu
|
||||
- **WHEN** user taps a grouped navigation item
|
||||
- **THEN** a bottom sheet or menu opens showing the grouped pages
|
||||
|
||||
#### Scenario: Active state for grouped items
|
||||
- **WHEN** user is on a page within a group
|
||||
- **THEN** the group's navigation item shows as active
|
||||
@@ -0,0 +1,83 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: File tree as primary view
|
||||
The mobile Repo Workspace SHALL display the file tree as the primary view with repository and branch selectors at the top.
|
||||
|
||||
#### Scenario: Viewing file tree
|
||||
- **WHEN** user navigates to a project's workspace on mobile
|
||||
- **THEN** a file tree is displayed showing folders and files in the repository
|
||||
|
||||
#### Scenario: Repository selection
|
||||
- **WHEN** user taps the repository selector dropdown
|
||||
- **THEN** a list of available repositories is shown for selection
|
||||
|
||||
#### Scenario: Branch selection
|
||||
- **WHEN** user taps the branch selector dropdown
|
||||
- **THEN** a list of branches is shown for selection
|
||||
|
||||
### Requirement: File tree interactions
|
||||
The file tree SHALL support folder expansion, file opening, and git status indicators.
|
||||
|
||||
#### Scenario: Expanding folder
|
||||
- **WHEN** user taps a folder in the tree
|
||||
- **THEN** the folder expands to show its contents, or collapses if already expanded
|
||||
|
||||
#### Scenario: Opening file
|
||||
- **WHEN** user taps a file in the tree
|
||||
- **THEN** the file opens in the editor view
|
||||
|
||||
#### Scenario: Git status indicators
|
||||
- **WHEN** files have git status (modified, staged, untracked)
|
||||
- **THEN** visual indicators (colors/icons) are shown next to affected files
|
||||
|
||||
### Requirement: Bottom tab navigation
|
||||
The mobile workspace SHALL provide bottom tabs for switching between File Tree, Editor, Git, and Terminal views.
|
||||
|
||||
#### Scenario: Switching to Editor tab
|
||||
- **WHEN** user taps the "Editor" tab
|
||||
- **THEN** the editor view is shown with the currently selected file (or empty state)
|
||||
|
||||
#### Scenario: Switching to Git tab
|
||||
- **WHEN** user taps the "Git" tab
|
||||
- **THEN** the git view is shown with status, commit form, and file lists
|
||||
|
||||
#### Scenario: Switching to Terminal tab
|
||||
- **WHEN** user taps the "Terminal" tab
|
||||
- **THEN** the terminal view is shown for the current repository
|
||||
|
||||
### Requirement: Editor view
|
||||
The editor view SHALL provide a full-screen code editing experience with syntax highlighting.
|
||||
|
||||
#### Scenario: Editing file
|
||||
- **WHEN** user opens a file and modifies it
|
||||
- **THEN** syntax highlighting is applied and changes can be saved
|
||||
|
||||
#### Scenario: Editor toolbar
|
||||
- **WHEN** viewing the editor
|
||||
- **THEN** a toolbar shows file name, save button, undo/redo buttons
|
||||
|
||||
### Requirement: Git view
|
||||
The git view SHALL show repository status and allow committing changes.
|
||||
|
||||
#### Scenario: Viewing git status
|
||||
- **WHEN** user opens the Git tab
|
||||
- **THEN** modified, staged, and untracked files are listed separately
|
||||
|
||||
#### Scenario: Staging files
|
||||
- **WHEN** user toggles a file's checkbox
|
||||
- **THEN** the file is staged or unstaged accordingly
|
||||
|
||||
#### Scenario: Committing changes
|
||||
- **WHEN** user enters a commit message and taps "Commit"
|
||||
- **THEN** staged files are committed with the provided message
|
||||
|
||||
### Requirement: Terminal view
|
||||
The terminal view SHALL provide a full-screen terminal for the repository's tool instance.
|
||||
|
||||
#### Scenario: Terminal for repository
|
||||
- **WHEN** user opens the Terminal tab
|
||||
- **THEN** a terminal is shown connected to the repository's active tool instance
|
||||
|
||||
#### Scenario: No active instance
|
||||
- **WHEN** no tool instance is running for the repository
|
||||
- **THEN** a message is shown with a button to start a new session
|
||||
@@ -0,0 +1,27 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Tools bottom sheet navigation
|
||||
The mobile bottom navigation SHALL provide access to both Tool Workshop and Config Profiles through a grouped "Tools" entry.
|
||||
|
||||
#### Scenario: Opening Tools menu
|
||||
- **WHEN** user taps the "Tools" item in the mobile bottom navigation
|
||||
- **THEN** a bottom sheet slides up showing "Tool Workshop" and "Config Profiles" options
|
||||
|
||||
#### Scenario: Navigating to Tool Workshop
|
||||
- **WHEN** user taps "Tool Workshop" in the bottom sheet
|
||||
- **THEN** the bottom sheet closes and the app navigates to the Tool Workshop page
|
||||
|
||||
#### Scenario: Navigating to Config Profiles
|
||||
- **WHEN** user taps "Config Profiles" in the bottom sheet
|
||||
- **THEN** the bottom sheet closes and the app navigates to the Config Profiles page
|
||||
|
||||
#### Scenario: Closing bottom sheet without selection
|
||||
- **WHEN** user taps outside the bottom sheet or swipes down
|
||||
- **THEN** the bottom sheet closes without navigation
|
||||
|
||||
### Requirement: Active state indication
|
||||
The "Tools" bottom nav item SHALL indicate when either Tool Workshop or Config Profiles is the active page.
|
||||
|
||||
#### Scenario: Active page indication
|
||||
- **WHEN** user is viewing Tool Workshop or Config Profiles
|
||||
- **THEN** the "Tools" item in the bottom nav appears active/highlighted
|
||||
@@ -0,0 +1,49 @@
|
||||
## 1. Mobile Navigation Updates
|
||||
|
||||
- [x] 1.1 Add "Tools" grouped item to MobileNav component with bottom sheet
|
||||
- [x] 1.2 Create ToolsBottomSheet component for Tool Workshop / Config Profiles selection
|
||||
- [x] 1.3 Update AppShell desktop nav to keep Tool Workshop and Config Profiles separate
|
||||
- [x] 1.4 Add active state logic for grouped nav items
|
||||
|
||||
## 2. Mobile List-Detail Components
|
||||
|
||||
- [x] 2.1 Create MobileListView component for displaying item cards
|
||||
- [x] 2.2 Create MobileDetailView component for read-only detail display
|
||||
- [x] 2.3 Create MobileEditView component for full-screen editing
|
||||
- [x] 2.4 Add swipe-to-delete and action sheet to MobileListView
|
||||
- [x] 2.5 Create FAB (Floating Action Button) component for creating items
|
||||
|
||||
## 3. Mobile Tool Workshop
|
||||
|
||||
- [ ] 3.1 Add mobile list view for tool types
|
||||
- [ ] 3.2 Add mobile detail view for tool types (read-only)
|
||||
- [ ] 3.3 Add mobile edit view for tool types
|
||||
- [ ] 3.4 Add mobile create view for tool types
|
||||
- [ ] 3.5 Implement list→detail navigation in ToolWorkshopPage
|
||||
|
||||
## 4. Mobile Config Profiles
|
||||
|
||||
- [ ] 4.1 Add mobile list view for config profiles
|
||||
- [ ] 4.2 Add mobile detail view for config profiles (read-only)
|
||||
- [ ] 4.3 Add mobile edit view for config profiles
|
||||
- [ ] 4.4 Add mobile create view for config profiles
|
||||
- [ ] 4.5 Implement list→detail navigation in ConfigProfilesPage
|
||||
|
||||
## 5. Mobile Repo Workspace
|
||||
|
||||
- [ ] 5.1 Create MobileFileTree component with folder expansion
|
||||
- [ ] 5.2 Create MobileEditorView component for full-screen editing
|
||||
- [ ] 5.3 Create MobileGitView component for git operations
|
||||
- [ ] 5.4 Create MobileTerminalView component for terminal access
|
||||
- [ ] 5.5 Add bottom tab navigation to RepoWorkspace (Tree, Editor, Git, Terminal)
|
||||
- [ ] 5.6 Implement repository and branch selectors for mobile
|
||||
|
||||
## 6. Testing & Polish
|
||||
|
||||
- [ ] 6.1 Test mobile navigation on iPhone SE (320px)
|
||||
- [ ] 6.2 Test mobile navigation on iPhone standard (375px)
|
||||
- [ ] 6.3 Verify desktop layouts remain unchanged
|
||||
- [ ] 6.4 Run npm run typecheck
|
||||
- [ ] 6.5 Run npm run lint
|
||||
- [ ] 6.6 Run npm run build
|
||||
- [ ] 6.7 Test swipe gestures and touch targets
|
||||
Reference in New Issue
Block a user