feat: rework mobile UI for Tool Workshop and Profile pages
- Rework ToolWorkshopMobileView to support full desktop functionality: definition type selection (Compose/Dockerfile/Manifest), manifest editor, conditional port, startup command, readiness probe, required variables, and validation feedback. - Add ProfileMobileView and wire ProfilePage to render it on mobile. - Update useToolWorkshop hook to return boolean success from submit. - Add responsive CSS for mobile forms, edit views, and manifest editor. - Update project maps. Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed) Refs: openspec/changes/mobile-tool-profile-ui
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ dir: .
|
||||
Trust boundary: index routes, map orients, source decides.
|
||||
|
||||
## role
|
||||
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
||||
Infrastructure and deployment configuration for a self-hosted project management and git repository platform with OAuth2 authentication.
|
||||
## parent
|
||||
-
|
||||
## children
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ index: ./.pi-map.index.md
|
||||
Trust boundary: index routes, map orients, source decides.
|
||||
|
||||
## role
|
||||
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
||||
A self-hosted, containerized project management and Git repository platform with OAuth2/SSO authentication, designed for deployment via Docker Compose with optional Traefik reverse proxy integration.
|
||||
## files
|
||||
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
|
||||
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
|
||||
@@ -31,7 +31,7 @@ Infrastructure and deployment configuration package for a self-hosted project ma
|
||||
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
|
||||
- swap-pane | Empty file with no functionality
|
||||
## arch
|
||||
Docker Compose-based microservices architecture with frontend/backend separation, PostgreSQL/Redis data layer, Traefik reverse proxy integration, and environment-driven configuration management following twelve-factor app principles.
|
||||
Multi-service containerized architecture using PostgreSQL and Redis for persistence/caching, a decoupled frontend/backend web stack, environment-driven configuration via .env files, and infrastructure-as-code deployment patterns with Docker Compose profiles for standalone and reverse-proxy modes.
|
||||
## tags
|
||||
docker, redis, git, application, postgresql, compose, traefik, project
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps
|
||||
|
||||
## role
|
||||
Contains the main deployable application modules or entry points for the project.
|
||||
Contains the main deployable application entry points and top-level configurations for the project.
|
||||
## parent
|
||||
index: ./.pi-map.index.md
|
||||
map: ./.pi-map.md
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@ dir: apps
|
||||
index: apps/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains the main deployable application modules or entry points for the project.
|
||||
Container directory for individual deployable applications within the project.
|
||||
## files
|
||||
## arch
|
||||
Modular monolith or microservices architecture with separate application boundaries, each potentially having its own configuration, dependencies, and lifecycle.
|
||||
Monorepo-style organization where each subdirectory represents a standalone application sharing common infrastructure and dependencies.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web
|
||||
|
||||
## role
|
||||
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
|
||||
Frontend web application for a browser-based code editing and terminal environment with client-side routing.
|
||||
## parent
|
||||
index: apps/.pi-map.index.md
|
||||
map: apps/.pi-map.md
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ dir: apps/web
|
||||
index: apps/web/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
|
||||
Frontend web application providing the user-facing interface for the "headquarter" project, built as a modern React SPA with code editing, terminal, and routing capabilities.
|
||||
## files
|
||||
- .env.example | Template file defining example environment variables for frontend API and application URL configuration
|
||||
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint
|
||||
@@ -16,7 +16,7 @@ Frontend web application providing a React-based UI with code editing, terminal,
|
||||
- tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite
|
||||
- vite.config.ts | Configures Vite build tool for a React project with custom dev server port and Vitest test settings. | dep: vite, @vitejs/plugin-react
|
||||
## arch
|
||||
Modern React SPA built with Vite and TypeScript, using nginx for production serving with client-side routing, multi-stage Docker deployment, and Vitest for testing.
|
||||
Modern React SPA architecture using Vite for build tooling, TypeScript for type safety, nginx for static serving with SPA routing support, and Docker multi-stage builds for containerized deployment with non-root security.
|
||||
## tags
|
||||
react, eslint, vite, typescript, dom, application, nginx, web
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src
|
||||
|
||||
## role
|
||||
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||
Entry point and core infrastructure for a React web application providing authentication-aware routing and shared type definitions.
|
||||
## parent
|
||||
index: apps/web/.pi-map.index.md
|
||||
map: apps/web/.pi-map.md
|
||||
|
||||
@@ -4,13 +4,13 @@ dir: apps/web/src
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||
Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend.
|
||||
## files
|
||||
- main.tsx | Bootstraps a React application with routing, authentication, and session management providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom
|
||||
- router.tsx | Defines the React Router configuration for a web application with protected routes, nested layouts, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
|
||||
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
||||
## arch
|
||||
Modular React SPA using React Router v6 with nested route layouts, protected route guards via authentication context, and centralized TypeScript domain models for session/workspace/project entities.
|
||||
Layered React SPA architecture using React Router v6 with nested route layouts, provider composition pattern for auth/session context, and centralized TypeScript domain modeling.
|
||||
## tags
|
||||
pages, styles, css, router, react, session, dom, project
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components
|
||||
|
||||
## role
|
||||
Provides reusable, accessible UI components and utilities for rendering the application shell, data states, icons, code display, notifications, and route protection in a React web application.
|
||||
Provides reusable, accessible UI primitives and layout components for a React web application, including shell layout, data states, icons, code display, routing guards, and toast notifications.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features
|
||||
|
||||
## role
|
||||
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
||||
Reusable UI feature components that compose domain-specific functionality for the web application
|
||||
## parent
|
||||
index: apps/web/src/components/.pi-map.index.md
|
||||
map: apps/web/src/components/.pi-map.md
|
||||
@@ -19,6 +19,9 @@ map: apps/web/src/components/.pi-map.md
|
||||
- apps/web/src/components/features/notification
|
||||
index: apps/web/src/components/features/notification/.pi-map.index.md
|
||||
map: apps/web/src/components/features/notification/.pi-map.md
|
||||
- apps/web/src/components/features/profile
|
||||
index: apps/web/src/components/features/profile/.pi-map.index.md
|
||||
map: apps/web/src/components/features/profile/.pi-map.md
|
||||
- apps/web/src/components/features/project
|
||||
index: apps/web/src/components/features/project/.pi-map.index.md
|
||||
map: apps/web/src/components/features/project/.pi-map.md
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# apps/web/src/components/features/profile (index)
|
||||
dir: apps/web/src/components/features/profile
|
||||
|
||||
## role
|
||||
Provides a mobile-specific UI for users to view and edit their profile information.
|
||||
## parent
|
||||
index: apps/web/src/components/features/.pi-map.index.md
|
||||
map: apps/web/src/components/features/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- ProfileMobileView.tsx
|
||||
## links
|
||||
index: apps/web/src/components/features/profile/.pi-map.index.md
|
||||
map: apps/web/src/components/features/profile/.pi-map.md
|
||||
## workflows
|
||||
- change profile behavior
|
||||
read: ProfileMobileView.tsx
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,20 @@
|
||||
# apps/web/src/components/features/profile
|
||||
dir: apps/web/src/components/features/profile
|
||||
|
||||
index: apps/web/src/components/features/profile/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides a mobile-specific UI for users to view and edit their profile information.
|
||||
## files
|
||||
- ProfileMobileView.tsx | Renders a mobile-optimized profile editing form with avatar upload, name/email fields, and save/cancel functionality. | exp: ProfileMobileView | dep: react-router-dom, ../mobile/mobile-edit-view, ../../icon, ../../../api/profile
|
||||
## arch
|
||||
Single feature-focused component with form handling and file upload, likely using controlled inputs and local state management.
|
||||
## tags
|
||||
mobile, profile, view, renders, optimized, editing, form, avatar
|
||||
## symbols
|
||||
- ProfileMobileView
|
||||
## workflows
|
||||
- change profile behavior
|
||||
read: ProfileMobileView.tsx
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MobileEditView } from "../mobile/mobile-edit-view";
|
||||
import { Icon } from "../../icon";
|
||||
import type { UserProfile } from "../../../api/profile";
|
||||
|
||||
interface ProfileMobileViewProps {
|
||||
profile: UserProfile;
|
||||
name: string;
|
||||
email: string;
|
||||
error: string | null;
|
||||
isSaving: boolean;
|
||||
avatarUrl: string | null;
|
||||
fileInputRef: React.RefObject<HTMLInputElement>;
|
||||
onNameChange: (value: string) => void;
|
||||
onEmailChange: (value: string) => void;
|
||||
onAvatarButtonClick: () => void;
|
||||
onAvatarChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export const ProfileMobileView = ({
|
||||
profile,
|
||||
name,
|
||||
email,
|
||||
error,
|
||||
isSaving,
|
||||
avatarUrl,
|
||||
fileInputRef,
|
||||
onNameChange,
|
||||
onEmailChange,
|
||||
onAvatarButtonClick,
|
||||
onAvatarChange,
|
||||
onSave,
|
||||
}: ProfileMobileViewProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<MobileEditView
|
||||
title="Profile"
|
||||
onCancel={() => navigate(-1)}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
>
|
||||
<div className="profile-mobile-avatar-section">
|
||||
<div className="profile-mobile-avatar">
|
||||
{avatarUrl ? (
|
||||
<img alt="Avatar" className="profile-mobile-avatar-image" src={avatarUrl} />
|
||||
) : (
|
||||
<div className="profile-mobile-avatar-placeholder">
|
||||
{profile.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={isSaving}
|
||||
onClick={onAvatarButtonClick}
|
||||
type="button"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Icon name="loading" size="sm" />
|
||||
Uploading...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="edit" size="sm" />
|
||||
Change Avatar
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<input
|
||||
accept="image/png,image/jpeg"
|
||||
onChange={onAvatarChange}
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label" htmlFor="profile-mobile-name">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
className="mobile-form-input"
|
||||
disabled={isSaving}
|
||||
id="profile-mobile-name"
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
placeholder="Your name"
|
||||
type="text"
|
||||
value={name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label" htmlFor="profile-mobile-email">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
className="mobile-form-input"
|
||||
disabled={isSaving}
|
||||
id="profile-mobile-email"
|
||||
onChange={(e) => onEmailChange(e.target.value)}
|
||||
placeholder="your.email@example.com"
|
||||
type="email"
|
||||
value={email}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="mobile-form-error">
|
||||
<Icon name="warning" size="sm" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</MobileEditView>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features/tool-workshop
|
||||
|
||||
## role
|
||||
Provides a complete UI for managing custom tool types in a "Tool Workshop" interface with list, edit, and mobile-responsive views.
|
||||
Provides a complete CRUD interface for managing reusable tool type definitions (Docker Compose, Dockerfile, Manifest) in a workshop-style admin panel with responsive desktop and mobile layouts.
|
||||
## parent
|
||||
index: apps/web/src/components/features/.pi-map.index.md
|
||||
map: apps/web/src/components/features/.pi-map.md
|
||||
|
||||
@@ -4,15 +4,15 @@ dir: apps/web/src/components/features/tool-workshop
|
||||
index: apps/web/src/components/features/tool-workshop/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides a complete UI for managing custom tool types in a "Tool Workshop" interface with list, edit, and mobile-responsive views.
|
||||
Provides a complete CRUD interface for managing reusable tool type definitions (Docker Compose, Dockerfile, Manifest) in a workshop-style admin panel with responsive desktop and mobile layouts.
|
||||
## files
|
||||
- ToolTypeEditorPanel.tsx | Renders a form panel for creating or editing tool types with support for compose, dockerfile, and manifest definition types | exp: ToolTypeFormState, ToolTypeEditorPanel | dep: ../../icon, ../tool/manifest-editor, ../../../api/tool-types, ../../../api/tool-definitions, React, Icon, ManifestEditor
|
||||
- ToolTypeListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting tool types in a "Tool Workshop" interface. | exp: ToolTypeListSidebar | dep: ../../icon, ../../../api/tool-types, React, Icon component, ToolType type
|
||||
- ToolWorkshopMobileView.tsx | Renders a mobile-responsive view for managing tool types with list, detail, and edit modes | exp: MobileView, ToolWorkshopMobileView | dep: ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../../../api/tool-types, ./ToolTypeEditorPanel, MobileListView, MobileDetailView, MobileEditView, MobileFAB, ToolType, ToolTypeFormState
|
||||
- ToolWorkshopMobileView.tsx | Renders a mobile-responsive three-view (list/detail/edit) interface for managing tool types in a tool workshop, with form handling for creating and editing tool configurations including Docker Compose, Dockerfile, or Manifest definitions. | exp: MobileView, ToolWorkshopMobileView | dep: react, ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../tool/manifest-editor, ../../icon, ../../../api/tool-types, ./ToolTypeEditorPanel, ../../../api/tool-definitions, mobile-list-view, mobile-detail-view, mobile-edit-view, mobile-fab, manifest-editor, icon, tool-types, ToolTypeEditorPanel, tool-definitions
|
||||
## arch
|
||||
Uses a split-pane sidebar/detail panel pattern with dedicated mobile breakpoint handling, separating list navigation from form editing concerns across three specialized view components.
|
||||
Compound component pattern with split-pane desktop layout (sidebar list + editor panel) and state-driven mobile view with three-view routing (list/detail/edit), using React state for form management and optimistic UI updates.
|
||||
## tags
|
||||
tool, mobile, type, view, types, list, panel, editor
|
||||
tool, mobile, view, type, types, editor, list, workshop
|
||||
## symbols
|
||||
- ToolTypeFormState
|
||||
- ToolTypeEditorPanel
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { MobileListView } from "../mobile/mobile-list-view";
|
||||
import { MobileDetailView } from "../mobile/mobile-detail-view";
|
||||
import { MobileEditView } from "../mobile/mobile-edit-view";
|
||||
import { MobileFAB } from "../mobile/mobile-fab";
|
||||
import { ManifestEditor } from "../tool/manifest-editor";
|
||||
import { Icon } from "../../icon";
|
||||
import type { ToolType } from "../../../api/tool-types";
|
||||
import type { ToolTypeFormState } from "./ToolTypeEditorPanel";
|
||||
import type { ToolDefinitionManifest } from "../../../api/tool-definitions";
|
||||
|
||||
export type MobileView = "list" | "detail" | "edit";
|
||||
|
||||
@@ -13,13 +18,18 @@ interface ToolWorkshopMobileViewProps {
|
||||
mobileView: MobileView;
|
||||
isCreating: boolean;
|
||||
toolTypeForm: ToolTypeFormState;
|
||||
manifestData: Record<string, unknown> | null;
|
||||
manifestDefinitionId: string | null;
|
||||
baseDefinitions: ToolDefinitionManifest[];
|
||||
toolTypeError: string | null;
|
||||
toolTypeDirty: boolean;
|
||||
onViewChange: (view: MobileView) => void;
|
||||
onSelect: (toolType: ToolType) => void;
|
||||
onCreate: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
||||
onSubmit: () => void;
|
||||
onManifestChange: (manifest: Record<string, unknown> | null) => void;
|
||||
onSubmit: (e?: React.FormEvent) => Promise<boolean>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
@@ -29,15 +39,41 @@ export const ToolWorkshopMobileView = ({
|
||||
mobileView,
|
||||
isCreating,
|
||||
toolTypeForm,
|
||||
manifestData,
|
||||
manifestDefinitionId,
|
||||
baseDefinitions,
|
||||
toolTypeError,
|
||||
toolTypeDirty,
|
||||
onViewChange,
|
||||
onSelect,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onFormChange,
|
||||
onManifestChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: ToolWorkshopMobileViewProps) => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const ok = await onSubmit();
|
||||
if (ok) {
|
||||
onViewChange("list");
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedToolType) {
|
||||
void onDelete(selectedToolType.id);
|
||||
onViewChange("list");
|
||||
}
|
||||
};
|
||||
|
||||
if (mobileView === "list") {
|
||||
return (
|
||||
<div className="mobile-page">
|
||||
@@ -49,7 +85,7 @@ export const ToolWorkshopMobileView = ({
|
||||
items={toolTypes.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.display_name,
|
||||
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||
subtitle: `${t.category || "Uncategorized"} · ${t.definition_type} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||
}))}
|
||||
onItemClick={(id) => {
|
||||
const toolType = toolTypes.find((t) => t.id === id);
|
||||
@@ -71,64 +107,70 @@ export const ToolWorkshopMobileView = ({
|
||||
}
|
||||
|
||||
if (mobileView === "detail" && selectedToolType) {
|
||||
const 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" as const,
|
||||
},
|
||||
...(selectedToolType.requires_port
|
||||
? [{ 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,
|
||||
},
|
||||
...(selectedToolType.definition_type !== "manifest"
|
||||
? [
|
||||
{
|
||||
label:
|
||||
selectedToolType.definition_type === "compose"
|
||||
? "Compose Template"
|
||||
: "Dockerfile Template",
|
||||
value:
|
||||
selectedToolType.definition_type === "compose"
|
||||
? selectedToolType.compose_template
|
||||
: selectedToolType.dockerfile_template,
|
||||
type: "code" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
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",
|
||||
},
|
||||
]}
|
||||
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type}`}
|
||||
fields={fields}
|
||||
onEdit={() => {
|
||||
onViewChange("edit");
|
||||
}}
|
||||
onDelete={() => {
|
||||
void onDelete(selectedToolType.id);
|
||||
onViewChange("list");
|
||||
}}
|
||||
onDelete={handleDelete}
|
||||
onBack={() => {
|
||||
onViewChange("list");
|
||||
}}
|
||||
@@ -137,18 +179,39 @@ export const ToolWorkshopMobileView = ({
|
||||
}
|
||||
|
||||
if (mobileView === "edit") {
|
||||
const templateValue =
|
||||
toolTypeForm.definition_type === "compose"
|
||||
? toolTypeForm.compose_template
|
||||
: toolTypeForm.dockerfile_template;
|
||||
|
||||
return (
|
||||
<MobileEditView
|
||||
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
|
||||
onCancel={onCancel}
|
||||
onSave={() => {
|
||||
onSubmit();
|
||||
if (!toolTypeError) {
|
||||
onViewChange("list");
|
||||
}
|
||||
}}
|
||||
isSaving={false}
|
||||
onSave={handleSave}
|
||||
isSaving={isSaving}
|
||||
>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Definition Type</label>
|
||||
<select
|
||||
value={toolTypeForm.definition_type}
|
||||
onChange={(e) =>
|
||||
onFormChange({
|
||||
definition_type: e.target.value as
|
||||
| "compose"
|
||||
| "dockerfile"
|
||||
| "manifest",
|
||||
})
|
||||
}
|
||||
className="mobile-form-select"
|
||||
disabled={!isCreating}
|
||||
>
|
||||
<option value="compose">Docker Compose</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="manifest">Manifest (Declarative)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Name *</label>
|
||||
<input
|
||||
@@ -157,8 +220,11 @@ export const ToolWorkshopMobileView = ({
|
||||
onChange={(e) => onFormChange({ name: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., my-tool"
|
||||
disabled={!isCreating}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Display Name *</label>
|
||||
<input
|
||||
@@ -167,8 +233,10 @@ export const ToolWorkshopMobileView = ({
|
||||
onChange={(e) => onFormChange({ display_name: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., My Tool"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Description</label>
|
||||
<textarea
|
||||
@@ -179,6 +247,7 @@ export const ToolWorkshopMobileView = ({
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Category</label>
|
||||
<input
|
||||
@@ -189,130 +258,179 @@ export const ToolWorkshopMobileView = ({
|
||||
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) =>
|
||||
onChange={(e) => {
|
||||
const value = e.target.value as "web" | "terminal";
|
||||
onFormChange({
|
||||
interface_type: e.target.value as "web" | "terminal",
|
||||
})
|
||||
}
|
||||
interface_type: value,
|
||||
requires_port: value === "web",
|
||||
default_port: value === "web" ? toolTypeForm.default_port : "",
|
||||
});
|
||||
}}
|
||||
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) => onFormChange({ 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) => onFormChange({ 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) =>
|
||||
onFormChange({
|
||||
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) => onFormChange({ startup_command: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="Command to run on startup"
|
||||
/>
|
||||
|
||||
<div className="mobile-form-group mobile-form-row">
|
||||
<label className="mobile-form-label mobile-form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={toolTypeForm.requires_port}
|
||||
onChange={(e) =>
|
||||
onFormChange({ requires_port: e.target.checked })
|
||||
}
|
||||
className="mobile-form-checkbox"
|
||||
/>
|
||||
Requires Port
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{toolTypeForm.requires_port && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Default Port *</label>
|
||||
<input
|
||||
type="number"
|
||||
value={toolTypeForm.default_port}
|
||||
onChange={(e) =>
|
||||
onFormChange({ default_port: e.target.value })
|
||||
}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., 8080"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toolTypeForm.interface_type === "terminal" && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Startup Command</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.startup_command}
|
||||
onChange={(e) =>
|
||||
onFormChange({ startup_command: e.target.value })
|
||||
}
|
||||
className="mobile-form-input"
|
||||
placeholder="Command to run on startup"
|
||||
/>
|
||||
<small className="mobile-form-help">
|
||||
Command to run before the interactive shell for each new
|
||||
terminal session.
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toolTypeForm.definition_type === "manifest" ? (
|
||||
<div className="mobile-manifest-editor-wrapper">
|
||||
<ManifestEditor
|
||||
manifest={manifestData}
|
||||
baseDefinitions={baseDefinitions}
|
||||
onChange={(m) => onManifestChange(m)}
|
||||
definitionId={manifestDefinitionId}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">
|
||||
{toolTypeForm.definition_type === "compose"
|
||||
? "Compose Template *"
|
||||
: "Dockerfile Template *"}
|
||||
</label>
|
||||
<textarea
|
||||
value={templateValue}
|
||||
onChange={(e) => {
|
||||
if (toolTypeForm.definition_type === "compose") {
|
||||
onFormChange({ compose_template: e.target.value });
|
||||
} else {
|
||||
onFormChange({ dockerfile_template: e.target.value });
|
||||
}
|
||||
}}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
placeholder={
|
||||
toolTypeForm.definition_type === "compose"
|
||||
? "version: '3'"
|
||||
: "FROM ubuntu:22.04"
|
||||
}
|
||||
rows={10}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Readiness Command</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.readiness_command}
|
||||
onChange={(e) => onFormChange({ readiness_command: e.target.value })}
|
||||
onChange={(e) =>
|
||||
onFormChange({ 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>
|
||||
<label className="mobile-form-label">Readiness Timeout (seconds)</label>
|
||||
<input
|
||||
type="text"
|
||||
type="number"
|
||||
value={toolTypeForm.readiness_timeout}
|
||||
onChange={(e) => onFormChange({ readiness_timeout: e.target.value })}
|
||||
onChange={(e) =>
|
||||
onFormChange({ 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>
|
||||
<label className="mobile-form-label">Readiness Interval (seconds)</label>
|
||||
<input
|
||||
type="text"
|
||||
type="number"
|
||||
value={toolTypeForm.readiness_interval}
|
||||
onChange={(e) => onFormChange({ readiness_interval: e.target.value })}
|
||||
onChange={(e) =>
|
||||
onFormChange({ 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) => onFormChange({ required_variables: e.target.value })}
|
||||
onChange={(e) =>
|
||||
onFormChange({ 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) => onFormChange({ compose_template: e.target.value })}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
placeholder="version: '3'"
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{toolTypeError && (
|
||||
<p className="mobile-form-error">
|
||||
<Icon name="warning" size="sm" /> {toolTypeError}
|
||||
</p>
|
||||
)}
|
||||
{toolTypeForm.definition_type === "dockerfile" && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Dockerfile Template</label>
|
||||
<textarea
|
||||
value={toolTypeForm.dockerfile_template}
|
||||
onChange={(e) =>
|
||||
onFormChange({ dockerfile_template: e.target.value })
|
||||
}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
placeholder="FROM ubuntu:22.04"
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{toolTypeDirty && (
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button mobile-discard-button"
|
||||
onClick={onCancel}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Discard Changes
|
||||
</button>
|
||||
)}
|
||||
</MobileEditView>
|
||||
);
|
||||
@@ -328,7 +446,7 @@ export const ToolWorkshopMobileView = ({
|
||||
items={toolTypes.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.display_name,
|
||||
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||
subtitle: `${t.category || "Uncategorized"} · ${t.definition_type} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||
}))}
|
||||
onItemClick={(id) => {
|
||||
const toolType = toolTypes.find((t) => t.id === id);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/hooks
|
||||
|
||||
## role
|
||||
A collection of custom React hooks providing reusable state management, API integration, and UI behavior logic for the web application.
|
||||
Provides a collection of reusable React custom hooks that encapsulate domain-specific business logic, API interactions, and UI state management for the web application.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,15 +4,15 @@ dir: apps/web/src/hooks
|
||||
index: apps/web/src/hooks/.pi-map.index.md
|
||||
|
||||
## role
|
||||
A collection of custom React hooks providing reusable state management, API integration, and UI behavior logic for the web application.
|
||||
Provides a collection of reusable React custom hooks that encapsulate domain-specific business logic, API interactions, and UI state management for the web application.
|
||||
## files
|
||||
- use-async-data.ts | A custom React hook that manages asynchronous data fetching with loading, error, and ready states, plus a manual reload capability. | exp: func:useAsyncData(fetcher: () => Promise<T>, deps: React.DependencyList) → UseAsyncDataResult<T>, call:useState, call:useCallback, call:setStatus, call:setError, call:fetcher, call:setData, call:load, call:useEffect | dep: react
|
||||
- use-auto-hide.ts | A React custom hook that automatically hides an element after a specified timeout and provides manual controls for showing, hiding, and toggling visibility. | exp: func:useAutoHide(options: AutoHideOptions), call:useState, call:useRef, call:Date.now, call:useCallback, call:setIsVisible, call:clearTimeout, call:setTimeout, call:hide, call:show, call:useEffect | dep: react
|
||||
- use-config-profiles.ts | A React custom hook that manages config profile CRUD operations, form state, profile inclusion with cycle detection, and drag-and-drop reordering. | exp: useConfigProfiles | dep: react, ../utils/errors, ../api/config-profiles, ../api/projects, ../api/tool-types, ../types
|
||||
- use-events.test.ts | Tests a React custom hook that manages Server-Sent Events connections with automatic reconnection, backoff strategies, and error handling. | dep: vitest, @testing-library/react, ./use-events, ../api/events, use-events hook, ../api/events module
|
||||
- use-events.ts | React hook that manages a Server-Sent Events connection with exponential backoff reconnection, event buffering, and auth/rate-limit handling. | exp: UseEventsReturn, func:useEvents() → UseEventsReturn, call:useState, call:useRef, call:useCallback, call:clearTimeout, call:createEventSource, call:setConnected, call:setError, call:setReconnectCount, call:JSON.parse, call:setEvents, call:es.close, call:Math.min, call:Math.pow, call:Math.random, call:setTimeout, call:probeEventStreamStatus, call:window.location.assign, call:connect, call:useEffect, call:esRef.current.close | dep: react, ../api/events, ../types/events
|
||||
- use-events.test.ts | Tests a React hook that manages Server-Sent Events (SSE) connections with automatic reconnection, backoff, and error handling | dep: vitest, @testing-library/react, ./use-events, ../api/events, use-events hook
|
||||
- use-events.ts | React hook that manages a Server-Sent Events connection with automatic exponential backoff reconnection, lifecycle event handling, and authentication redirect on 401 errors. | exp: UseEventsReturn, func:useEvents() → UseEventsReturn, call:useState, call:useRef, call:useCallback, call:clearTimeout, call:createEventSource, call:setConnected, call:setError, call:setReconnectCount, call:JSON.parse, call:setEvents, call:es.addEventListener, call:es.close, call:Math.min, call:Math.pow, call:Math.random, call:setTimeout, call:probeEventStreamStatus, call:window.location.assign, call:connect, call:useEffect, call:esRef.current.close | dep: react, ../api/events, ../types/events
|
||||
- use-git-repo.ts | A custom React hook that centralizes all git repository operations (branch management, status tracking, commit history, and git actions) into a reusable interface for components. | exp: GitStatus, UseGitRepoResult, func:useGitRepo(projectId: string | undefined, repoId: string | undefined) → UseGitRepoResult, call:useState, call:useCallback, call:setLoading, call:setError, call:fn, call:extractError, call:withLoading, call:listRepositoryBranches, call:setBranches, call:data.branches.map, call:setDefaultBranch, call:getRepositoryStatus, call:setStatus, call:getRepositoryHistory, call:setHistory, call:getCommitDetail, call:setCommitDetail, call:commitChanges, call:refreshStatus, call:pushRepository, call:pullRepository, call:fetchRepository, call:checkoutBranch, call:refreshBranches, call:createBranch, call:deleteBranch, call:mergeBranches, call:refreshHistory, call:useEffect, raise:err | dep: react, ../api/git-repositories
|
||||
- use-instance-actions.ts | A custom React hook that provides a centralized interface for managing instance/session actions including opening, starting, stopping, deleting, force-deleting, recreating tunnels, and renaming, with loading states and dirty delete handling for conflict resolution. | exp: func:useInstanceActions(options: UseInstanceActionsOptions) → UseInstanceActionsReturn, call:useSessions, call:useSessionOperations, call:useState, call:useRef, call:useCallback, call:tabRefs.current.get, call:existing.focus, call:session.tool_type_interfaces?.includes, call:window.open, call:tabRefs.current.set, call:setLoadingSessionId, call:startOperation, call:startInstance, call:onRefresh, call:completeOperation, call:stopInstance, call:deleteInstance, call:setDirtyDeleteSession, call:setDirtyDeleteFiles, call:removeSession, call:recreateInstanceTunnel, call:alert, call:newName.trim, call:renameInstance | dep: react, ../api/sessions, ../state/sessions, ../state/session-operations
|
||||
- use-instance-actions.ts | A React custom hook that manages instance/session actions including opening, starting, stopping, deleting, force-deleting, recreating tunnels, and renaming with loading states and dirty delete handling. | exp: func:useInstanceActions(options: UseInstanceActionsOptions) → UseInstanceActionsReturn, call:useSessions, call:useState, call:useRef, call:useCallback, call:tabRefs.current.get, call:existing.focus, call:session.tool_type_interfaces?.includes, call:window.open, call:tabRefs.current.set, call:setLoadingSessionId, call:startInstance, call:onRefresh, call:stopInstance, call:deleteInstance, call:setDirtyDeleteSession, call:setDirtyDeleteFiles, call:removeSession, call:recreateInstanceTunnel, call:alert, call:newName.trim, call:renameInstance | dep: react, ../api/sessions, ../state/sessions
|
||||
- use-mobile-viewport.ts | A React hook that tracks whether the viewport width is below a mobile breakpoint (768px) | exp: func:useMobileViewport(), call:useState, call:useEffect, call:setIsMobile, call:window.addEventListener, call:window.removeEventListener | dep: react
|
||||
- use-notifications.test.tsx | Tests the useNotifications custom React hook with mocked API calls, covering optimistic updates, polling behavior, and error handling. | dep: vitest, @testing-library/react, ./use-notifications, ../state/notifications, ../api/notifications, use-notifications
|
||||
- use-notifications.ts | Custom React hook that provides access to notification state and ensures it's used within a NotificationProvider | exp: func:useNotifications(), call:useContext, raise:Error | dep: react, ../state/notifications
|
||||
@@ -23,7 +23,7 @@ A collection of custom React hooks providing reusable state management, API inte
|
||||
- use-terminal-page.ts | Manages terminal page state including sessions, keyboard shortcuts, fullscreen mode, mobile viewport handling, and terminal lifecycle operations. | exp: useTerminalPage | dep: react, react-router-dom, ../components/features/terminal/terminal, ../components/features/terminal/terminal-session-tabs, ./use-mobile-viewport, ./use-auto-hide, ./use-virtual-keyboard, ./use-terminal-sessions, ../api/terminal, ./use-special-keys, use-mobile-viewport, use-auto-hide, use-virtual-keyboard, use-terminal-sessions, terminal, terminal-session-tabs, api/terminal, api/sessions, use-special-keys
|
||||
- use-terminal-sessions.ts | React custom hook that manages terminal session state (CRUD operations, active session tracking) for a given instance | exp: UseTerminalSessionsResult, func:useTerminalSessions(instanceId: string) → UseTerminalSessionsResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listTerminalSessions, call:setSessions, call:setActiveSessionId, call:createTerminalSession, call:closeTerminalSession, call:prev.filter, call:renameTerminalSession, call:prev.map, call:resetTerminalSession, call:loadSessions, call:useEffect | dep: react, ../api/terminal
|
||||
- use-theme.ts | React hook that fetches user theme preference on mount and applies it to the document root element via data-theme attribute | exp: func:useTheme(), call:useEffect, call:getUserConfig, call:document.documentElement.removeAttribute, call:document.documentElement.setAttribute | dep: react, ../api/settings
|
||||
- use-tool-workshop.ts | React custom hook that manages state and operations for a tool workshop UI, including CRUD operations for tool types and tool definitions with form handling and validation. | exp: useToolWorkshop | dep: react, ../utils/errors, ../api/tool-types, ../api/tool-definitions, ../components/features/tool-workshop/ToolTypeEditorPanel
|
||||
- use-tool-workshop.ts | Custom React hook that manages state and operations for a tool workshop UI, including CRUD operations for tool types and tool definitions with form handling and validation. | exp: useToolWorkshop | dep: react, ../utils/errors, ../api/tool-types, ../api/tool-definitions, ../components/features/tool-workshop/ToolTypeEditorPanel
|
||||
- use-virtual-keyboard.ts | React hook that detects virtual keyboard open/close state and measures its height on mobile devices | exp: func:useVirtualKeyboard(), call:useState, call:useCallback, call:setState, call:useEffect, call:visualViewport.addEventListener, call:window.addEventListener, call:updateKeyboardState, call:visualViewport.removeEventListener, call:window.removeEventListener | dep: react
|
||||
- use-workspace-actions.ts | Provides a React hook that encapsulates workspace CRUD operations with loading state management and user confirmation dialogs for destructive actions. | exp: UseWorkspaceActionsResult, func:useWorkspaceActions() → UseWorkspaceActionsResult, call:useState, call:useCallback, call:createWorkspace, call:setLoadingId, call:deleteWorkspace, call:onRefresh, call:window.confirm, call:instances.map((i) => `- ${i.name}`).join, call:syncWorkspace, call:updateWorkspace, raise:err | dep: react, ../api/workspaces, ../types/workspace
|
||||
- use-workspace-files.ts | Custom React hook for managing workspace file operations including listing, loading, saving, and navigating files. | exp: UseWorkspaceFilesResult, func:useWorkspaceFiles(workspaceId: string) → UseWorkspaceFilesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceFiles, call:setEntries, call:setCurrentPath, call:setContent, call:getWorkspaceFileContent, call:saveWorkspaceFile, call:refresh, call:useEffect | dep: react, ../api/workspace-files
|
||||
@@ -31,7 +31,7 @@ A collection of custom React hooks providing reusable state management, API inte
|
||||
- use-workspace-instances.ts | Custom React hook for managing workspace instances with CRUD operations, loading states, and error handling. | exp: UseWorkspaceInstancesResult, func:useWorkspaceInstances(workspaceId: string) → UseWorkspaceInstancesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceInstances, call:setInstances, call:createWorkspaceInstance, call:refresh, call:useEffect | dep: react, ../api/workspace-instances, ../api/sessions
|
||||
- use-workspaces.ts | Custom React hook that fetches and manages workspace data with loading and error states. | exp: UseWorkspacesResult, func:useWorkspaces(projectId: string, repoId: string) → UseWorkspacesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaces, call:listAllWorkspaces, call:setWorkspaces, call:useEffect, call:refresh | dep: react, ../api/workspaces, ../types/workspace
|
||||
## arch
|
||||
Follows a feature-based composition pattern where each hook encapsulates a specific domain concern (data fetching, CRUD operations, terminal/session management, UI interactions), often combining React state with API calls, side effects, and provider context integration.
|
||||
Follows a feature-based composition pattern where each hook is a self-contained unit managing specific concerns (data fetching, CRUD operations, lifecycle management, UI behaviors) using React primitives (useState, useEffect, useCallback) with consistent patterns for loading/error states, optimistic updates, and cleanup; hooks are granular and single-responsibility, often wrapping TanStack Query or direct API calls, with some hooks providing imperative controls and others integrating with browser APIs (SSE, viewport, keyboard, theme).
|
||||
## tags
|
||||
call:set, call:use, workspace, react, state, terminal, api, callback
|
||||
## symbols
|
||||
|
||||
@@ -151,13 +151,13 @@ export const useToolWorkshop = () => {
|
||||
resetToolTypeForm();
|
||||
};
|
||||
|
||||
const handleToolTypeSubmit = async (e?: React.FormEvent) => {
|
||||
const handleToolTypeSubmit = async (e?: React.FormEvent): Promise<boolean> => {
|
||||
e?.preventDefault();
|
||||
setToolTypeError(null);
|
||||
|
||||
if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) {
|
||||
setToolTypeError("Name and display name are required");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -166,7 +166,7 @@ export const useToolWorkshop = () => {
|
||||
isNaN(Number(toolTypeForm.default_port)))
|
||||
) {
|
||||
setToolTypeError("Default port is required and must be a number");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toolTypeForm.definition_type !== "manifest") {
|
||||
@@ -179,13 +179,13 @@ export const useToolWorkshop = () => {
|
||||
setToolTypeError(
|
||||
`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`,
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
} else if (!manifestData) {
|
||||
setToolTypeError(
|
||||
"Manifest data is required for manifest definition type",
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const variables = toolTypeForm.required_variables
|
||||
@@ -307,8 +307,10 @@ export const useToolWorkshop = () => {
|
||||
setToolTypeDirty(false);
|
||||
}
|
||||
await loadData();
|
||||
return true;
|
||||
} catch (err) {
|
||||
setToolTypeError(extractErrorMessage(err));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/pages
|
||||
|
||||
## role
|
||||
Contains top-level page components that serve as route endpoints for the web application's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations.
|
||||
Contains top-level React page components that render the main views of the web application, each handling a specific domain area (workspaces, projects, git, settings, etc.) with responsive mobile/desktop layouts.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: apps/web/src/pages
|
||||
index: apps/web/src/pages/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains top-level page components that serve as route endpoints for the web application's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations.
|
||||
Contains top-level React page components that render the main views of the web application, each handling a specific domain area (workspaces, projects, git, settings, etc.) with responsive mobile/desktop layouts.
|
||||
## files
|
||||
- ConfigProfilesPage.tsx | Renders a responsive configuration profiles management page with sidebar list and editor panel for desktop, and a dedicated mobile view for creating, editing, and managing config profiles. | exp: ConfigProfilesPage | dep: react, ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-config-profiles, ../components/features/config-profiles/ConfigProfileListSidebar, ../components/features/config-profiles/ConfigProfileEditorPanel, ../components/features/config-profiles/ConfigProfilesMobileView
|
||||
- DashboardPage.test.tsx | Unit tests for the DashboardPage component verifying overview loading and error retry behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./DashboardPage, ../state/sessions, DashboardPage, SessionsProvider
|
||||
@@ -12,22 +12,22 @@ Contains top-level page components that serve as route endpoints for the web app
|
||||
- GitHistoryPage.tsx | Renders a Git commit history page with branch selection, commit list with graph visualization, and a detail panel showing commit metadata, stats, and diffs. | exp: GitHistoryPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../hooks/use-async-data
|
||||
- GitRepositoriesPage.tsx | Displays and manages a project's Git repositories with CRUD operations including listing, creating, navigating to history, and deleting with confirmation | exp: GitRepositoriesPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../components/features/project/repository-create-dialog, ../hooks/use-async-data
|
||||
- PlaceholderPage.tsx | Exports three simple React page components (PlaceholderPage, NotFoundPage, LoginRedirectPage) for a frontend scaffold. | exp: PlaceholderPage, NotFoundPage, LoginRedirectPage | dep: ../components/icon, React
|
||||
- ProfilePage.tsx | A React component that displays and allows editing of a user profile, including name, email, and avatar upload with validation. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../state/auth, ../hooks/use-async-data
|
||||
- ProfilePage.tsx | Renders a user profile page that supports viewing, editing, and saving profile data including name, email, and avatar upload, with responsive mobile/desktop layouts. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../components/features/profile/ProfileMobileView, ../state/auth, ../hooks/use-async-data, ../hooks/use-mobile-viewport
|
||||
- ProjectSettingsPage.tsx | Renders a project settings page with tabbed navigation for general settings, repositories, and members, including project data fetching, editing, and deletion capabilities. | exp: ProjectSettingsPage | dep: react, react-router-dom, ../components/features/settings/settings-tab-layout, ../components/features/project/repositories-settings-tab, ../api/client, ../types
|
||||
- ProjectsPage.test.tsx | Unit tests for the ProjectsPage component covering loading, empty, error, create, edit, and delete states with API mocking. | dep: @testing-library/react, react-router-dom, vitest, ./ProjectsPage, ../api/projects, ProjectsPage
|
||||
- ProjectsPage.tsx | Renders a responsive projects management page with separate mobile and desktop layouts, supporting project CRUD operations, repository management, and workspace actions. | exp: ProjectsPage | dep: react, ../components/data-states, ../components/icon, ../hooks/use-mobile-viewport, ../components/features/project/ProjectCard, ../components/features/project/ProjectDialog, ../components/features/project/repository-create-dialog, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-fab, ../hooks/use-projects, ../types
|
||||
- ProjectsPage.tsx | Renders a responsive projects management page with separate mobile and desktop layouts, supporting project CRUD operations, repository management, and workspace actions. | exp: ProjectsPage | dep: react, ../components/data-states, ../components/icon, ../hooks/use-mobile-viewport, ../components/features/project/ProjectCard, ../components/features/project/ProjectDialog, ../components/features/project/repository-create-dialog, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-fab, ../hooks/use-projects, ../types, use-mobile-viewport, use-projects, data-states, icon, ProjectCard, ProjectDialog, RepositoryCreateDialog, MobileListView, MobileFAB
|
||||
- SessionsPage.tsx | Renders a sessions management page that displays, polls health for, and handles CRUD operations on development environment sessions with dirty delete confirmation. | exp: SessionsPage | dep: react, ../api/sessions, ../api/settings, ../components/data-states, ../components/features/session/session-list, ../components/features/session/session-card, ../hooks/use-instance-actions, ../state/sessions
|
||||
- SettingsPage.tsx | A React settings page component that loads, displays, and manages user configuration with tabbed navigation and nested outlet for child routes. | exp: SettingsPage | dep: react, react-router-dom, ../api/settings, ../components/data-states, ../hooks/use-async-data, ../components/features/settings/GeneralSettingsTab
|
||||
- SshKeysPage.tsx | React page component for managing SSH keys including generation, listing, signing, verification, and deletion | exp: SSHKeysPage | dep: react-router-dom, ../components/data-states, ../hooks/use-ssh-keys, ../components/features/ssh-keys/SSHKeyCreateForm, ../components/features/ssh-keys/SSHKeyList
|
||||
- TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView
|
||||
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with sidebar/editor layout for desktop and tabbed mobile view for managing tool types | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
|
||||
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with sidebar navigation and editor panel for managing tool types, adapting layout for mobile and desktop viewports. | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, tool-workshop components
|
||||
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
|
||||
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tab-based navigation for files, git, tools, and settings panels, with mobile-responsive layout. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../hooks/use-workspaces, ../hooks/use-mobile-viewport, ../components/features/workspace/workspace-detail-header, ../components/features/workspace/workspace-tab-bar, ../components/features/workspace/workspace-file-panel, ../components/features/workspace/workspace-git-panel, ../components/features/workspace/workspace-tools-panel, ../components/features/workspace/workspace-settings-panel, use-workspaces, use-mobile-viewport, workspace-detail-header, workspace-tab-bar, workspace-file-panel, workspace-git-panel, workspace-tools-panel, workspace-settings-panel
|
||||
- WorkspacesPage.tsx | Renders a responsive workspaces management page with separate mobile and desktop layouts, supporting workspace listing, creation, deletion, sync, and tool launching. | exp: func:WorkspacesPage(), call:useMobileViewport, call:useState, call:useWorkspaces, call:useWorkspaceActions, call:actions.delete, call:actions.sync, call:setMobileView, call:refresh, call:setStartWorkspace, call:handleDelete, call:setSelectedWorkspace, call:workspaces.map, call:e.stopPropagation, call:setShowCreate | dep: react, ../components/icon, ../hooks/use-mobile-viewport, ../hooks/use-workspaces, ../hooks/use-workspace-actions, ../components/features/workspace/workspace-card, ../components/features/workspace/workspace-create-form, ../components/features/mobile/mobile-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
|
||||
- WorkspacesPage.tsx | Renders a responsive workspaces management page with mobile and desktop views supporting listing, creating, viewing details, syncing, deleting, and starting tools for workspaces. | exp: func:WorkspacesPage(), call:useMobileViewport, call:useState, call:useWorkspaces, call:useWorkspaceActions, call:actions.delete, call:actions.sync, call:setMobileView, call:refresh, call:setStartWorkspace, call:handleDelete, call:setSelectedWorkspace, call:workspaces.map, call:e.stopPropagation, call:setShowCreate | dep: react, ../components/icon, ../hooks/use-mobile-viewport, ../hooks/use-workspaces, ../hooks/use-workspace-actions, ../components/features/workspace/workspace-card, ../components/features/workspace/workspace-create-form, ../components/features/mobile/mobile-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
|
||||
## arch
|
||||
Follows a page-based routing architecture where each file maps to a URL route, using responsive design patterns with explicit mobile/desktop view branching, compound component layouts (sidebar/editor, tabbed panels), polling for real-time data, and direct API integration within page components rather than abstracted service layers.
|
||||
Follows a page-based routing architecture where each file corresponds to a route; uses responsive design patterns with explicit mobile/desktop view branching, tabbed navigation for complex pages, polling for real-time data, and direct API integration with loading/error states rather than a centralized state management layer.
|
||||
## tags
|
||||
page, components, workspace, features, react, mobile, settings, hooks
|
||||
page, components, workspace, features, mobile, react, hooks, settings
|
||||
## symbols
|
||||
- WorkspaceDetailPage
|
||||
- WorkspacesPage
|
||||
|
||||
@@ -3,13 +3,16 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
|
||||
import { ErrorState, LoadingState } from "../components/data-states";
|
||||
import { Icon } from "../components/icon";
|
||||
import { ProfileMobileView } from "../components/features/profile/ProfileMobileView";
|
||||
import { useAuth } from "../state/auth";
|
||||
import { useAsyncData } from "../hooks/use-async-data";
|
||||
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
||||
import type { UserProfile } from "../api/profile";
|
||||
|
||||
type ProfileStatus = "loading" | "ready" | "error" | "saving";
|
||||
|
||||
export const ProfilePage = () => {
|
||||
const isMobile = useMobileViewport();
|
||||
const { refreshSession } = useAuth();
|
||||
const { data: profile, status: loadStatus, reload } = useAsyncData<UserProfile>(getProfile, []);
|
||||
const [displayStatus, setDisplayStatus] = useState<ProfileStatus>("loading");
|
||||
@@ -97,7 +100,23 @@ export const ProfilePage = () => {
|
||||
{displayStatus === "error" && <ErrorState message="Failed to load profile" onRetry={reload} />}
|
||||
|
||||
{(displayStatus === "ready" || displayStatus === "saving") && profile && (
|
||||
<div className="card stack">
|
||||
isMobile ? (
|
||||
<ProfileMobileView
|
||||
profile={profile}
|
||||
name={name}
|
||||
email={email}
|
||||
error={error}
|
||||
isSaving={displayStatus === "saving"}
|
||||
avatarUrl={avatarUrl}
|
||||
fileInputRef={fileInputRef}
|
||||
onNameChange={setName}
|
||||
onEmailChange={setEmail}
|
||||
onAvatarButtonClick={() => fileInputRef.current?.click()}
|
||||
onAvatarChange={handleAvatarChange}
|
||||
onSave={() => void handleSave()}
|
||||
/>
|
||||
) : (
|
||||
<div className="card stack">
|
||||
<div className="profile-avatar-section">
|
||||
<div className="avatar-preview">
|
||||
{avatarUrl ? (
|
||||
@@ -178,7 +197,7 @@ export const ProfilePage = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -57,12 +57,20 @@ export const ToolWorkshopPage = () => {
|
||||
mobileView={mobileView}
|
||||
isCreating={isCreating}
|
||||
toolTypeForm={toolTypeForm}
|
||||
manifestData={manifestData}
|
||||
manifestDefinitionId={manifestDefinitionId}
|
||||
baseDefinitions={baseDefinitions}
|
||||
toolTypeError={toolTypeError}
|
||||
toolTypeDirty={toolTypeDirty}
|
||||
onViewChange={setMobileView}
|
||||
onSelect={handleSelectToolType}
|
||||
onCreate={handleCreateNew}
|
||||
onDelete={handleDeleteToolType}
|
||||
onFormChange={handleFormChange}
|
||||
onManifestChange={(m) => {
|
||||
setManifestData(m);
|
||||
setToolTypeDirty(true);
|
||||
}}
|
||||
onSubmit={handleToolTypeSubmit}
|
||||
onCancel={() => {
|
||||
if (toolTypeDirty) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/styles
|
||||
|
||||
## role
|
||||
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, design tokens, utility classes, and component-specific styles.
|
||||
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, theme tokens, syntax highlighting, and utility classes.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,14 +4,14 @@ dir: apps/web/src/styles
|
||||
index: apps/web/src/styles/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, design tokens, utility classes, and component-specific styles.
|
||||
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, theme tokens, syntax highlighting, and utility classes.
|
||||
## files
|
||||
- global.css | Defines global CSS styles for a web application shell layout, navigation, cards, forms, dialogs, settings pages, and responsive design patterns. | dep: CSS custom properties (CSS variables: --border, --panel, --brand, --muted, --ink, --bg, --danger, --success, --warning, --font-size-xs, --font-size-sm, --space-2, --space-3, --space-4, --space-5)
|
||||
- syntax-highlight.css | Stylesheet for a syntax highlighting component with toolbar, line numbers, code display, and Prism.js theme integration | dep: Prism.js
|
||||
- tokens.css | Defines a comprehensive CSS design token system with light/dark themes, spacing scales, breakpoints, and fluid typography for a web application.
|
||||
- utilities.css | Provides responsive CSS utility classes and component-specific styles for a web application featuring terminals, dialogs, tables, forms, and navigation with mobile-first breakpoints | dep: CSS custom properties (variables like --space-*, --border, --bg, --brand, --muted, --success, --danger, --text-xs, --text-sm), xterm.js (terminal library)
|
||||
- utilities.css | Provides responsive CSS utility classes and component-specific styles for a web application featuring terminals, dialogs, tables, forms, and navigation. | dep: CSS custom properties (variables), xterm.js (terminal integration)
|
||||
## arch
|
||||
CSS custom properties (variables) based theming system with light/dark mode support, mobile-first responsive breakpoints, fluid typography scales, utility-first class patterns, and modular separation of concerns across tokens, utilities, global layouts, and component-specific styles.
|
||||
CSS custom properties-based design token architecture with theme-aware variables, modular separation of concerns across global/base/syntax/utility layers, and responsive breakpoint system with fluid typography scaling.
|
||||
## tags
|
||||
space, global, css, web, application, syntax, defines, styles
|
||||
## symbols
|
||||
|
||||
@@ -2109,6 +2109,166 @@ a.nav-item,
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Mobile Page */
|
||||
.mobile-page {
|
||||
padding: var(--space-2);
|
||||
padding-bottom: calc(var(--space-4) + 64px);
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Mobile Edit View */
|
||||
.mobile-edit-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.mobile-edit-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.mobile-edit-cancel,
|
||||
.mobile-edit-save {
|
||||
min-height: 44px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: 10px;
|
||||
font: inherit;
|
||||
font-size: 0.9375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-edit-cancel {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.mobile-edit-save {
|
||||
background: var(--brand);
|
||||
border: 1px solid var(--brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mobile-edit-save:disabled,
|
||||
.mobile-edit-cancel:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mobile-edit-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mobile-edit-form {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-3);
|
||||
padding-bottom: calc(var(--space-4) + 64px);
|
||||
}
|
||||
|
||||
.mobile-edit-field {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.mobile-edit-field-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-edit-input,
|
||||
.mobile-edit-textarea,
|
||||
.mobile-edit-select {
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.mobile-edit-textarea {
|
||||
resize: vertical;
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.mobile-edit-code {
|
||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||
Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.mobile-edit-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-edit-checkbox input {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
accent-color: var(--brand);
|
||||
}
|
||||
|
||||
/* Mobile Profile */
|
||||
.profile-mobile-avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-6) var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.profile-mobile-avatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
border: 2px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-mobile-avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.profile-mobile-avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* Mobile Page Header */
|
||||
.mobile-page-header {
|
||||
display: flex;
|
||||
@@ -2179,13 +2339,119 @@ a.nav-item,
|
||||
|
||||
/* Mobile form group (inline fields) */
|
||||
.mobile-form-group {
|
||||
padding: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.mobile-form-input,
|
||||
.mobile-form-select,
|
||||
.mobile-form-textarea {
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.mobile-form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.mobile-form-code {
|
||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||
Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.mobile-form-select {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.mobile-form-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-form-checkbox {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
min-width: 22px;
|
||||
min-height: 22px;
|
||||
accent-color: var(--brand);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-form-help {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--muted);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.mobile-form-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
background: var(--danger-light);
|
||||
color: var(--danger);
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
margin: var(--space-2) 0;
|
||||
}
|
||||
|
||||
.mobile-discard-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
/* Manifest editor responsive overrides inside mobile forms */
|
||||
.mobile-manifest-editor-wrapper .card {
|
||||
padding: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.mobile-manifest-editor-wrapper .row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-manifest-editor-wrapper .form-group,
|
||||
.mobile-manifest-editor-wrapper input,
|
||||
.mobile-manifest-editor-wrapper select,
|
||||
.mobile-manifest-editor-wrapper textarea {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-manifest-editor-wrapper input[style*="width:"] {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Ensure minimum touch targets on mobile */
|
||||
button,
|
||||
a,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Mobile Tool Workshop & Profile UI Rework
|
||||
|
||||
## Goal
|
||||
|
||||
Completely rework the mobile experience for the **Tool Workshop** (tool configuration) and **Profile** (user profile configuration) pages so they are usable on small viewports while preserving all existing desktop functionality.
|
||||
|
||||
## Scope
|
||||
|
||||
- Rework `ToolWorkshopMobileView` to expose every field and action available on the desktop editor, including manifest definition support.
|
||||
- Add a new `ProfileMobileView` and update `ProfilePage` to render it on small viewports.
|
||||
- Keep desktop layouts and behavior unchanged.
|
||||
- No backend API changes.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- On viewports narrower than 768 px, the Tool Workshop page renders a mobile-friendly list/detail/edit flow.
|
||||
- Tool type creation and editing on mobile support: definition type selection (Compose/Dockerfile/Manifest), manifest editor, interface type, conditional port, startup command, readiness probe, required variables, and all existing validation.
|
||||
- On viewports narrower than 768 px, the Profile page renders a mobile-friendly form with avatar upload, name/email editing, validation, and save feedback.
|
||||
- All existing desktop functionality remains intact.
|
||||
- `npm run typecheck` and `npm run lint` pass in `apps/web`.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Backend changes.
|
||||
- Changes to other pages (sessions, settings, etc.).
|
||||
- New features beyond the existing desktop capabilities.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Tasks: Mobile Tool Workshop & Profile UI Rework
|
||||
|
||||
- [x] Inspect existing mobile components, desktop editors, and hooks.
|
||||
- [x] Rework `ToolWorkshopMobileView` to support full desktop functionality (definition type, manifest editor, conditional port, startup command, readiness probe, required variables, validation).
|
||||
- [x] Add responsive CSS for manifest editor and tool-workshop mobile forms.
|
||||
- [x] Create `ProfileMobileView` component with mobile-friendly avatar/name/email form.
|
||||
- [x] Update `ProfilePage` to render `ProfileMobileView` on mobile viewports.
|
||||
- [x] Run `npm run typecheck` and `npm run lint` in `apps/web` and fix issues.
|
||||
- [x] Update project maps for changed files.
|
||||
Reference in New Issue
Block a user