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.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
-
|
-
|
||||||
## children
|
## children
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ index: ./.pi-map.index.md
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
|
- .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
|
- .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
|
- 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
|
- swap-pane | Empty file with no functionality
|
||||||
## arch
|
## 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
|
## tags
|
||||||
docker, redis, git, application, postgresql, compose, traefik, project
|
docker, redis, git, application, postgresql, compose, traefik, project
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps
|
dir: apps
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,10 +4,10 @@ dir: apps
|
|||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
## arch
|
## 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
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web
|
dir: apps/web
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
map: apps/.pi-map.md
|
map: apps/.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ dir: apps/web
|
|||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
- .env.example | Template file defining example environment variables for frontend API and application URL configuration
|
- .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
|
- .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
|
- 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
|
- 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
|
## 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
|
## tags
|
||||||
react, eslint, vite, typescript, dom, application, nginx, web
|
react, eslint, vite, typescript, dom, application, nginx, web
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src
|
dir: apps/web/src
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
map: apps/web/.pi-map.md
|
map: apps/web/.pi-map.md
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ dir: apps/web/src
|
|||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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
|
- 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
|
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
||||||
## arch
|
## 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
|
## tags
|
||||||
pages, styles, css, router, react, session, dom, project
|
pages, styles, css, router, react, session, dom, project
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components
|
dir: apps/web/src/components
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.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
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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
|
- 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.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
|
- 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
|
## 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
|
## tags
|
||||||
toast, icon, react, state, code, loading, event, editor
|
toast, icon, react, state, code, loading, event, editor
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features
|
dir: apps/web/src/components/features
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
map: apps/web/src/components/.pi-map.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
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
## arch
|
## 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
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/config-profiles
|
dir: apps/web/src/components/features/config-profiles
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.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
|
index: apps/web/src/components/features/config-profiles/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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
|
- 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
|
## 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
|
## tags
|
||||||
config, mobile, profiles, profile, view, editor, icon, list
|
mobile, config, view, profiles, editor, profile, list, icon
|
||||||
## symbols
|
## symbols
|
||||||
- ConfigProfileEditorPanel
|
- ConfigProfileEditorPanel
|
||||||
- ConfigProfileListSidebar
|
- ConfigProfileListSidebar
|
||||||
|
|||||||
@@ -1,35 +1,86 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { MobileListView } from "../mobile/mobile-list-view";
|
import { MobileListView } from "../mobile/mobile-list-view";
|
||||||
import { MobileDetailView } from "../mobile/mobile-detail-view";
|
import { MobileDetailView } from "../mobile/mobile-detail-view";
|
||||||
import { MobileEditView } from "../mobile/mobile-edit-view";
|
import { MobileEditView } from "../mobile/mobile-edit-view";
|
||||||
import { MobileFAB } from "../mobile/mobile-fab";
|
import { MobileFAB } from "../mobile/mobile-fab";
|
||||||
|
import { GitMountEditor } from "../git/git-mount-editor";
|
||||||
import { Icon } from "../../icon";
|
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 {
|
interface Props {
|
||||||
profiles: ConfigProfile[];
|
profiles: ConfigProfile[];
|
||||||
|
projects: ProjectWithRepos[];
|
||||||
|
toolTypes: ToolType[];
|
||||||
selectedProfile: ConfigProfile | null;
|
selectedProfile: ConfigProfile | null;
|
||||||
mobileView: MobileView;
|
mobileView: MobileView;
|
||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
formData: CreateConfigProfileRequest;
|
formData: CreateConfigProfileRequest;
|
||||||
|
includedProfileIds: string[];
|
||||||
|
availableProfiles: ConfigProfile[];
|
||||||
|
previewData: ResolvedProfile | null;
|
||||||
|
previewingId: string | null;
|
||||||
saveStatus: "idle" | "saving" | "saved" | "error";
|
saveStatus: "idle" | "saving" | "saved" | "error";
|
||||||
|
error: string | null;
|
||||||
onViewChange: (view: MobileView) => void;
|
onViewChange: (view: MobileView) => void;
|
||||||
onSelect: (profile: ConfigProfile) => void;
|
onSelect: (profile: ConfigProfile) => void;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
onFormChange: <K extends keyof CreateConfigProfileRequest>(key: K, value: CreateConfigProfileRequest[K]) => void;
|
onFormChange: <K extends keyof CreateConfigProfileRequest>(
|
||||||
onSubmit: () => void;
|
key: K,
|
||||||
|
value: CreateConfigProfileRequest[K],
|
||||||
|
) => void;
|
||||||
|
onSubmit: (e?: React.FormEvent) => Promise<boolean>;
|
||||||
getScopeLabel: (profile: ConfigProfile) => string;
|
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 = ({
|
export const ConfigProfilesMobileView = ({
|
||||||
profiles,
|
profiles,
|
||||||
|
projects,
|
||||||
|
toolTypes,
|
||||||
selectedProfile,
|
selectedProfile,
|
||||||
mobileView,
|
mobileView,
|
||||||
isCreating,
|
isCreating,
|
||||||
formData,
|
formData,
|
||||||
|
includedProfileIds,
|
||||||
|
availableProfiles,
|
||||||
|
previewData,
|
||||||
|
previewingId,
|
||||||
saveStatus,
|
saveStatus,
|
||||||
|
error,
|
||||||
onViewChange,
|
onViewChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
onCreate,
|
onCreate,
|
||||||
@@ -37,12 +88,57 @@ export const ConfigProfilesMobileView = ({
|
|||||||
onFormChange,
|
onFormChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
getScopeLabel,
|
getScopeLabel,
|
||||||
|
getIncludedProfile,
|
||||||
|
onAddInclude,
|
||||||
|
onRemoveInclude,
|
||||||
|
onAddEnvVar,
|
||||||
|
onUpdateEnvVar,
|
||||||
|
onRemoveEnvVar,
|
||||||
|
onAddFile,
|
||||||
|
onUpdateFile,
|
||||||
|
onRemoveFile,
|
||||||
|
onAddMount,
|
||||||
|
onUpdateMount,
|
||||||
|
onRemoveMount,
|
||||||
|
onAddMountFile,
|
||||||
|
onUpdateMountFile,
|
||||||
|
onRemoveMountFile,
|
||||||
|
onPreview,
|
||||||
|
onClosePreview,
|
||||||
}: Props) => {
|
}: 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") {
|
if (mobileView === "list") {
|
||||||
return (
|
return (
|
||||||
<div className="mobile-page">
|
<div className="mobile-page">
|
||||||
<div className="mobile-page-header">
|
<div className="mobile-page-header">
|
||||||
<h1>Config Profiles</h1>
|
<h1>Config Profiles</h1>
|
||||||
|
<span className="muted">{profiles.length} profiles</span>
|
||||||
</div>
|
</div>
|
||||||
<MobileListView
|
<MobileListView
|
||||||
items={profiles.map((profile) => ({
|
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) {
|
if (mobileView === "detail" && selectedProfile) {
|
||||||
const fields = [
|
const fields = [
|
||||||
{ label: "Name", value: selectedProfile.name },
|
{ label: "Name", value: selectedProfile.name },
|
||||||
{ label: "Description", value: selectedProfile.description || "-" },
|
{
|
||||||
|
label: "Description",
|
||||||
|
value: selectedProfile.description || null,
|
||||||
|
},
|
||||||
{ label: "Scope", value: getScopeLabel(selectedProfile) },
|
{ 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",
|
label: "Environment Variables",
|
||||||
value:
|
value:
|
||||||
@@ -83,21 +313,40 @@ export const ConfigProfilesMobileView = ({
|
|||||||
? Object.entries(selectedProfile.env_vars)
|
? Object.entries(selectedProfile.env_vars)
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
.join(", ")
|
.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",
|
label: "Mounts",
|
||||||
value:
|
value:
|
||||||
selectedProfile.mounts.length > 0
|
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:
|
value:
|
||||||
selectedProfile.includes.length > 0
|
selectedProfile.git_mounts.length > 0
|
||||||
? `${selectedProfile.includes.length} profile(s)`
|
? selectedProfile.git_mounts
|
||||||
: "-",
|
.map((m) => m.remote_url)
|
||||||
|
.join(", ")
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -111,8 +360,33 @@ export const ConfigProfilesMobileView = ({
|
|||||||
onSelect(selectedProfile);
|
onSelect(selectedProfile);
|
||||||
onViewChange("edit");
|
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={() => {
|
onCancel={() => {
|
||||||
onViewChange(isCreating ? "list" : "detail");
|
onViewChange(isCreating ? "list" : "detail");
|
||||||
}}
|
}}
|
||||||
onSave={onSubmit}
|
onSave={handleSave}
|
||||||
isSaving={saveStatus === "saving"}
|
isSaving={isSaving || saveStatus === "saving"}
|
||||||
>
|
>
|
||||||
<div className="form-group">
|
<div className="mobile-form-group">
|
||||||
<label>Name *</label>
|
<label className="mobile-form-label" htmlFor="cp-name">
|
||||||
|
Name *
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="cp-name"
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => onFormChange("name", e.target.value)}
|
onChange={(e) => onFormChange("name", e.target.value)}
|
||||||
placeholder="Profile name"
|
placeholder="e.g., Development Environment"
|
||||||
|
className="mobile-form-input"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="mobile-form-group">
|
||||||
<label>Description</label>
|
<label className="mobile-form-label" htmlFor="cp-description">
|
||||||
<textarea
|
Description
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="cp-description"
|
||||||
|
type="text"
|
||||||
value={formData.description || ""}
|
value={formData.description || ""}
|
||||||
onChange={(e) => onFormChange("description", e.target.value || undefined)}
|
onChange={(e) =>
|
||||||
|
onFormChange("description", e.target.value || undefined)
|
||||||
|
}
|
||||||
placeholder="Optional description"
|
placeholder="Optional description"
|
||||||
rows={3}
|
className="mobile-form-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="mobile-form-group">
|
||||||
<label>Project</label>
|
<label className="mobile-form-label" htmlFor="cp-project">
|
||||||
|
Project
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
|
id="cp-project"
|
||||||
value={formData.project_id || ""}
|
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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="mobile-form-group">
|
||||||
<label>Tool Type</label>
|
<label className="mobile-form-label" htmlFor="cp-tool">
|
||||||
|
Tool Type
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
|
id="cp-tool"
|
||||||
value={formData.tool_type_id || ""}
|
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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="mobile-form-group mobile-form-row">
|
||||||
<label>
|
<label className="mobile-form-label mobile-form-checkbox-label">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.is_default || false}
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="mobile-form-section">
|
||||||
<label>Environment Variables</label>
|
<div className="mobile-form-section-header">
|
||||||
{Object.entries(formData.env_vars || {}).map(([key, value], index) => (
|
<h4>Includes</h4>
|
||||||
<div key={index} style={{ display: "flex", gap: "0.5rem", marginBottom: "0.5rem" }}>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={key}
|
value={key}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const newEnvVars = { ...formData.env_vars };
|
onUpdateEnvVar(key, e.target.value, value)
|
||||||
delete newEnvVars[key];
|
}
|
||||||
newEnvVars[e.target.value] = value;
|
placeholder="VAR_NAME"
|
||||||
onFormChange("env_vars", newEnvVars);
|
className="mobile-form-input"
|
||||||
}}
|
|
||||||
placeholder="KEY"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const newEnvVars = { ...formData.env_vars };
|
onUpdateEnvVar(key, key, e.target.value)
|
||||||
newEnvVars[key] = e.target.value;
|
}
|
||||||
onFormChange("env_vars", newEnvVars);
|
|
||||||
}}
|
|
||||||
placeholder="value"
|
placeholder="value"
|
||||||
style={{ flex: 1 }}
|
className="mobile-form-input"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
className="ghost-button small"
|
||||||
const newEnvVars = { ...formData.env_vars };
|
onClick={() => onRemoveEnvVar(key)}
|
||||||
delete newEnvVars[key];
|
|
||||||
onFormChange("env_vars", newEnvVars);
|
|
||||||
}}
|
|
||||||
className="secondary-button"
|
|
||||||
>
|
>
|
||||||
<Icon name="delete" size="sm" />
|
<Icon name="delete" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -220,49 +576,158 @@ export const ConfigProfilesMobileView = ({
|
|||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="secondary-button"
|
className="secondary-button mobile-add-button"
|
||||||
onClick={() => {
|
onClick={onAddEnvVar}
|
||||||
onFormChange("env_vars", { ...formData.env_vars, "": "" });
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon name="add" size="sm" /> Add Variable
|
<Icon name="add" size="sm" /> Add Variable
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="mobile-form-section">
|
||||||
<label>Mounts</label>
|
<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) => (
|
{(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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={mount.target}
|
value={mount.target}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const newMounts = [...(formData.mounts || [])];
|
onUpdateMount(index, { target: e.target.value })
|
||||||
newMounts[index] = { ...mount, target: e.target.value };
|
}
|
||||||
onFormChange("mounts", newMounts);
|
placeholder="/target/path"
|
||||||
}}
|
className="mobile-form-input"
|
||||||
placeholder="Target path"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
value={mount.mode}
|
value={mount.mode}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const newMounts = [...(formData.mounts || [])];
|
onUpdateMount(index, {
|
||||||
newMounts[index] = { ...mount, mode: e.target.value as "ro" | "rw" };
|
mode: e.target.value as "ro" | "rw",
|
||||||
onFormChange("mounts", newMounts);
|
})
|
||||||
}}
|
}
|
||||||
style={{ width: "80px" }}
|
className="mobile-form-select"
|
||||||
>
|
>
|
||||||
<option value="ro">Read</option>
|
<option value="rw">Read/Write</option>
|
||||||
<option value="rw">Write</option>
|
<option value="ro">Read-Only</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
className="ghost-button small"
|
||||||
const newMounts = (formData.mounts || []).filter((_, i) => i !== index);
|
onClick={() => onRemoveMount(index)}
|
||||||
onFormChange("mounts", newMounts);
|
>
|
||||||
}}
|
<Icon name="delete" size="sm" />
|
||||||
className="secondary-button"
|
</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" />
|
<Icon name="delete" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -270,14 +735,44 @@ export const ConfigProfilesMobileView = ({
|
|||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="secondary-button"
|
className="secondary-button small mobile-add-button"
|
||||||
onClick={() => {
|
onClick={() => onAddMountFile(index)}
|
||||||
onFormChange("mounts", [...(formData.mounts || []), { target: "/", mode: "rw", files: {} }]);
|
>
|
||||||
}}
|
<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
|
<Icon name="add" size="sm" /> Add Mount
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</MobileEditView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -286,6 +781,7 @@ export const ConfigProfilesMobileView = ({
|
|||||||
<div className="mobile-page">
|
<div className="mobile-page">
|
||||||
<div className="mobile-page-header">
|
<div className="mobile-page-header">
|
||||||
<h1>Config Profiles</h1>
|
<h1>Config Profiles</h1>
|
||||||
|
<span className="muted">{profiles.length} profiles</span>
|
||||||
</div>
|
</div>
|
||||||
<MobileListView
|
<MobileListView
|
||||||
items={profiles.map((profile) => ({
|
items={profiles.map((profile) => ({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/mobile
|
dir: apps/web/src/components/features/mobile
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.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
|
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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-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-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-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-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-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-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
|
- 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
|
- 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
|
## 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
|
## tags
|
||||||
mobile, terminal, sheet, react, icon, view, header, renders
|
mobile, terminal, sheet, icon, view, react, header, renders
|
||||||
## symbols
|
## symbols
|
||||||
- MobileActionSheet
|
- MobileActionSheet
|
||||||
- MobilePageHeader
|
- MobilePageHeader
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface MobileDetailViewProps {
|
|||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
||||||
@@ -22,6 +23,7 @@ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onBack,
|
onBack,
|
||||||
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const renderValue = (field: Field) => {
|
const renderValue = (field: Field) => {
|
||||||
if (field.value === null || field.value === undefined) {
|
if (field.value === null || field.value === undefined) {
|
||||||
@@ -90,6 +92,7 @@ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/hooks
|
dir: apps/web/src/hooks
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.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
|
index: apps/web/src/hooks/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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-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-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.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-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-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-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
|
- 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
|
## 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
|
## tags
|
||||||
call:set, call:use, workspace, react, state, terminal, api, callback
|
call:set, call:use, workspace, react, state, terminal, api, callback
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export const useConfigProfiles = () => {
|
|||||||
setDragOverIndex(null);
|
setDragOverIndex(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e?: React.FormEvent) => {
|
const handleSubmit = async (e?: React.FormEvent): Promise<boolean> => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
setSaveStatus("saving");
|
setSaveStatus("saving");
|
||||||
@@ -201,7 +201,7 @@ export const useConfigProfiles = () => {
|
|||||||
if (!formData.name?.trim()) {
|
if (!formData.name?.trim()) {
|
||||||
setError("Name is required");
|
setError("Name is required");
|
||||||
setSaveStatus("error");
|
setSaveStatus("error");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -224,9 +224,11 @@ export const useConfigProfiles = () => {
|
|||||||
const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id);
|
const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id);
|
||||||
if (refreshed) populateForm(refreshed);
|
if (refreshed) populateForm(refreshed);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(extractErrorMessage(err));
|
setError(extractErrorMessage(err));
|
||||||
setSaveStatus("error");
|
setSaveStatus("error");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/pages
|
dir: apps/web/src/pages
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.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
|
index: apps/web/src/pages/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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.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
|
- 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
|
- 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
|
- 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
|
- 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
|
## 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
|
## tags
|
||||||
page, components, workspace, features, react, mobile, hooks, settings
|
page, components, workspace, features, mobile, react, hooks, settings
|
||||||
## symbols
|
## symbols
|
||||||
- WorkspaceDetailPage
|
- WorkspaceDetailPage
|
||||||
- WorkspacesPage
|
- WorkspacesPage
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ConfigProfileListSidebar } from "../components/features/config-profiles
|
|||||||
import { ConfigProfileEditorPanel } from "../components/features/config-profiles/ConfigProfileEditorPanel";
|
import { ConfigProfileEditorPanel } from "../components/features/config-profiles/ConfigProfileEditorPanel";
|
||||||
import { ConfigProfilesMobileView } from "../components/features/config-profiles/ConfigProfilesMobileView";
|
import { ConfigProfilesMobileView } from "../components/features/config-profiles/ConfigProfilesMobileView";
|
||||||
|
|
||||||
type MobileView = "list" | "detail" | "edit";
|
type MobileView = "list" | "detail" | "edit" | "preview";
|
||||||
|
|
||||||
export const ConfigProfilesPage = () => {
|
export const ConfigProfilesPage = () => {
|
||||||
const isMobile = useMobileViewport();
|
const isMobile = useMobileViewport();
|
||||||
@@ -100,11 +100,18 @@ export const ConfigProfilesPage = () => {
|
|||||||
return (
|
return (
|
||||||
<ConfigProfilesMobileView
|
<ConfigProfilesMobileView
|
||||||
profiles={profiles}
|
profiles={profiles}
|
||||||
|
projects={projects}
|
||||||
|
toolTypes={toolTypes}
|
||||||
selectedProfile={selectedProfile}
|
selectedProfile={selectedProfile}
|
||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
|
includedProfileIds={includedProfileIds}
|
||||||
|
availableProfiles={availableProfilesForInclude()}
|
||||||
|
previewData={previewData}
|
||||||
|
previewingId={previewingId}
|
||||||
saveStatus={saveStatus}
|
saveStatus={saveStatus}
|
||||||
|
error={error}
|
||||||
onViewChange={setMobileView}
|
onViewChange={setMobileView}
|
||||||
onSelect={handleSelectProfile}
|
onSelect={handleSelectProfile}
|
||||||
onCreate={handleCreateNew}
|
onCreate={handleCreateNew}
|
||||||
@@ -112,6 +119,23 @@ export const ConfigProfilesPage = () => {
|
|||||||
onFormChange={updateFormField}
|
onFormChange={updateFormField}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
getScopeLabel={getScopeLabel}
|
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
|
dir: apps/web/src/styles
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.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
|
index: apps/web/src/styles/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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)
|
- 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
|
- 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.
|
- 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
|
## 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
|
## tags
|
||||||
space, global, css, web, application, syntax, defines, styles
|
space, global, css, web, application, responsive, design, syntax
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -2452,6 +2452,235 @@ a.nav-item,
|
|||||||
width: 100% !important;
|
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 */
|
/* Ensure minimum touch targets on mobile */
|
||||||
button,
|
button,
|
||||||
a,
|
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