feat: redesign mobile Config Profiles detail, preview, and edit pages
- Rewrite ConfigProfilesMobileView to match desktop functionality: full detail view with all fields, preview action showing resolved profile, and edit view with project/tool selects, includes, environment variables, runtime hints, files, mounts with nested files, and git mounts. - Update useConfigProfiles.handleSubmit to return boolean success. - Update ConfigProfilesPage to pass required state and callbacks. - Add mobile-specific CSS for config profile forms, includes, mount/file cards, and preview panels. - Allow MobileDetailView to render extra children. Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed) Refs: openspec/changes/mobile-config-profiles-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 for a self-hosted project management and git repository platform with OAuth2 authentication.
|
||||
A self-hosted, containerized project management and Git repository platform with OAuth2 SSO integration, designed for deployment via Docker Compose with or without Traefik reverse proxy.
|
||||
## 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 for a self-hosted project management and git repository platform with OAuth2 authentication.
|
||||
Infrastructure and deployment configuration for a self-hosted project management and git repository platform with multi-service Docker orchestration.
|
||||
## 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 for a self-hosted project management
|
||||
- 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
|
||||
Containerized microservices architecture using Docker Compose with PostgreSQL/Redis data layer, Traefik reverse proxy for TLS/ingress, and environment-driven configuration across frontend/backend services.
|
||||
Docker Compose-based microservices with PostgreSQL/Redis backend, Traefik reverse proxy, OAuth2 SSO integration, and environment-driven configuration management.
|
||||
## tags
|
||||
docker, redis, git, application, postgresql, compose, traefik, project
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps
|
||||
|
||||
## role
|
||||
Contains the main deployable application entry points and top-level configurations for the project.
|
||||
Contains the main deployable application entry points and executable 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 entry points and top-level configurations for the project.
|
||||
Contains the main application entry points and executable modules for the project.
|
||||
## files
|
||||
## arch
|
||||
Monorepo-style app separation with distinct application boundaries, likely sharing common libraries/modules while maintaining independent deployment artifacts.
|
||||
Modular application structure with separate deployable units, typically following framework-specific conventions (e.g., Next.js, React, or other app-based architectures).
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web
|
||||
|
||||
## role
|
||||
Frontend web application for a React-based developer workspace with code editing, terminal, and routing capabilities.
|
||||
Frontend web application for a React-based "headquarter" project providing code editing, terminal, and routing capabilities via Vite/TypeScript.
|
||||
## 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 for a React-based developer workspace with code editing, terminal, and routing capabilities.
|
||||
Frontend web application for a React-based "headquarter" project providing code editing, terminal, and routing capabilities via Vite/TypeScript.
|
||||
## 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 for a React-based developer workspace with code editing
|
||||
- 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 Vite-based SPA with TypeScript, nginx reverse proxy serving, multi-stage Docker containerization, and client-side routing.
|
||||
Modern SPA architecture using Vite bundler with React, TypeScript, nginx static serving, multi-stage Docker containerization, and client-side routing with optimized asset caching.
|
||||
## tags
|
||||
react, eslint, vite, typescript, dom, application, nginx, web
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src
|
||||
|
||||
## role
|
||||
Entry point and core infrastructure for a React web application handling authentication, routing, and domain type definitions.
|
||||
Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend.
|
||||
## 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
|
||||
Entry point and core infrastructure for a React web application handling authentication, routing, and domain type definitions.
|
||||
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||
## 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
|
||||
Provider pattern wrapping React Router with nested route layouts, protected route guards, and centralized TypeScript type definitions for session-based auth and workspace/project domain models.
|
||||
Modular React SPA with provider-pattern state management, nested routing with authentication guards, and centralized TypeScript domain modeling for multi-tenant workspace/project hierarchy.
|
||||
## tags
|
||||
pages, styles, css, router, react, session, dom, project
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components
|
||||
|
||||
## role
|
||||
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.
|
||||
Provides reusable, accessible UI primitives and layout components for a React web application, including shell structure, data visualization states, iconography, code editing/display, authentication guards, and toast notifications.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: apps/web/src/components
|
||||
index: apps/web/src/components/.pi-map.index.md
|
||||
|
||||
## 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 UI components, layout primitives, and auth guards for the web application's React-based frontend.
|
||||
## files
|
||||
- app-shell.tsx | Renders the main application shell layout with navigation, header, session management, and mobile-responsive behavior for a React Router-based SPA. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
|
||||
- code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
|
||||
@@ -17,7 +17,7 @@ Provides reusable, accessible UI components and utilities for rendering the appl
|
||||
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
|
||||
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type
|
||||
## arch
|
||||
Component-based React architecture with functional components, composition patterns, separation of concerns (presentation vs logic), and test coverage for critical utilities; integrates third-party libraries (Phosphor icons, react-simple-code-editor) and React Router for SPA navigation.
|
||||
Component-based React architecture with functional components, composition patterns, separation of concerns (presentation, auth, feedback), and test colocation.
|
||||
## tags
|
||||
toast, icon, react, state, code, loading, event, editor
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features
|
||||
|
||||
## role
|
||||
Reusable UI feature components that compose domain-specific functionality for the web application
|
||||
Reusable UI feature components for the web application that compose domain-specific user interface elements.
|
||||
## parent
|
||||
index: apps/web/src/components/.pi-map.index.md
|
||||
map: apps/web/src/components/.pi-map.md
|
||||
|
||||
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
|
||||
index: apps/web/src/components/features/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
||||
Reusable UI feature components for the web application that compose domain-specific user interface elements.
|
||||
## files
|
||||
## arch
|
||||
Feature-based component organization with domain-specific UI building blocks following React composition patterns, likely co-located with related hooks, utilities, or sub-components for each feature area.
|
||||
Modular feature-based component organization with domain-specific grouping, likely following atomic design or container/presentational patterns with shared component composition.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features/config-profiles
|
||||
|
||||
## role
|
||||
Provides a complete UI subsystem for managing configuration profiles with desktop and mobile views, supporting CRUD operations, profile relationships, and advanced configuration options.
|
||||
Provides a complete UI subsystem for managing container configuration profiles across desktop and mobile form factors.
|
||||
## 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/config-profiles
|
||||
index: apps/web/src/components/features/config-profiles/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides a complete UI subsystem for managing configuration profiles with desktop and mobile views, supporting CRUD operations, profile relationships, and advanced configuration options.
|
||||
Provides a complete UI subsystem for managing container configuration profiles across desktop and mobile form factors.
|
||||
## files
|
||||
- ConfigProfileEditorPanel.tsx | React component that renders a form-based editor panel for creating and editing configuration profiles with support for includes, environment variables, runtime hints, files, and mounts. | exp: ConfigProfileEditorPanel | dep: ../../icon, ../git/git-mount-editor, ../../../api/config-profiles, ../../../types, ../../../api/tool-types, React, Icon, GitMountEditor, ConfigProfile, CreateConfigProfileRequest, ResolvedProfile, ProjectWithRepos, ToolType
|
||||
- ConfigProfileListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting configuration profiles with visual indicators for default status, scope, and includes. | exp: ConfigProfileListSidebar | dep: ../../icon, ../../../api/config-profiles, Icon, ConfigProfile
|
||||
- ConfigProfilesMobileView.tsx | Renders a mobile-responsive CRUD interface for managing configuration profiles with list, detail, and edit views. | exp: ConfigProfilesMobileView | dep: ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../../icon, ../../../api/config-profiles, MobileListView, MobileDetailView, MobileEditView, MobileFAB, Icon, ConfigProfile, CreateConfigProfileRequest
|
||||
- ConfigProfilesMobileView.tsx | Renders a mobile-optimized view for managing configuration profiles with list, detail, edit, and preview states. | exp: ConfigProfilesMobileView | dep: react, ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../git/git-mount-editor, ../../icon, ../../../api/config-profiles, ../../../types, ../../../api/tool-types, mobile-list-view, mobile-detail-view, mobile-edit-view, mobile-fab, git-mount-editor, icon, config-profiles API types, project types, tool-types API
|
||||
## arch
|
||||
Compound component architecture with panel/sidebar split for desktop, dedicated mobile view with state-driven routing, shared state management across list/editor views, and form-based configuration with nested array/object handling for includes, env vars, files, and mounts.
|
||||
Compound component architecture with state-driven mobile adaptation, separating list navigation from editing concerns with explicit responsive breakpoint handling.
|
||||
## tags
|
||||
config, mobile, profiles, profile, view, editor, icon, list
|
||||
mobile, config, view, profiles, editor, profile, list, icon
|
||||
## symbols
|
||||
- ConfigProfileEditorPanel
|
||||
- ConfigProfileListSidebar
|
||||
|
||||
@@ -1,35 +1,86 @@
|
||||
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 { GitMountEditor } from "../git/git-mount-editor";
|
||||
import { Icon } from "../../icon";
|
||||
import type { ConfigProfile, CreateConfigProfileRequest } from "../../../api/config-profiles";
|
||||
import type {
|
||||
ConfigProfile,
|
||||
CreateConfigProfileRequest,
|
||||
ResolvedProfile,
|
||||
} from "../../../api/config-profiles";
|
||||
import type { ProjectWithRepos } from "../../../types";
|
||||
import type { ToolType } from "../../../api/tool-types";
|
||||
|
||||
type MobileView = "list" | "detail" | "edit";
|
||||
type MobileView = "list" | "detail" | "edit" | "preview";
|
||||
|
||||
interface Props {
|
||||
profiles: ConfigProfile[];
|
||||
projects: ProjectWithRepos[];
|
||||
toolTypes: ToolType[];
|
||||
selectedProfile: ConfigProfile | null;
|
||||
mobileView: MobileView;
|
||||
isCreating: boolean;
|
||||
formData: CreateConfigProfileRequest;
|
||||
includedProfileIds: string[];
|
||||
availableProfiles: ConfigProfile[];
|
||||
previewData: ResolvedProfile | null;
|
||||
previewingId: string | null;
|
||||
saveStatus: "idle" | "saving" | "saved" | "error";
|
||||
error: string | null;
|
||||
onViewChange: (view: MobileView) => void;
|
||||
onSelect: (profile: ConfigProfile) => void;
|
||||
onCreate: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
onFormChange: <K extends keyof CreateConfigProfileRequest>(key: K, value: CreateConfigProfileRequest[K]) => void;
|
||||
onSubmit: () => void;
|
||||
onFormChange: <K extends keyof CreateConfigProfileRequest>(
|
||||
key: K,
|
||||
value: CreateConfigProfileRequest[K],
|
||||
) => void;
|
||||
onSubmit: (e?: React.FormEvent) => Promise<boolean>;
|
||||
getScopeLabel: (profile: ConfigProfile) => string;
|
||||
getIncludedProfile: (id: string) => ConfigProfile | undefined;
|
||||
onAddInclude: (id: string) => void;
|
||||
onRemoveInclude: (index: number) => void;
|
||||
onAddEnvVar: () => void;
|
||||
onUpdateEnvVar: (oldKey: string, newKey: string, value: string) => void;
|
||||
onRemoveEnvVar: (key: string) => void;
|
||||
onAddFile: () => void;
|
||||
onUpdateFile: (oldPath: string, newPath: string, content: string) => void;
|
||||
onRemoveFile: (path: string) => void;
|
||||
onAddMount: () => void;
|
||||
onUpdateMount: (
|
||||
index: number,
|
||||
updates: Partial<ConfigProfile["mounts"][0]>,
|
||||
) => void;
|
||||
onRemoveMount: (index: number) => void;
|
||||
onAddMountFile: (mountIndex: number) => void;
|
||||
onUpdateMountFile: (
|
||||
mountIndex: number,
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
content: string,
|
||||
) => void;
|
||||
onRemoveMountFile: (mountIndex: number, path: string) => void;
|
||||
onPreview: (id: string) => void;
|
||||
onClosePreview: () => void;
|
||||
}
|
||||
|
||||
export const ConfigProfilesMobileView = ({
|
||||
profiles,
|
||||
projects,
|
||||
toolTypes,
|
||||
selectedProfile,
|
||||
mobileView,
|
||||
isCreating,
|
||||
formData,
|
||||
includedProfileIds,
|
||||
availableProfiles,
|
||||
previewData,
|
||||
previewingId,
|
||||
saveStatus,
|
||||
error,
|
||||
onViewChange,
|
||||
onSelect,
|
||||
onCreate,
|
||||
@@ -37,12 +88,57 @@ export const ConfigProfilesMobileView = ({
|
||||
onFormChange,
|
||||
onSubmit,
|
||||
getScopeLabel,
|
||||
getIncludedProfile,
|
||||
onAddInclude,
|
||||
onRemoveInclude,
|
||||
onAddEnvVar,
|
||||
onUpdateEnvVar,
|
||||
onRemoveEnvVar,
|
||||
onAddFile,
|
||||
onUpdateFile,
|
||||
onRemoveFile,
|
||||
onAddMount,
|
||||
onUpdateMount,
|
||||
onRemoveMount,
|
||||
onAddMountFile,
|
||||
onUpdateMountFile,
|
||||
onRemoveMountFile,
|
||||
onPreview,
|
||||
onClosePreview,
|
||||
}: Props) => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const ok = await onSubmit();
|
||||
if (ok) {
|
||||
onViewChange(isCreating ? "list" : "detail");
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
if (selectedProfile) {
|
||||
onDelete(selectedProfile.id);
|
||||
onViewChange("list");
|
||||
}
|
||||
};
|
||||
|
||||
const projectName = (id?: string) =>
|
||||
projects.find((p) => p.id === id)?.name || id || "None";
|
||||
|
||||
const toolTypeName = (id?: string) =>
|
||||
toolTypes.find((t) => t.id === id)?.display_name || id || "None";
|
||||
|
||||
if (mobileView === "list") {
|
||||
return (
|
||||
<div className="mobile-page">
|
||||
<div className="mobile-page-header">
|
||||
<h1>Config Profiles</h1>
|
||||
<span className="muted">{profiles.length} profiles</span>
|
||||
</div>
|
||||
<MobileListView
|
||||
items={profiles.map((profile) => ({
|
||||
@@ -70,12 +166,146 @@ export const ConfigProfilesMobileView = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileView === "preview" && selectedProfile && previewData) {
|
||||
return (
|
||||
<div className="mobile-edit-view">
|
||||
<header className="mobile-edit-header">
|
||||
<button
|
||||
className="mobile-edit-cancel"
|
||||
onClick={() => onViewChange("detail")}
|
||||
type="button"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<h1 className="mobile-edit-title">Resolved Preview</h1>
|
||||
<div style={{ width: "4rem" }} />
|
||||
</header>
|
||||
<div className="mobile-edit-form">
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Profile</label>
|
||||
<p>{previewData.profile_name}</p>
|
||||
</div>
|
||||
|
||||
{previewData.included_profiles.length > 0 && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Included Profiles</label>
|
||||
<ul className="mobile-list-plain">
|
||||
{previewData.included_profiles.map((p) => (
|
||||
<li key={p.id}>{p.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(previewData.env_vars).length > 0 && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Environment Variables</label>
|
||||
<pre className="mobile-preview-code">
|
||||
{JSON.stringify(previewData.env_vars, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(previewData.runtime_hints).length > 0 && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Runtime Hints</label>
|
||||
<pre className="mobile-preview-code">
|
||||
{JSON.stringify(previewData.runtime_hints, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewData.mounts.length > 0 && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Mounts</label>
|
||||
{previewData.mounts.map((m, idx) => (
|
||||
<div key={idx} className="mobile-preview-item">
|
||||
<strong>
|
||||
{m.target} ({m.mode})
|
||||
</strong>
|
||||
{Object.keys(m.files).length > 0 && (
|
||||
<pre className="mobile-preview-code">
|
||||
{JSON.stringify(m.files, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewData.git_mounts.length > 0 && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Git Mounts</label>
|
||||
{previewData.git_mounts.map((m, idx) => (
|
||||
<div key={idx} className="mobile-preview-item">
|
||||
<strong>
|
||||
{m.remote_url}
|
||||
{m.branch ? ` @${m.branch}` : ""}
|
||||
</strong>
|
||||
{m.mappings.map((mapping, mi) => (
|
||||
<div
|
||||
key={mi}
|
||||
className="mobile-preview-mapping"
|
||||
>
|
||||
{mapping.source_path || "."} →{" "}
|
||||
{mapping.target_path}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(previewData.files).length > 0 && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Files</label>
|
||||
<pre className="mobile-preview-code">
|
||||
{JSON.stringify(previewData.files, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(previewData.overrides.env_vars).length > 0 && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Overrides</label>
|
||||
<pre className="mobile-preview-code">
|
||||
{JSON.stringify(previewData.overrides, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileView === "detail" && selectedProfile) {
|
||||
const fields = [
|
||||
{ label: "Name", value: selectedProfile.name },
|
||||
{ label: "Description", value: selectedProfile.description || "-" },
|
||||
{
|
||||
label: "Description",
|
||||
value: selectedProfile.description || null,
|
||||
},
|
||||
{ label: "Scope", value: getScopeLabel(selectedProfile) },
|
||||
{ label: "Default", value: selectedProfile.is_default ? "Yes" : "No" },
|
||||
{ label: "Project", value: projectName(selectedProfile.project_id || undefined) },
|
||||
{ label: "Tool Type", value: toolTypeName(selectedProfile.tool_type_id || undefined) },
|
||||
{
|
||||
label: "Default",
|
||||
value: selectedProfile.is_default,
|
||||
type: "boolean" as const,
|
||||
},
|
||||
{
|
||||
label: "Includes",
|
||||
value:
|
||||
selectedProfile.includes.length > 0
|
||||
? selectedProfile.includes
|
||||
.map(
|
||||
(inc) =>
|
||||
getIncludedProfile(inc.included_profile_id)?.name ||
|
||||
inc.included_profile_id,
|
||||
)
|
||||
.join(", ")
|
||||
: null,
|
||||
},
|
||||
{
|
||||
label: "Environment Variables",
|
||||
value:
|
||||
@@ -83,21 +313,40 @@ export const ConfigProfilesMobileView = ({
|
||||
? Object.entries(selectedProfile.env_vars)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(", ")
|
||||
: "-",
|
||||
: null,
|
||||
},
|
||||
{
|
||||
label: "Runtime Hints",
|
||||
value:
|
||||
Object.keys(selectedProfile.runtime_hints).length > 0
|
||||
? JSON.stringify(selectedProfile.runtime_hints, null, 2)
|
||||
: null,
|
||||
type: "code" as const,
|
||||
},
|
||||
{
|
||||
label: "Files",
|
||||
value:
|
||||
Object.keys(selectedProfile.files).length > 0
|
||||
? Object.keys(selectedProfile.files).join(", ")
|
||||
: null,
|
||||
},
|
||||
{
|
||||
label: "Mounts",
|
||||
value:
|
||||
selectedProfile.mounts.length > 0
|
||||
? selectedProfile.mounts.map((m) => `${m.target} (${m.mode})`).join(", ")
|
||||
: "-",
|
||||
? selectedProfile.mounts
|
||||
.map((m) => `${m.target} (${m.mode})`)
|
||||
.join(", ")
|
||||
: null,
|
||||
},
|
||||
{
|
||||
label: "Includes",
|
||||
label: "Git Mounts",
|
||||
value:
|
||||
selectedProfile.includes.length > 0
|
||||
? `${selectedProfile.includes.length} profile(s)`
|
||||
: "-",
|
||||
selectedProfile.git_mounts.length > 0
|
||||
? selectedProfile.git_mounts
|
||||
.map((m) => m.remote_url)
|
||||
.join(", ")
|
||||
: null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -111,8 +360,33 @@ export const ConfigProfilesMobileView = ({
|
||||
onSelect(selectedProfile);
|
||||
onViewChange("edit");
|
||||
}}
|
||||
onDelete={() => onDelete(selectedProfile.id)}
|
||||
/>
|
||||
onDelete={handleDeleteClick}
|
||||
>
|
||||
<div className="mobile-detail-actions-extra">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={previewingId === selectedProfile.id}
|
||||
onClick={() => {
|
||||
onClosePreview();
|
||||
void onPreview(selectedProfile.id);
|
||||
onViewChange("preview");
|
||||
}}
|
||||
>
|
||||
{previewingId === selectedProfile.id ? (
|
||||
<>
|
||||
<Icon name="loading" size="sm" />
|
||||
Previewing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="info" size="sm" />
|
||||
Preview
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</MobileDetailView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,96 +397,178 @@ export const ConfigProfilesMobileView = ({
|
||||
onCancel={() => {
|
||||
onViewChange(isCreating ? "list" : "detail");
|
||||
}}
|
||||
onSave={onSubmit}
|
||||
isSaving={saveStatus === "saving"}
|
||||
onSave={handleSave}
|
||||
isSaving={isSaving || saveStatus === "saving"}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>Name *</label>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label" htmlFor="cp-name">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
id="cp-name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => onFormChange("name", e.target.value)}
|
||||
placeholder="Profile name"
|
||||
placeholder="e.g., Development Environment"
|
||||
className="mobile-form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label" htmlFor="cp-description">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
id="cp-description"
|
||||
type="text"
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => onFormChange("description", e.target.value || undefined)}
|
||||
onChange={(e) =>
|
||||
onFormChange("description", e.target.value || undefined)
|
||||
}
|
||||
placeholder="Optional description"
|
||||
rows={3}
|
||||
className="mobile-form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Project</label>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label" htmlFor="cp-project">
|
||||
Project
|
||||
</label>
|
||||
<select
|
||||
id="cp-project"
|
||||
value={formData.project_id || ""}
|
||||
onChange={(e) => onFormChange("project_id", e.target.value || undefined)}
|
||||
onChange={(e) =>
|
||||
onFormChange("project_id", e.target.value || undefined)
|
||||
}
|
||||
className="mobile-form-select"
|
||||
>
|
||||
<option value="">Global (all projects)</option>
|
||||
<option value="">None (Global)</option>
|
||||
{projects.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Tool Type</label>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label" htmlFor="cp-tool">
|
||||
Tool Type
|
||||
</label>
|
||||
<select
|
||||
id="cp-tool"
|
||||
value={formData.tool_type_id || ""}
|
||||
onChange={(e) => onFormChange("tool_type_id", e.target.value || undefined)}
|
||||
onChange={(e) =>
|
||||
onFormChange("tool_type_id", e.target.value || undefined)
|
||||
}
|
||||
className="mobile-form-select"
|
||||
>
|
||||
<option value="">Any tool type</option>
|
||||
<option value="">None</option>
|
||||
{toolTypes.map((toolType) => (
|
||||
<option key={toolType.id} value={toolType.id}>
|
||||
{toolType.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<div className="mobile-form-group mobile-form-row">
|
||||
<label className="mobile-form-label mobile-form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_default || false}
|
||||
onChange={(e) => onFormChange("is_default", e.target.checked)}
|
||||
onChange={(e) =>
|
||||
onFormChange("is_default", e.target.checked)
|
||||
}
|
||||
className="mobile-form-checkbox"
|
||||
/>
|
||||
Default Profile
|
||||
Set as default for this scope
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Environment Variables</label>
|
||||
{Object.entries(formData.env_vars || {}).map(([key, value], index) => (
|
||||
<div key={index} style={{ display: "flex", gap: "0.5rem", marginBottom: "0.5rem" }}>
|
||||
<div className="mobile-form-section">
|
||||
<div className="mobile-form-section-header">
|
||||
<h4>Includes</h4>
|
||||
<span className="muted">{includedProfileIds.length} included</span>
|
||||
</div>
|
||||
{includedProfileIds.length === 0 ? (
|
||||
<p className="muted mobile-form-help">
|
||||
No profiles included. Add profiles to compose configurations.
|
||||
</p>
|
||||
) : (
|
||||
<div className="mobile-includes-list">
|
||||
{includedProfileIds.map((profileId, index) => {
|
||||
const profile = getIncludedProfile(profileId);
|
||||
if (!profile) return null;
|
||||
return (
|
||||
<div key={`${profileId}-${index}`} className="mobile-include-item">
|
||||
<span className="mobile-include-name">{profile.name}</span>
|
||||
<span className="mobile-include-scope">
|
||||
{getScopeLabel(profile)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button small"
|
||||
onClick={() => onRemoveInclude(index)}
|
||||
title="Remove include"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{availableProfiles.length > 0 && (
|
||||
<select
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
onAddInclude(e.target.value);
|
||||
e.target.value = "";
|
||||
}
|
||||
}}
|
||||
className="mobile-form-select"
|
||||
>
|
||||
<option value="">+ Add Include...</option>
|
||||
{availableProfiles.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name} ({getScopeLabel(p)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-section">
|
||||
<div className="mobile-form-section-header">
|
||||
<h4>Environment Variables</h4>
|
||||
</div>
|
||||
{Object.entries(formData.env_vars || {}).map(([key, value], idx) => (
|
||||
<div key={idx} className="mobile-key-value-row">
|
||||
<input
|
||||
type="text"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
const newEnvVars = { ...formData.env_vars };
|
||||
delete newEnvVars[key];
|
||||
newEnvVars[e.target.value] = value;
|
||||
onFormChange("env_vars", newEnvVars);
|
||||
}}
|
||||
placeholder="KEY"
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) =>
|
||||
onUpdateEnvVar(key, e.target.value, value)
|
||||
}
|
||||
placeholder="VAR_NAME"
|
||||
className="mobile-form-input"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const newEnvVars = { ...formData.env_vars };
|
||||
newEnvVars[key] = e.target.value;
|
||||
onFormChange("env_vars", newEnvVars);
|
||||
}}
|
||||
onChange={(e) =>
|
||||
onUpdateEnvVar(key, key, e.target.value)
|
||||
}
|
||||
placeholder="value"
|
||||
style={{ flex: 1 }}
|
||||
className="mobile-form-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newEnvVars = { ...formData.env_vars };
|
||||
delete newEnvVars[key];
|
||||
onFormChange("env_vars", newEnvVars);
|
||||
}}
|
||||
className="secondary-button"
|
||||
className="ghost-button small"
|
||||
onClick={() => onRemoveEnvVar(key)}
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
@@ -220,49 +576,158 @@ export const ConfigProfilesMobileView = ({
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => {
|
||||
onFormChange("env_vars", { ...formData.env_vars, "": "" });
|
||||
}}
|
||||
className="secondary-button mobile-add-button"
|
||||
onClick={onAddEnvVar}
|
||||
>
|
||||
<Icon name="add" size="sm" /> Add Variable
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Mounts</label>
|
||||
<div className="mobile-form-section">
|
||||
<div className="mobile-form-section-header">
|
||||
<h4>Runtime Hints</h4>
|
||||
</div>
|
||||
<textarea
|
||||
value={JSON.stringify(formData.runtime_hints || {}, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
onFormChange("runtime_hints", parsed);
|
||||
} catch {
|
||||
/* ignore invalid JSON while typing */
|
||||
}
|
||||
}}
|
||||
placeholder='{"start_command": "npm start"}'
|
||||
rows={4}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-section">
|
||||
<div className="mobile-form-section-header">
|
||||
<h4>Files</h4>
|
||||
</div>
|
||||
<p className="muted mobile-form-help">
|
||||
Relative paths written to the instance directory. Use Mounts below
|
||||
for absolute container paths.
|
||||
</p>
|
||||
{Object.entries(formData.files || {}).map(([path, content], idx) => (
|
||||
<div key={idx} className="mobile-file-card">
|
||||
<div className="mobile-file-header">
|
||||
<input
|
||||
type="text"
|
||||
value={path}
|
||||
onChange={(e) =>
|
||||
onUpdateFile(path, e.target.value, content)
|
||||
}
|
||||
placeholder="relative/path/to/file"
|
||||
className="mobile-form-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button small"
|
||||
onClick={() => onRemoveFile(path)}
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) =>
|
||||
onUpdateFile(path, path, e.target.value)
|
||||
}
|
||||
placeholder="File content"
|
||||
rows={3}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button mobile-add-button"
|
||||
onClick={onAddFile}
|
||||
>
|
||||
<Icon name="add" size="sm" /> Add File
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-section">
|
||||
<div className="mobile-form-section-header">
|
||||
<h4>Mounts</h4>
|
||||
</div>
|
||||
<p className="muted mobile-form-help">
|
||||
Bind directories into the container at absolute paths. Files are
|
||||
relative to the mount target.
|
||||
</p>
|
||||
{(formData.mounts || []).map((mount, index) => (
|
||||
<div key={index} style={{ display: "flex", gap: "0.5rem", marginBottom: "0.5rem" }}>
|
||||
<div key={index} className="mobile-mount-card">
|
||||
<div className="mobile-mount-header">
|
||||
<input
|
||||
type="text"
|
||||
value={mount.target}
|
||||
onChange={(e) => {
|
||||
const newMounts = [...(formData.mounts || [])];
|
||||
newMounts[index] = { ...mount, target: e.target.value };
|
||||
onFormChange("mounts", newMounts);
|
||||
}}
|
||||
placeholder="Target path"
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) =>
|
||||
onUpdateMount(index, { target: e.target.value })
|
||||
}
|
||||
placeholder="/target/path"
|
||||
className="mobile-form-input"
|
||||
/>
|
||||
<select
|
||||
value={mount.mode}
|
||||
onChange={(e) => {
|
||||
const newMounts = [...(formData.mounts || [])];
|
||||
newMounts[index] = { ...mount, mode: e.target.value as "ro" | "rw" };
|
||||
onFormChange("mounts", newMounts);
|
||||
}}
|
||||
style={{ width: "80px" }}
|
||||
onChange={(e) =>
|
||||
onUpdateMount(index, {
|
||||
mode: e.target.value as "ro" | "rw",
|
||||
})
|
||||
}
|
||||
className="mobile-form-select"
|
||||
>
|
||||
<option value="ro">Read</option>
|
||||
<option value="rw">Write</option>
|
||||
<option value="rw">Read/Write</option>
|
||||
<option value="ro">Read-Only</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newMounts = (formData.mounts || []).filter((_, i) => i !== index);
|
||||
onFormChange("mounts", newMounts);
|
||||
}}
|
||||
className="secondary-button"
|
||||
className="ghost-button small"
|
||||
onClick={() => onRemoveMount(index)}
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mobile-mount-files">
|
||||
{Object.entries(mount.files).map(([path, content], idx) => (
|
||||
<div key={idx} className="mobile-mount-file-row">
|
||||
<input
|
||||
type="text"
|
||||
value={path}
|
||||
onChange={(e) =>
|
||||
onUpdateMountFile(
|
||||
index,
|
||||
path,
|
||||
e.target.value,
|
||||
content,
|
||||
)
|
||||
}
|
||||
placeholder="relative/path"
|
||||
className="mobile-form-input"
|
||||
/>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) =>
|
||||
onUpdateMountFile(
|
||||
index,
|
||||
path,
|
||||
path,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="File content"
|
||||
rows={2}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button small"
|
||||
onClick={() =>
|
||||
onRemoveMountFile(index, path)
|
||||
}
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
@@ -270,14 +735,44 @@ export const ConfigProfilesMobileView = ({
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => {
|
||||
onFormChange("mounts", [...(formData.mounts || []), { target: "/", mode: "rw", files: {} }]);
|
||||
}}
|
||||
className="secondary-button small mobile-add-button"
|
||||
onClick={() => onAddMountFile(index)}
|
||||
>
|
||||
<Icon name="add" size="sm" /> Add File to Mount
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button mobile-add-button"
|
||||
onClick={onAddMount}
|
||||
>
|
||||
<Icon name="add" size="sm" /> Add Mount
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mobile-form-section">
|
||||
<GitMountEditor
|
||||
mounts={formData.git_mounts || []}
|
||||
onChange={(git_mounts) =>
|
||||
onFormChange("git_mounts", git_mounts)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(error || saveStatus === "error") && (
|
||||
<p className="mobile-form-error">
|
||||
<Icon name="warning" size="sm" /> {error || "Failed to save"}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{saveStatus === "saved" && (
|
||||
<div className="mobile-form-success">
|
||||
<Icon name="success" size="sm" />
|
||||
Profile saved successfully
|
||||
</div>
|
||||
)}
|
||||
</MobileEditView>
|
||||
);
|
||||
}
|
||||
@@ -286,6 +781,7 @@ export const ConfigProfilesMobileView = ({
|
||||
<div className="mobile-page">
|
||||
<div className="mobile-page-header">
|
||||
<h1>Config Profiles</h1>
|
||||
<span className="muted">{profiles.length} profiles</span>
|
||||
</div>
|
||||
<MobileListView
|
||||
items={profiles.map((profile) => ({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features/mobile
|
||||
|
||||
## role
|
||||
Provides mobile-optimized UI components for core application features including navigation, data views, forms, modals, and specialized terminal interfaces.
|
||||
Provides a complete set of mobile-optimized UI components for building responsive mobile interfaces including navigation, lists, detail/edit views, action sheets, and specialized terminal wrappers.
|
||||
## parent
|
||||
index: apps/web/src/components/features/.pi-map.index.md
|
||||
map: apps/web/src/components/features/.pi-map.md
|
||||
|
||||
@@ -4,22 +4,22 @@ dir: apps/web/src/components/features/mobile
|
||||
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides mobile-optimized UI components for core application features including navigation, data views, forms, modals, and specialized terminal interfaces.
|
||||
Provides a complete set of mobile-optimized UI components for building responsive mobile interfaces including navigation, lists, detail/edit views, action sheets, and specialized terminal wrappers.
|
||||
## files
|
||||
- mobile-action-sheet.tsx | Renders a mobile-optimized action sheet modal with title, configurable action buttons, and cancel option. | exp: MobileActionSheetItem, func:MobileActionSheet({ isOpen, onClose, title, actions, }: MobileActionSheetProps), call:useRef, call:useEffect, call:onClose, call:document.addEventListener, call:document.removeEventListener, call:e.stopPropagation, call:actions.map, call:action.onClick | dep: react, ../../icon, icon
|
||||
- mobile-detail-view.tsx | A React component that renders a mobile-optimized detail view with a header (back button, title, edit/delete actions) and a field list supporting multiple value types (text, code, JSON, boolean). | exp: MobileDetailView | dep: ../../icon, react
|
||||
- mobile-detail-view.tsx | A React component that renders a mobile-optimized detail view with a header, back/edit/delete actions, and a configurable list of typed fields. | exp: MobileDetailView | dep: ../../icon, React
|
||||
- mobile-edit-view.tsx | A React component that renders a mobile-optimized form for editing data with configurable field types and save/cancel actions. | exp: MobileEditView | dep: react
|
||||
- mobile-fab.tsx | Renders a floating action button component for mobile with an add icon and configurable click handler and label. | exp: MobileFAB | dep: ../../icon, react, icon
|
||||
- mobile-list-view.tsx | Renders a mobile-optimized list view with optional search, empty state, and customizable item rendering | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon
|
||||
- mobile-nav.tsx | Renders a mobile navigation bar with grouped items that open bottom sheets and standard items that use React Router links, including active state indicators and session count badges. | exp: MobileNav | dep: react, react-router-dom, ../../icon, ../tool/tools-bottom-sheet, ./spaces-bottom-sheet, ../../../utils/icons
|
||||
- mobile-nav.tsx | Renders a mobile navigation bar with grouped items that trigger bottom sheets and standard links with active state highlighting | exp: MobileNav | dep: react, react-router-dom, ../../icon, ../tool/tools-bottom-sheet, ./spaces-bottom-sheet, ../../../utils/icons
|
||||
- mobile-page-header.tsx | Renders a mobile-only page header with optional back navigation and custom actions. | exp: func:MobilePageHeader({ title, showBack = true, actions }: MobilePageHeaderProps), call:useNavigate, call:useMobileViewport, call:navigate | dep: react-router-dom, ../../../hooks/use-mobile-viewport, ../../icon, use-mobile-viewport hook, Icon component
|
||||
- mobile-terminal-header.tsx | Renders a mobile-responsive header for a terminal interface with navigation, title, connection status, font size controls, and close actions. | exp: MobileTerminalHeader | dep: react, ../../icon, icon
|
||||
- mobile-terminal-wrapper.tsx | Wraps a terminal component with mobile-specific UI including auto-hiding header, virtual keyboard handling, and special keys interface. | exp: MobileTerminalWrapper | dep: react, ../terminal/terminal, ./mobile-terminal-header, ../terminal/special-keys-strip, ../terminal/special-keys-panel, ../../../hooks/use-mobile-viewport, ../../../hooks/use-virtual-keyboard, ../../../hooks/use-auto-hide, ../../../hooks/use-special-keys
|
||||
- spaces-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for switching between "Projects" and "Workspaces" spaces with active state highlighting. | exp: SpacesBottomSheet | dep: react-router-dom, ../../icon, icon
|
||||
## arch
|
||||
Compositional React component library with feature-specific mobile adaptations, using bottom sheets/action sheets for mobile-native UX patterns, and wrapper components that inject mobile-specific behaviors (auto-hiding headers, virtual keyboard handling) into existing features.
|
||||
Feature-based component library using compound mobile patterns (bottom sheets, FABs, auto-hiding headers) with typed configurable props and progressive enhancement for touch/keyboard interactions.
|
||||
## tags
|
||||
mobile, terminal, sheet, react, icon, view, header, renders
|
||||
mobile, terminal, sheet, icon, view, react, header, renders
|
||||
## symbols
|
||||
- MobileActionSheet
|
||||
- MobilePageHeader
|
||||
|
||||
@@ -13,6 +13,7 @@ interface MobileDetailViewProps {
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onBack: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
||||
@@ -22,6 +23,7 @@ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
||||
onEdit,
|
||||
onDelete,
|
||||
onBack,
|
||||
children,
|
||||
}) => {
|
||||
const renderValue = (field: Field) => {
|
||||
if (field.value === null || field.value === undefined) {
|
||||
@@ -90,6 +92,7 @@ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/hooks
|
||||
|
||||
## role
|
||||
Provides a collection of reusable React custom hooks that encapsulate domain-specific business logic, API interactions, and UI state management for the web application.
|
||||
Provides reusable React custom hooks that encapsulate stateful logic, side effects, and API interactions for the web application's UI components, covering data fetching, terminal management, workspace operations, notifications, and mobile-specific behaviors.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,11 +4,11 @@ dir: apps/web/src/hooks
|
||||
index: apps/web/src/hooks/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides a collection of reusable React custom hooks that encapsulate domain-specific business logic, API interactions, and UI state management for the web application.
|
||||
Provides reusable React custom hooks that encapsulate stateful logic, side effects, and API interactions for the web application's UI components, covering data fetching, terminal management, workspace operations, notifications, and mobile-specific behaviors.
|
||||
## 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-config-profiles.ts | A React custom hook that manages CRUD operations, form state, drag-and-drop ordering, and cyclic dependency prevention for configuration profiles. | exp: useConfigProfiles | dep: react, ../utils/errors, ../api/config-profiles, ../api/projects, ../api/tool-types, ../types
|
||||
- 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
|
||||
@@ -31,7 +31,7 @@ Provides a collection of reusable React custom hooks that encapsulate domain-spe
|
||||
- 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 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).
|
||||
Follows a custom hook composition pattern where each hook encapsulates a single domain concern (data fetching, CRUD operations, UI state, device detection), often combining useState/useEffect/useCallback with SWR-style async data management, and layering higher-level hooks (use-terminal-page) from lower-level ones (use-mobile-viewport, use-virtual-keyboard).
|
||||
## tags
|
||||
call:set, call:use, workspace, react, state, terminal, api, callback
|
||||
## symbols
|
||||
|
||||
@@ -193,7 +193,7 @@ export const useConfigProfiles = () => {
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent) => {
|
||||
const handleSubmit = async (e?: React.FormEvent): Promise<boolean> => {
|
||||
e?.preventDefault();
|
||||
setError(null);
|
||||
setSaveStatus("saving");
|
||||
@@ -201,7 +201,7 @@ export const useConfigProfiles = () => {
|
||||
if (!formData.name?.trim()) {
|
||||
setError("Name is required");
|
||||
setSaveStatus("error");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -224,9 +224,11 @@ export const useConfigProfiles = () => {
|
||||
const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id);
|
||||
if (refreshed) populateForm(refreshed);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(extractErrorMessage(err));
|
||||
setSaveStatus("error");
|
||||
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, each managing a distinct functional domain of the developer workspace platform.
|
||||
Contains top-level page components that render the main application screens for workspace management, project administration, Git operations, user settings, and terminal sessions.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,9 +4,9 @@ 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, each managing a distinct functional domain of the developer workspace platform.
|
||||
Contains top-level page components that render the main application screens for workspace management, project administration, Git operations, user settings, and terminal sessions.
|
||||
## 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
|
||||
- ConfigProfilesPage.tsx | Renders a configuration profiles management page with responsive desktop/mobile layouts for creating, editing, previewing, and organizing 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, use-mobile-viewport, use-config-profiles, data-states, ConfigProfileListSidebar, ConfigProfileEditorPanel, 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
|
||||
- DashboardPage.tsx | Renders a dashboard homepage that displays workspace overview, active/recent sessions, summary statistics, and polling health checks for running instances. | exp: HomePage | dep: react, react-router-dom, ../api/dashboard, ../api/sessions, ../components/data-states, ../components/features/session/session-list, ../hooks/use-instance-actions, ../state/sessions
|
||||
- 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
|
||||
@@ -25,9 +25,9 @@ Contains top-level page components that serve as route endpoints for the web app
|
||||
- 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 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 with responsive mobile/desktop split layouts, heavy use of tabbed navigation and sidebar+editor panel patterns, polling-based real-time data updates, and CRUD-heavy container components with embedded API logic.
|
||||
Follows a page-based routing architecture with responsive mobile/desktop view splitting, tabbed navigation patterns, polling-based health checks, and CRUD operations backed by API hooks, with co-located test files.
|
||||
## tags
|
||||
page, components, workspace, features, react, mobile, hooks, settings
|
||||
page, components, workspace, features, mobile, react, hooks, settings
|
||||
## symbols
|
||||
- WorkspaceDetailPage
|
||||
- WorkspacesPage
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ConfigProfileListSidebar } from "../components/features/config-profiles
|
||||
import { ConfigProfileEditorPanel } from "../components/features/config-profiles/ConfigProfileEditorPanel";
|
||||
import { ConfigProfilesMobileView } from "../components/features/config-profiles/ConfigProfilesMobileView";
|
||||
|
||||
type MobileView = "list" | "detail" | "edit";
|
||||
type MobileView = "list" | "detail" | "edit" | "preview";
|
||||
|
||||
export const ConfigProfilesPage = () => {
|
||||
const isMobile = useMobileViewport();
|
||||
@@ -100,11 +100,18 @@ export const ConfigProfilesPage = () => {
|
||||
return (
|
||||
<ConfigProfilesMobileView
|
||||
profiles={profiles}
|
||||
projects={projects}
|
||||
toolTypes={toolTypes}
|
||||
selectedProfile={selectedProfile}
|
||||
mobileView={mobileView}
|
||||
isCreating={isCreating}
|
||||
formData={formData}
|
||||
includedProfileIds={includedProfileIds}
|
||||
availableProfiles={availableProfilesForInclude()}
|
||||
previewData={previewData}
|
||||
previewingId={previewingId}
|
||||
saveStatus={saveStatus}
|
||||
error={error}
|
||||
onViewChange={setMobileView}
|
||||
onSelect={handleSelectProfile}
|
||||
onCreate={handleCreateNew}
|
||||
@@ -112,6 +119,23 @@ export const ConfigProfilesPage = () => {
|
||||
onFormChange={updateFormField}
|
||||
onSubmit={handleSubmit}
|
||||
getScopeLabel={getScopeLabel}
|
||||
getIncludedProfile={getIncludedProfile}
|
||||
onAddInclude={addInclude}
|
||||
onRemoveInclude={removeInclude}
|
||||
onAddEnvVar={addEnvVar}
|
||||
onUpdateEnvVar={updateEnvVar}
|
||||
onRemoveEnvVar={removeEnvVar}
|
||||
onAddFile={addFile}
|
||||
onUpdateFile={updateFile}
|
||||
onRemoveFile={removeFile}
|
||||
onAddMount={addMount}
|
||||
onUpdateMount={updateMount}
|
||||
onRemoveMount={removeMount}
|
||||
onAddMountFile={addMountFile}
|
||||
onUpdateMountFile={updateMountFile}
|
||||
onRemoveMountFile={removeMountFile}
|
||||
onPreview={handlePreview}
|
||||
onClosePreview={() => setPreviewData(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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, theme tokens, syntax highlighting, and utility classes.
|
||||
Houses the complete CSS styling system for the web application, providing design tokens, global styles, component-specific styles, syntax highlighting themes, and responsive utility classes.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,16 +4,16 @@ 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, theme tokens, syntax highlighting, and utility classes.
|
||||
Houses the complete CSS styling system for the web application, providing design tokens, global styles, component-specific styles, syntax highlighting themes, and responsive 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. | dep: CSS custom properties (variables), xterm.js (terminal integration)
|
||||
- utilities.css | Provides responsive CSS utility classes and component-specific styles for a web application featuring terminals, session management, dialogs, tables, and forms with mobile-first responsive design. | dep: CSS custom properties (variables like --space-*, --border, --bg, --brand, --muted, --success, --danger, --text-xs, --text-sm), xterm.js (terminal library)
|
||||
## arch
|
||||
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.
|
||||
Token-based CSS architecture using CSS custom properties for light/dark theming, mobile-first responsive design with breakpoint scaling, and modular separation of concerns across tokens, globals, utilities, and component-specific stylesheets.
|
||||
## tags
|
||||
space, global, css, web, application, syntax, defines, styles
|
||||
space, global, css, web, application, responsive, design, syntax
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
|
||||
@@ -2452,6 +2452,235 @@ a.nav-item,
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Mobile config profile form extras */
|
||||
.mobile-form-group p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mobile-form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-form-section {
|
||||
padding: var(--space-3);
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-form-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.mobile-form-section-header h4 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.mobile-add-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-form-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
margin: var(--space-2) 0;
|
||||
}
|
||||
|
||||
.mobile-list-plain {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.mobile-list-plain li {
|
||||
padding: var(--space-2);
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Mobile detail extra actions */
|
||||
.mobile-detail-actions-extra {
|
||||
padding: var(--space-3);
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--space-2) * -1);
|
||||
}
|
||||
|
||||
.mobile-detail-actions-extra .secondary-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Mobile includes */
|
||||
.mobile-includes-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.mobile-include-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mobile-include-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-include-scope {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: var(--bg);
|
||||
color: var(--muted);
|
||||
border-radius: 999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
/* Mobile key/value rows */
|
||||
.mobile-key-value-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-key-value-row .mobile-form-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Mobile file and mount cards */
|
||||
.mobile-file-card,
|
||||
.mobile-mount-card {
|
||||
padding: var(--space-3);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-file-header,
|
||||
.mobile-mount-header {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-file-header .mobile-form-input,
|
||||
.mobile-mount-header .mobile-form-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mobile-mount-files {
|
||||
padding-left: var(--space-3);
|
||||
border-left: 2px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-mount-file-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mobile-mount-file-row .mobile-form-input,
|
||||
.mobile-mount-file-row .mobile-form-textarea {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Mobile preview */
|
||||
.mobile-preview-code {
|
||||
background: var(--code-bg, #1e1e1e);
|
||||
color: var(--code-fg, #d4d4d4);
|
||||
padding: var(--space-3);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.8125rem;
|
||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||
Consolas, monospace;
|
||||
margin: var(--space-2) 0 0 0;
|
||||
}
|
||||
|
||||
.mobile-preview-item {
|
||||
padding: var(--space-3);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: var(--space-2);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.mobile-preview-item strong {
|
||||
display: block;
|
||||
margin-bottom: var(--space-2);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.mobile-preview-mapping {
|
||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||
Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--muted);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
/* Mobile git mount editor overrides */
|
||||
.mobile-form-section .git-mount-editor .card {
|
||||
padding: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-form-section .git-mount-editor .form-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.mobile-form-section .git-mount-editor input,
|
||||
.mobile-form-section .git-mount-editor select,
|
||||
.mobile-form-section .git-mount-editor textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-form-section .git-mount-editor .form-actions {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.mobile-form-section .git-mount-editor button {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* Ensure minimum touch targets on mobile */
|
||||
button,
|
||||
a,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Mobile Config Profiles UI Rework
|
||||
|
||||
## Goal
|
||||
|
||||
Redesign the mobile **Config Profiles** detail ("preview") and edit pages to match the full desktop functionality, following the same mobile UX pattern used for the Tool Workshop.
|
||||
|
||||
## Scope
|
||||
|
||||
- Rewrite `ConfigProfilesMobileView` so the detail view displays all profile fields and supports resolving/previewing the profile.
|
||||
- Rewrite the mobile edit view to expose every field available on the desktop editor:
|
||||
- name, description, project, tool type, default flag
|
||||
- includes with add/remove/reorder
|
||||
- environment variables
|
||||
- runtime hints
|
||||
- files
|
||||
- mounts with nested files
|
||||
- git mounts
|
||||
- Update `ConfigProfilesPage` to pass the required state and callbacks.
|
||||
- Update `useConfigProfiles` so `handleSubmit` returns success status for cleaner mobile navigation.
|
||||
- Add mobile-specific CSS for config profile forms.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- On viewports narrower than 768 px, the Config Profiles page renders a mobile-friendly list/detail/edit flow.
|
||||
- The mobile detail view shows all profile fields and offers a Preview action that displays the resolved profile.
|
||||
- The mobile edit view supports all desktop fields, including includes, runtime hints, files, mounts, nested mount files, and git mounts.
|
||||
- All existing desktop functionality remains intact.
|
||||
- `npm run typecheck` and `npm run lint` pass in `apps/web`.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Backend API changes.
|
||||
- Changes to other pages.
|
||||
- New features beyond existing desktop capabilities.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Tasks: Mobile Config Profiles UI Rework
|
||||
|
||||
- [x] Inspect existing ConfigProfilesMobileView, desktop editor, hook, and page.
|
||||
- [x] Update `useConfigProfiles.handleSubmit` to return boolean success.
|
||||
- [x] Rewrite `ConfigProfilesMobileView` detail view with full field display and preview action.
|
||||
- [x] Rewrite `ConfigProfilesMobileView` edit view with all desktop fields (includes, env vars, runtime hints, files, mounts, git mounts).
|
||||
- [x] Update `ConfigProfilesPage` to pass required props to mobile view.
|
||||
- [x] Add responsive CSS for config profile mobile forms.
|
||||
- [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