Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c31ef9577 | |||
| a12d6a8169 | |||
| 13b3c60abc | |||
| 23fc0a6b82 | |||
| 8b15689fb3 | |||
| 98d4393387 | |||
| 35f0a3ea2e | |||
| 1ae8d0e45f | |||
| 025ccc5817 | |||
| 350b393457 |
+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 package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
A self-hosted web-based project management and git repository platform with OAuth2 authentication, built as a containerized multi-service application with PostgreSQL, Redis, and Traefik deployment support.
|
||||||
## 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 package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
A self-hosted web-based project management and git repository platform with OAuth2 authentication, built as a containerized multi-service application with PostgreSQL, Redis, and Traefik deployment support.
|
||||||
## 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 package for a self-hosted project ma
|
|||||||
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
|
- 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
|
||||||
Docker Compose-based microservices architecture with frontend/backend separation, PostgreSQL/Redis data layer, Traefik reverse proxy integration, and environment-driven configuration management following twelve-factor app principles.
|
Monolithic full-stack architecture with separate frontend/backend services, Docker Compose orchestration, environment-driven configuration via .env files, and reverse proxy integration (Traefik) for TLS-terminated deployment; follows OpenSpec-driven AI-assisted development workflows with phased refactoring approach.
|
||||||
## 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 modules or entry points for the project.
|
Contains the main application entry points and executable targets 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 modules or entry points for the project.
|
This appears to be an empty or minimal apps directory, typically serving as a container for application-specific modules or entry points in a larger project structure.
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Modular monolith or microservices architecture with separate application boundaries, each potentially having its own configuration, dependencies, and lifecycle.
|
Standard monorepo or multi-app directory structure following organizational conventions where individual applications or services would be housed as subdirectories.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web
|
dir: apps/web
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
|
Frontend web application providing the browser-based user interface for the "headquarter" project, built as a modern React SPA with code editing, terminal, and routing capabilities.
|
||||||
## 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 providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
|
Frontend web application providing the main user interface for the "headquarter" project as a React-based single-page application.
|
||||||
## 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 providing a React-based UI with code editing, terminal,
|
|||||||
- tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite
|
- 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 React SPA built with Vite and TypeScript, using nginx for production serving with client-side routing, multi-stage Docker deployment, and Vitest for testing.
|
Modern React SPA built with Vite, TypeScript, and nginx serving, featuring containerized deployment with multi-stage Docker builds, client-side routing, and optimized static asset delivery.
|
||||||
## 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
|
||||||
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
Entry point and core infrastructure for a React web application handling routing, authentication, and type definitions.
|
||||||
## 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
|
||||||
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
Entry point and core infrastructure for a React web application handling routing, authentication, and type definitions.
|
||||||
## 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
|
||||||
Modular React SPA using React Router v6 with nested route layouts, protected route guards via authentication context, and centralized TypeScript domain models for session/workspace/project entities.
|
Modular React SPA with React Router v6 (nested routes, protected routes, layout nesting), provider-based context pattern for auth/session management, and centralized TypeScript type definitions.
|
||||||
## 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 components and utilities for rendering the application shell, data states, icons, code display, notifications, and route protection in a React web application.
|
Provides reusable, accessible UI primitives and layout components for a React web application, including shell layout, data states, icons, code editing, routing 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, accessible UI primitives and layout components for a React web application, including shell layout, data states, icons, code editing, routing guards, and toast notifications.
|
||||||
## 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, and separation of concerns between presentation (UI rendering), logic (rules/hooks), and testing; integrates third-party libraries (react-simple-code-editor, Phosphor icons) and React Router for SPA navigation.
|
||||||
## 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
|
||||||
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
Contains reusable React components that implement specific product features and user-facing functionality across the web application.
|
||||||
## 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
|
||||||
@@ -19,6 +19,9 @@ map: apps/web/src/components/.pi-map.md
|
|||||||
- apps/web/src/components/features/notification
|
- apps/web/src/components/features/notification
|
||||||
index: apps/web/src/components/features/notification/.pi-map.index.md
|
index: apps/web/src/components/features/notification/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/notification/.pi-map.md
|
map: apps/web/src/components/features/notification/.pi-map.md
|
||||||
|
- apps/web/src/components/features/profile
|
||||||
|
index: apps/web/src/components/features/profile/.pi-map.index.md
|
||||||
|
map: apps/web/src/components/features/profile/.pi-map.md
|
||||||
- apps/web/src/components/features/project
|
- apps/web/src/components/features/project
|
||||||
index: apps/web/src/components/features/project/.pi-map.index.md
|
index: apps/web/src/components/features/project/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/project/.pi-map.md
|
map: apps/web/src/components/features/project/.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.
|
Contains reusable React components that implement specific product features and user-facing functionality across the web application.
|
||||||
## 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.
|
Feature-based component organization with domain-specific groupings, likely combining presentational and container patterns with hooks for state management, following a modular architecture where each feature encapsulates its own UI, logic, and data fetching concerns.
|
||||||
## 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 UI components for managing Docker configuration profiles including creation, editing, listing, and mobile-responsive views.
|
||||||
## 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 UI components for managing Docker configuration profiles including creation, editing, listing, and mobile-responsive views.
|
||||||
## 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-responsive 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, types
|
||||||
## 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.
|
Feature-based component composition with state-driven panels (list/detail/edit/preview), form-based editing, and responsive layout adaptation for mobile/desktop.
|
||||||
## tags
|
## tags
|
||||||
config, mobile, profiles, profile, view, editor, icon, list
|
mobile, config, view, profiles, editor, profile, list, icon
|
||||||
## symbols
|
## symbols
|
||||||
- ConfigProfileEditorPanel
|
- ConfigProfileEditorPanel
|
||||||
- ConfigProfileListSidebar
|
- ConfigProfileListSidebar
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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 CRUD operations, navigation, and terminal interfaces in a responsive web application.
|
||||||
## 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 CRUD operations, navigation, and terminal interfaces in a responsive web application.
|
||||||
## 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 records with configurable field types and save/cancel/delete 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 | A reusable React component that renders a mobile-optimized list view with optional search, custom item rendering, and action buttons (duplicate/delete). | 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 architecture using compound mobile patterns (bottom sheets, action sheets, FABs) with typed configurable props, consistent mobile-first design system, and specialized terminal wrapper with native-like behaviors (auto-hiding headers, virtual keyboard handling).
|
||||||
## tags
|
## tags
|
||||||
mobile, terminal, sheet, react, icon, view, header, renders
|
mobile, terminal, sheet, react, icon, view, header, action
|
||||||
## 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ interface MobileEditViewProps {
|
|||||||
fields?: FormField[];
|
fields?: FormField[];
|
||||||
onSave: (data: Record<string, string | number | boolean>) => void;
|
onSave: (data: Record<string, string | number | boolean>) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
deleteLabel?: string;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -25,6 +27,8 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
|
|||||||
fields,
|
fields,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
onDelete,
|
||||||
|
deleteLabel = "Delete",
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -59,14 +63,7 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<h1 className="mobile-edit-title">{title}</h1>
|
<h1 className="mobile-edit-title">{title}</h1>
|
||||||
<button
|
<div style={{ width: "4rem" }} />
|
||||||
className="mobile-edit-save"
|
|
||||||
onClick={() => onSave(formData)}
|
|
||||||
type="button"
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
{isSaving ? "Saving..." : "Save"}
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form className="mobile-edit-form" onSubmit={handleSubmit}>
|
<form className="mobile-edit-form" onSubmit={handleSubmit}>
|
||||||
@@ -156,6 +153,27 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div className="mobile-edit-actions">
|
||||||
|
<button
|
||||||
|
className="mobile-edit-save"
|
||||||
|
onClick={() => onSave(formData)}
|
||||||
|
type="button"
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? "Saving..." : "Save"}
|
||||||
|
</button>
|
||||||
|
{onDelete && (
|
||||||
|
<button
|
||||||
|
className="mobile-edit-delete"
|
||||||
|
onClick={onDelete}
|
||||||
|
type="button"
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{deleteLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ interface MobileListViewProps {
|
|||||||
export const MobileListView: React.FC<MobileListViewProps> = ({
|
export const MobileListView: React.FC<MobileListViewProps> = ({
|
||||||
items,
|
items,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
|
onItemDelete,
|
||||||
|
onItemDuplicate,
|
||||||
emptyMessage = "No items found",
|
emptyMessage = "No items found",
|
||||||
searchPlaceholder = "Search...",
|
searchPlaceholder = "Search...",
|
||||||
onSearch,
|
onSearch,
|
||||||
@@ -73,11 +75,38 @@ export const MobileListView: React.FC<MobileListViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div className="mobile-list-item-actions">
|
||||||
className="mobile-list-item-actions"
|
{onItemDuplicate && (
|
||||||
style={{ transform: "rotate(180deg)" }}
|
<button
|
||||||
>
|
type="button"
|
||||||
<Icon name="arrow-left" size="sm" />
|
className="mobile-list-item-action mobile-list-item-action-duplicate"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onItemDuplicate(item.id);
|
||||||
|
}}
|
||||||
|
aria-label="Duplicate"
|
||||||
|
>
|
||||||
|
<Icon name="copy" size="sm" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onItemDelete && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mobile-list-item-action mobile-list-item-action-delete"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onItemDelete(item.id);
|
||||||
|
}}
|
||||||
|
aria-label="Delete"
|
||||||
|
>
|
||||||
|
<Icon name="delete" size="sm" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!onItemDelete && !onItemDuplicate && (
|
||||||
|
<span style={{ transform: "rotate(180deg)" }}>
|
||||||
|
<Icon name="arrow-left" size="sm" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# apps/web/src/components/features/profile (index)
|
||||||
|
dir: apps/web/src/components/features/profile
|
||||||
|
|
||||||
|
## role
|
||||||
|
Provides a mobile-specific UI for users to view and edit their profile information.
|
||||||
|
## parent
|
||||||
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
|
map: apps/web/src/components/features/.pi-map.md
|
||||||
|
## children
|
||||||
|
-
|
||||||
|
## files
|
||||||
|
- ProfileMobileView.tsx
|
||||||
|
## links
|
||||||
|
index: apps/web/src/components/features/profile/.pi-map.index.md
|
||||||
|
map: apps/web/src/components/features/profile/.pi-map.md
|
||||||
|
## workflows
|
||||||
|
- change profile behavior
|
||||||
|
read: ProfileMobileView.tsx
|
||||||
|
## dirty
|
||||||
|
-
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# apps/web/src/components/features/profile
|
||||||
|
dir: apps/web/src/components/features/profile
|
||||||
|
|
||||||
|
index: apps/web/src/components/features/profile/.pi-map.index.md
|
||||||
|
|
||||||
|
## role
|
||||||
|
Provides a mobile-specific UI for users to view and edit their profile information.
|
||||||
|
## files
|
||||||
|
- ProfileMobileView.tsx | Renders a mobile-optimized profile editing form with avatar upload, name/email fields, and save/cancel functionality. | exp: ProfileMobileView | dep: react-router-dom, ../mobile/mobile-edit-view, ../../icon, ../../../api/profile
|
||||||
|
## arch
|
||||||
|
Single feature-focused component with form handling and file upload, likely using controlled inputs and local state management.
|
||||||
|
## tags
|
||||||
|
mobile, profile, view, renders, optimized, editing, form, avatar
|
||||||
|
## symbols
|
||||||
|
- ProfileMobileView
|
||||||
|
## workflows
|
||||||
|
- change profile behavior
|
||||||
|
read: ProfileMobileView.tsx
|
||||||
|
## dirty
|
||||||
|
-
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { MobileEditView } from "../mobile/mobile-edit-view";
|
||||||
|
import { Icon } from "../../icon";
|
||||||
|
import type { UserProfile } from "../../../api/profile";
|
||||||
|
|
||||||
|
interface ProfileMobileViewProps {
|
||||||
|
profile: UserProfile;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
error: string | null;
|
||||||
|
isSaving: boolean;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
fileInputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
onNameChange: (value: string) => void;
|
||||||
|
onEmailChange: (value: string) => void;
|
||||||
|
onAvatarButtonClick: () => void;
|
||||||
|
onAvatarChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
onSave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileMobileView = ({
|
||||||
|
profile,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
avatarUrl,
|
||||||
|
fileInputRef,
|
||||||
|
onNameChange,
|
||||||
|
onEmailChange,
|
||||||
|
onAvatarButtonClick,
|
||||||
|
onAvatarChange,
|
||||||
|
onSave,
|
||||||
|
}: ProfileMobileViewProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileEditView
|
||||||
|
title="Profile"
|
||||||
|
onCancel={() => navigate(-1)}
|
||||||
|
onSave={onSave}
|
||||||
|
isSaving={isSaving}
|
||||||
|
>
|
||||||
|
<div className="profile-mobile-avatar-section">
|
||||||
|
<div className="profile-mobile-avatar">
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img alt="Avatar" className="profile-mobile-avatar-image" src={avatarUrl} />
|
||||||
|
) : (
|
||||||
|
<div className="profile-mobile-avatar-placeholder">
|
||||||
|
{profile.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="secondary-button"
|
||||||
|
disabled={isSaving}
|
||||||
|
onClick={onAvatarButtonClick}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<Icon name="loading" size="sm" />
|
||||||
|
Uploading...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="edit" size="sm" />
|
||||||
|
Change Avatar
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
accept="image/png,image/jpeg"
|
||||||
|
onChange={onAvatarChange}
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label" htmlFor="profile-mobile-name">
|
||||||
|
Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="mobile-form-input"
|
||||||
|
disabled={isSaving}
|
||||||
|
id="profile-mobile-name"
|
||||||
|
onChange={(e) => onNameChange(e.target.value)}
|
||||||
|
placeholder="Your name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label" htmlFor="profile-mobile-email">
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="mobile-form-input"
|
||||||
|
disabled={isSaving}
|
||||||
|
id="profile-mobile-email"
|
||||||
|
onChange={(e) => onEmailChange(e.target.value)}
|
||||||
|
placeholder="your.email@example.com"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="mobile-form-error">
|
||||||
|
<Icon name="warning" size="sm" />
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</MobileEditView>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -363,32 +363,34 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
let startY = 0;
|
let startY = 0;
|
||||||
let startX = 0;
|
let startX = 0;
|
||||||
let isScrolling = false;
|
let isScrolling = false;
|
||||||
let debugOverlay: HTMLDivElement | null = null;
|
let scrollPending = 0;
|
||||||
const showDebug = (msg: string) => {
|
const WHEEL_DISTANCE = 12;
|
||||||
if (!debugOverlay) {
|
|
||||||
debugOverlay = document.createElement("div");
|
const flushScroll = (force = false) => {
|
||||||
debugOverlay.style.position = "fixed";
|
if (scrollPending === 0) return;
|
||||||
debugOverlay.style.bottom = "120px";
|
if (!force && Math.abs(scrollPending) < WHEEL_DISTANCE) return;
|
||||||
debugOverlay.style.left = "8px";
|
const direction = Math.sign(scrollPending);
|
||||||
debugOverlay.style.right = "8px";
|
const steps = Math.max(
|
||||||
debugOverlay.style.zIndex = "9999";
|
1,
|
||||||
debugOverlay.style.background = "rgba(0,0,0,0.8)";
|
Math.floor(Math.abs(scrollPending) / WHEEL_DISTANCE),
|
||||||
debugOverlay.style.color = "#0f0";
|
);
|
||||||
debugOverlay.style.fontFamily = "monospace";
|
const ws = wsRef.current;
|
||||||
debugOverlay.style.fontSize = "11px";
|
if (ws?.readyState === WebSocket.OPEN && termRef.current) {
|
||||||
debugOverlay.style.padding = "8px";
|
const buf = termRef.current.buffer.active;
|
||||||
debugOverlay.style.borderRadius = "6px";
|
const col = buf.cursorX + 1;
|
||||||
debugOverlay.style.pointerEvents = "none";
|
const row = buf.cursorY + 1;
|
||||||
debugOverlay.style.whiteSpace = "pre-wrap";
|
const btn = direction > 0 ? 64 : 65;
|
||||||
debugOverlay.style.maxHeight = "120px";
|
for (let i = 0; i < steps; i++) {
|
||||||
debugOverlay.style.overflow = "auto";
|
ws.send(`\x1b[<${btn};${col};${row}M`);
|
||||||
document.body.appendChild(debugOverlay);
|
}
|
||||||
}
|
}
|
||||||
const line = `${new Date().toLocaleTimeString()} ${msg}`;
|
scrollPending = 0;
|
||||||
debugOverlay.textContent =
|
};
|
||||||
line +
|
|
||||||
"\n" +
|
const isAlternateScreen = () => {
|
||||||
debugOverlay.textContent?.split("\n").slice(0, 8).join("\n");
|
const term = termRef.current;
|
||||||
|
if (!term) return false;
|
||||||
|
return term.buffer.active === term.buffer.alternate;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTouchStart = (e: TouchEvent) => {
|
const onTouchStart = (e: TouchEvent) => {
|
||||||
@@ -396,6 +398,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
startY = e.touches[0].clientY;
|
startY = e.touches[0].clientY;
|
||||||
startX = e.touches[0].clientX;
|
startX = e.touches[0].clientX;
|
||||||
isScrolling = false;
|
isScrolling = false;
|
||||||
|
scrollPending = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onTouchMove = (e: TouchEvent) => {
|
const onTouchMove = (e: TouchEvent) => {
|
||||||
@@ -403,7 +406,6 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const deltaY = startY - touch.clientY;
|
const deltaY = startY - touch.clientY;
|
||||||
const deltaX = Math.abs(startX - touch.clientX);
|
const deltaX = Math.abs(startX - touch.clientX);
|
||||||
showDebug(`move dy=${deltaY.toFixed(0)} dx=${deltaX.toFixed(0)} scrolling=${isScrolling}`);
|
|
||||||
|
|
||||||
// Decide early whether this is a vertical scroll gesture.
|
// Decide early whether this is a vertical scroll gesture.
|
||||||
if (!isScrolling) {
|
if (!isScrolling) {
|
||||||
@@ -413,34 +415,32 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
}
|
}
|
||||||
if (!isScrolling) return;
|
if (!isScrolling) return;
|
||||||
|
|
||||||
// Take over the gesture so the browser never scrolls the
|
// Take over the gesture so the page/toolbar never scrolls.
|
||||||
// page or the terminal canvas. We synthesize wheel events
|
|
||||||
// and dispatch them into xterm.js, which already handles
|
|
||||||
// both normal buffer scrolling and alternate-screen apps
|
|
||||||
// (tmux/vim) correctly.
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const target = container.querySelector(".xterm-viewport") as
|
if (isAlternateScreen()) {
|
||||||
| HTMLElement
|
// Alternate screen (tmux/vim): accumulate the swipe and
|
||||||
| null;
|
// send SGR 1006 mouse-wheel events in steps.
|
||||||
if (target) {
|
scrollPending += deltaY;
|
||||||
showDebug(`dispatch wheel deltaY=${deltaY.toFixed(0)}`);
|
flushScroll();
|
||||||
const wheel = new WheelEvent("wheel", {
|
} else if (termRef.current) {
|
||||||
deltaY,
|
// Normal buffer: let xterm.js scroll its own viewport by
|
||||||
deltaX: 0,
|
// the number of lines corresponding to the swipe distance.
|
||||||
deltaZ: 0,
|
const lineHeight =
|
||||||
clientX: touch.clientX,
|
termRef.current.options.fontSize != null
|
||||||
clientY: touch.clientY,
|
? (termRef.current.options.fontSize as number) * 1.2
|
||||||
screenX: touch.screenX,
|
: 10;
|
||||||
screenY: touch.screenY,
|
const lines = Math.round(deltaY / lineHeight);
|
||||||
bubbles: true,
|
if (lines !== 0) {
|
||||||
cancelable: true,
|
termRef.current.scrollLines(lines);
|
||||||
});
|
}
|
||||||
target.dispatchEvent(wheel);
|
|
||||||
}
|
}
|
||||||
startY = touch.clientY;
|
startY = touch.clientY;
|
||||||
};
|
};
|
||||||
const onTouchEnd = () => {
|
const onTouchEnd = () => {
|
||||||
|
if (isScrolling) {
|
||||||
|
flushScroll(true);
|
||||||
|
}
|
||||||
isScrolling = false;
|
isScrolling = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/tool-workshop
|
dir: apps/web/src/components/features/tool-workshop
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides a complete UI for managing custom tool types in a "Tool Workshop" interface with list, edit, and mobile-responsive views.
|
Provides a complete UI for managing custom tool types (compose, dockerfile, manifest) in a workshop interface with list, edit, and mobile-responsive views.
|
||||||
## 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/tool-workshop
|
|||||||
index: apps/web/src/components/features/tool-workshop/.pi-map.index.md
|
index: apps/web/src/components/features/tool-workshop/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides a complete UI for managing custom tool types in a "Tool Workshop" interface with list, edit, and mobile-responsive views.
|
Provides a complete UI for managing custom tool types (compose, dockerfile, manifest) in a workshop interface with list, edit, and mobile-responsive views.
|
||||||
## files
|
## files
|
||||||
- ToolTypeEditorPanel.tsx | Renders a form panel for creating or editing tool types with support for compose, dockerfile, and manifest definition types | exp: ToolTypeFormState, ToolTypeEditorPanel | dep: ../../icon, ../tool/manifest-editor, ../../../api/tool-types, ../../../api/tool-definitions, React, Icon, ManifestEditor
|
- ToolTypeEditorPanel.tsx | Renders a form panel for creating or editing tool types with support for compose, dockerfile, and manifest definition types | exp: ToolTypeFormState, ToolTypeEditorPanel | dep: ../../icon, ../tool/manifest-editor, ../../../api/tool-types, ../../../api/tool-definitions, React, Icon, ManifestEditor
|
||||||
- ToolTypeListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting tool types in a "Tool Workshop" interface. | exp: ToolTypeListSidebar | dep: ../../icon, ../../../api/tool-types, React, Icon component, ToolType type
|
- ToolTypeListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting tool types in a "Tool Workshop" interface. | exp: ToolTypeListSidebar | dep: ../../icon, ../../../api/tool-types, React, Icon component, ToolType type
|
||||||
- ToolWorkshopMobileView.tsx | Renders a mobile-responsive view for managing tool types with list, detail, and edit modes | exp: MobileView, ToolWorkshopMobileView | dep: ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../../../api/tool-types, ./ToolTypeEditorPanel, MobileListView, MobileDetailView, MobileEditView, MobileFAB, ToolType, ToolTypeFormState
|
- ToolWorkshopMobileView.tsx | Renders a mobile-responsive workshop interface for managing tool types with list, detail, and edit views. | exp: MobileView, ToolWorkshopMobileView | dep: react, ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../tool/manifest-editor, ../../icon, ../../../api/tool-types, ./ToolTypeEditorPanel, ../../../api/tool-definitions, mobile-list-view, mobile-detail-view, mobile-edit-view, mobile-fab, manifest-editor, icon, tool-types, tool-definitions
|
||||||
## arch
|
## arch
|
||||||
Uses a split-pane sidebar/detail panel pattern with dedicated mobile breakpoint handling, separating list navigation from form editing concerns across three specialized view components.
|
Feature-based component composition with split-pane layout (sidebar + editor panel), responsive mobile adaptation, and CRUD operations for tool type definitions.
|
||||||
## tags
|
## tags
|
||||||
tool, mobile, type, view, types, list, panel, editor
|
tool, mobile, view, type, types, editor, list, workshop
|
||||||
## symbols
|
## symbols
|
||||||
- ToolTypeFormState
|
- ToolTypeFormState
|
||||||
- ToolTypeEditorPanel
|
- ToolTypeEditorPanel
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
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 { ManifestEditor } from "../tool/manifest-editor";
|
||||||
|
import { Icon } from "../../icon";
|
||||||
import type { ToolType } from "../../../api/tool-types";
|
import type { ToolType } from "../../../api/tool-types";
|
||||||
import type { ToolTypeFormState } from "./ToolTypeEditorPanel";
|
import type { ToolTypeFormState } from "./ToolTypeEditorPanel";
|
||||||
|
import type { ToolDefinitionManifest } from "../../../api/tool-definitions";
|
||||||
|
|
||||||
export type MobileView = "list" | "detail" | "edit";
|
export type MobileView = "list" | "detail" | "edit";
|
||||||
|
|
||||||
@@ -13,13 +18,18 @@ interface ToolWorkshopMobileViewProps {
|
|||||||
mobileView: MobileView;
|
mobileView: MobileView;
|
||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
toolTypeForm: ToolTypeFormState;
|
toolTypeForm: ToolTypeFormState;
|
||||||
|
manifestData: Record<string, unknown> | null;
|
||||||
|
manifestDefinitionId: string | null;
|
||||||
|
baseDefinitions: ToolDefinitionManifest[];
|
||||||
toolTypeError: string | null;
|
toolTypeError: string | null;
|
||||||
|
toolTypeDirty: boolean;
|
||||||
onViewChange: (view: MobileView) => void;
|
onViewChange: (view: MobileView) => void;
|
||||||
onSelect: (toolType: ToolType) => void;
|
onSelect: (toolType: ToolType) => void;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
||||||
onSubmit: () => void;
|
onManifestChange: (manifest: Record<string, unknown> | null) => void;
|
||||||
|
onSubmit: (e?: React.FormEvent) => Promise<boolean>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,15 +39,41 @@ export const ToolWorkshopMobileView = ({
|
|||||||
mobileView,
|
mobileView,
|
||||||
isCreating,
|
isCreating,
|
||||||
toolTypeForm,
|
toolTypeForm,
|
||||||
|
manifestData,
|
||||||
|
manifestDefinitionId,
|
||||||
|
baseDefinitions,
|
||||||
toolTypeError,
|
toolTypeError,
|
||||||
|
toolTypeDirty,
|
||||||
onViewChange,
|
onViewChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
onFormChange,
|
onFormChange,
|
||||||
|
onManifestChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: ToolWorkshopMobileViewProps) => {
|
}: ToolWorkshopMobileViewProps) => {
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const ok = await onSubmit();
|
||||||
|
if (ok) {
|
||||||
|
onViewChange(isCreating ? "list" : "edit");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedToolType) {
|
||||||
|
void onDelete(selectedToolType.id);
|
||||||
|
onViewChange("list");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (mobileView === "list") {
|
if (mobileView === "list") {
|
||||||
return (
|
return (
|
||||||
<div className="mobile-page">
|
<div className="mobile-page">
|
||||||
@@ -49,15 +85,16 @@ export const ToolWorkshopMobileView = ({
|
|||||||
items={toolTypes.map((t) => ({
|
items={toolTypes.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
title: t.display_name,
|
title: t.display_name,
|
||||||
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
subtitle: `${t.category || "Uncategorized"} · ${t.definition_type} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||||
}))}
|
}))}
|
||||||
onItemClick={(id) => {
|
onItemClick={(id) => {
|
||||||
const toolType = toolTypes.find((t) => t.id === id);
|
const toolType = toolTypes.find((t) => t.id === id);
|
||||||
if (toolType) {
|
if (toolType) {
|
||||||
onSelect(toolType);
|
onSelect(toolType);
|
||||||
onViewChange("detail");
|
onViewChange("edit");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onItemDelete={(id) => onDelete(id)}
|
||||||
emptyMessage="No tool types yet"
|
emptyMessage="No tool types yet"
|
||||||
/>
|
/>
|
||||||
<MobileFAB
|
<MobileFAB
|
||||||
@@ -71,64 +108,70 @@ export const ToolWorkshopMobileView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mobileView === "detail" && selectedToolType) {
|
if (mobileView === "detail" && selectedToolType) {
|
||||||
|
const fields = [
|
||||||
|
{ label: "Name", value: selectedToolType.name },
|
||||||
|
{ label: "Display Name", value: selectedToolType.display_name },
|
||||||
|
{ label: "Description", value: selectedToolType.description },
|
||||||
|
{ label: "Category", value: selectedToolType.category },
|
||||||
|
{ label: "Interface Type", value: selectedToolType.interface_type },
|
||||||
|
{
|
||||||
|
label: "Requires Port",
|
||||||
|
value: selectedToolType.requires_port,
|
||||||
|
type: "boolean" as const,
|
||||||
|
},
|
||||||
|
...(selectedToolType.requires_port
|
||||||
|
? [{ label: "Default Port", value: selectedToolType.default_port }]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
label: "Definition Type",
|
||||||
|
value: selectedToolType.definition_type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Startup Command",
|
||||||
|
value: selectedToolType.startup_command,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Command",
|
||||||
|
value: selectedToolType.readiness_probe?.command ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Timeout",
|
||||||
|
value: selectedToolType.readiness_probe?.timeout ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Interval",
|
||||||
|
value: selectedToolType.readiness_probe?.interval ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Required Variables",
|
||||||
|
value: selectedToolType.required_variables?.join(", ") ?? null,
|
||||||
|
},
|
||||||
|
...(selectedToolType.definition_type !== "manifest"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
selectedToolType.definition_type === "compose"
|
||||||
|
? "Compose Template"
|
||||||
|
: "Dockerfile Template",
|
||||||
|
value:
|
||||||
|
selectedToolType.definition_type === "compose"
|
||||||
|
? selectedToolType.compose_template
|
||||||
|
: selectedToolType.dockerfile_template,
|
||||||
|
type: "code" as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileDetailView
|
<MobileDetailView
|
||||||
title={selectedToolType.display_name}
|
title={selectedToolType.display_name}
|
||||||
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type} · ${selectedToolType.interface_type === "web" ? `Port ${selectedToolType.default_port}` : "Terminal"}`}
|
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type}`}
|
||||||
fields={[
|
fields={fields}
|
||||||
{ label: "Name", value: selectedToolType.name },
|
|
||||||
{ label: "Display Name", value: selectedToolType.display_name },
|
|
||||||
{ label: "Description", value: selectedToolType.description },
|
|
||||||
{ label: "Category", value: selectedToolType.category },
|
|
||||||
{ label: "Interface Type", value: selectedToolType.interface_type },
|
|
||||||
{
|
|
||||||
label: "Requires Port",
|
|
||||||
value: selectedToolType.requires_port,
|
|
||||||
type: "boolean",
|
|
||||||
},
|
|
||||||
{ label: "Default Port", value: selectedToolType.default_port },
|
|
||||||
{
|
|
||||||
label: "Definition Type",
|
|
||||||
value: selectedToolType.definition_type,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Startup Command",
|
|
||||||
value: selectedToolType.startup_command,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Readiness Command",
|
|
||||||
value: selectedToolType.readiness_probe?.command ?? null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Readiness Timeout",
|
|
||||||
value: selectedToolType.readiness_probe?.timeout ?? null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Readiness Interval",
|
|
||||||
value: selectedToolType.readiness_probe?.interval ?? null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Required Variables",
|
|
||||||
value: selectedToolType.required_variables?.join(", ") ?? null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Compose Template",
|
|
||||||
value: selectedToolType.compose_template,
|
|
||||||
type: "code",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Dockerfile Template",
|
|
||||||
value: selectedToolType.dockerfile_template,
|
|
||||||
type: "code",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
onViewChange("edit");
|
onViewChange("edit");
|
||||||
}}
|
}}
|
||||||
onDelete={() => {
|
onDelete={handleDelete}
|
||||||
void onDelete(selectedToolType.id);
|
|
||||||
onViewChange("list");
|
|
||||||
}}
|
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
onViewChange("list");
|
onViewChange("list");
|
||||||
}}
|
}}
|
||||||
@@ -137,18 +180,41 @@ export const ToolWorkshopMobileView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mobileView === "edit") {
|
if (mobileView === "edit") {
|
||||||
|
const templateValue =
|
||||||
|
toolTypeForm.definition_type === "compose"
|
||||||
|
? toolTypeForm.compose_template
|
||||||
|
: toolTypeForm.dockerfile_template;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileEditView
|
<MobileEditView
|
||||||
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
|
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onSave={() => {
|
onSave={handleSave}
|
||||||
onSubmit();
|
onDelete={isCreating ? undefined : handleDelete}
|
||||||
if (!toolTypeError) {
|
deleteLabel="Delete Tool Type"
|
||||||
onViewChange("list");
|
isSaving={isSaving}
|
||||||
}
|
|
||||||
}}
|
|
||||||
isSaving={false}
|
|
||||||
>
|
>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Definition Type</label>
|
||||||
|
<select
|
||||||
|
value={toolTypeForm.definition_type}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFormChange({
|
||||||
|
definition_type: e.target.value as
|
||||||
|
| "compose"
|
||||||
|
| "dockerfile"
|
||||||
|
| "manifest",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="mobile-form-select"
|
||||||
|
disabled={!isCreating}
|
||||||
|
>
|
||||||
|
<option value="compose">Docker Compose</option>
|
||||||
|
<option value="dockerfile">Dockerfile</option>
|
||||||
|
<option value="manifest">Manifest (Declarative)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Name *</label>
|
<label className="mobile-form-label">Name *</label>
|
||||||
<input
|
<input
|
||||||
@@ -157,8 +223,11 @@ export const ToolWorkshopMobileView = ({
|
|||||||
onChange={(e) => onFormChange({ name: e.target.value })}
|
onChange={(e) => onFormChange({ name: e.target.value })}
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="e.g., my-tool"
|
placeholder="e.g., my-tool"
|
||||||
|
disabled={!isCreating}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Display Name *</label>
|
<label className="mobile-form-label">Display Name *</label>
|
||||||
<input
|
<input
|
||||||
@@ -167,8 +236,10 @@ export const ToolWorkshopMobileView = ({
|
|||||||
onChange={(e) => onFormChange({ display_name: e.target.value })}
|
onChange={(e) => onFormChange({ display_name: e.target.value })}
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="e.g., My Tool"
|
placeholder="e.g., My Tool"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Description</label>
|
<label className="mobile-form-label">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -179,6 +250,7 @@ export const ToolWorkshopMobileView = ({
|
|||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Category</label>
|
<label className="mobile-form-label">Category</label>
|
||||||
<input
|
<input
|
||||||
@@ -189,130 +261,179 @@ export const ToolWorkshopMobileView = ({
|
|||||||
placeholder="e.g., development"
|
placeholder="e.g., development"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Interface Type</label>
|
<label className="mobile-form-label">Interface Type</label>
|
||||||
<select
|
<select
|
||||||
value={toolTypeForm.interface_type}
|
value={toolTypeForm.interface_type}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
const value = e.target.value as "web" | "terminal";
|
||||||
onFormChange({
|
onFormChange({
|
||||||
interface_type: e.target.value as "web" | "terminal",
|
interface_type: value,
|
||||||
})
|
requires_port: value === "web",
|
||||||
}
|
default_port: value === "web" ? toolTypeForm.default_port : "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
className="mobile-form-select"
|
className="mobile-form-select"
|
||||||
>
|
>
|
||||||
<option value="web">Web</option>
|
<option value="web">Web</option>
|
||||||
<option value="terminal">Terminal</option>
|
<option value="terminal">Terminal</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label">Requires Port</label>
|
<div className="mobile-form-group mobile-form-row">
|
||||||
<input
|
<label className="mobile-form-label mobile-form-checkbox-label">
|
||||||
type="checkbox"
|
<input
|
||||||
checked={toolTypeForm.requires_port}
|
type="checkbox"
|
||||||
onChange={(e) => onFormChange({ requires_port: e.target.checked })}
|
checked={toolTypeForm.requires_port}
|
||||||
className="mobile-form-checkbox"
|
onChange={(e) =>
|
||||||
/>
|
onFormChange({ requires_port: e.target.checked })
|
||||||
</div>
|
}
|
||||||
<div className="mobile-form-group">
|
className="mobile-form-checkbox"
|
||||||
<label className="mobile-form-label">Default Port</label>
|
/>
|
||||||
<input
|
Requires Port
|
||||||
type="text"
|
</label>
|
||||||
value={toolTypeForm.default_port}
|
|
||||||
onChange={(e) => onFormChange({ default_port: e.target.value })}
|
|
||||||
className="mobile-form-input"
|
|
||||||
placeholder="e.g., 8080"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label">Definition Type</label>
|
|
||||||
<select
|
|
||||||
value={toolTypeForm.definition_type}
|
|
||||||
onChange={(e) =>
|
|
||||||
onFormChange({
|
|
||||||
definition_type: e.target.value as "compose" | "dockerfile",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="mobile-form-select"
|
|
||||||
>
|
|
||||||
<option value="compose">Compose</option>
|
|
||||||
<option value="dockerfile">Dockerfile</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label">Startup Command</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={toolTypeForm.startup_command}
|
|
||||||
onChange={(e) => onFormChange({ startup_command: e.target.value })}
|
|
||||||
className="mobile-form-input"
|
|
||||||
placeholder="Command to run on startup"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{toolTypeForm.requires_port && (
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Default Port *</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={toolTypeForm.default_port}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFormChange({ default_port: e.target.value })
|
||||||
|
}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="e.g., 8080"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toolTypeForm.interface_type === "terminal" && (
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Startup Command</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.startup_command}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFormChange({ startup_command: e.target.value })
|
||||||
|
}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="Command to run on startup"
|
||||||
|
/>
|
||||||
|
<small className="mobile-form-help">
|
||||||
|
Command to run before the interactive shell for each new
|
||||||
|
terminal session.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toolTypeForm.definition_type === "manifest" ? (
|
||||||
|
<div className="mobile-manifest-editor-wrapper">
|
||||||
|
<ManifestEditor
|
||||||
|
manifest={manifestData}
|
||||||
|
baseDefinitions={baseDefinitions}
|
||||||
|
onChange={(m) => onManifestChange(m)}
|
||||||
|
definitionId={manifestDefinitionId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">
|
||||||
|
{toolTypeForm.definition_type === "compose"
|
||||||
|
? "Compose Template *"
|
||||||
|
: "Dockerfile Template *"}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={templateValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (toolTypeForm.definition_type === "compose") {
|
||||||
|
onFormChange({ compose_template: e.target.value });
|
||||||
|
} else {
|
||||||
|
onFormChange({ dockerfile_template: e.target.value });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mobile-form-textarea mobile-form-code"
|
||||||
|
placeholder={
|
||||||
|
toolTypeForm.definition_type === "compose"
|
||||||
|
? "version: '3'"
|
||||||
|
: "FROM ubuntu:22.04"
|
||||||
|
}
|
||||||
|
rows={10}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Readiness Command</label>
|
<label className="mobile-form-label">Readiness Command</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={toolTypeForm.readiness_command}
|
value={toolTypeForm.readiness_command}
|
||||||
onChange={(e) => onFormChange({ readiness_command: e.target.value })}
|
onChange={(e) =>
|
||||||
|
onFormChange({ readiness_command: e.target.value })
|
||||||
|
}
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="e.g., curl -f http://localhost:8080/health"
|
placeholder="e.g., curl -f http://localhost:8080/health"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Readiness Timeout</label>
|
<label className="mobile-form-label">Readiness Timeout (seconds)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="number"
|
||||||
value={toolTypeForm.readiness_timeout}
|
value={toolTypeForm.readiness_timeout}
|
||||||
onChange={(e) => onFormChange({ readiness_timeout: e.target.value })}
|
onChange={(e) =>
|
||||||
|
onFormChange({ readiness_timeout: e.target.value })
|
||||||
|
}
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Readiness Interval</label>
|
<label className="mobile-form-label">Readiness Interval (seconds)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="number"
|
||||||
value={toolTypeForm.readiness_interval}
|
value={toolTypeForm.readiness_interval}
|
||||||
onChange={(e) => onFormChange({ readiness_interval: e.target.value })}
|
onChange={(e) =>
|
||||||
|
onFormChange({ readiness_interval: e.target.value })
|
||||||
|
}
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="2"
|
placeholder="2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Required Variables</label>
|
<label className="mobile-form-label">Required Variables</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={toolTypeForm.required_variables}
|
value={toolTypeForm.required_variables}
|
||||||
onChange={(e) => onFormChange({ required_variables: e.target.value })}
|
onChange={(e) =>
|
||||||
|
onFormChange({ required_variables: e.target.value })
|
||||||
|
}
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="VAR1, VAR2, VAR3"
|
placeholder="VAR1, VAR2, VAR3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{toolTypeForm.definition_type === "compose" && (
|
|
||||||
<div className="mobile-form-group">
|
{toolTypeError && (
|
||||||
<label className="mobile-form-label">Compose Template</label>
|
<p className="mobile-form-error">
|
||||||
<textarea
|
<Icon name="warning" size="sm" /> {toolTypeError}
|
||||||
value={toolTypeForm.compose_template}
|
</p>
|
||||||
onChange={(e) => onFormChange({ compose_template: e.target.value })}
|
|
||||||
className="mobile-form-textarea mobile-form-code"
|
|
||||||
placeholder="version: '3'"
|
|
||||||
rows={10}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{toolTypeForm.definition_type === "dockerfile" && (
|
|
||||||
<div className="mobile-form-group">
|
{toolTypeDirty && (
|
||||||
<label className="mobile-form-label">Dockerfile Template</label>
|
<button
|
||||||
<textarea
|
type="button"
|
||||||
value={toolTypeForm.dockerfile_template}
|
className="secondary-button mobile-discard-button"
|
||||||
onChange={(e) =>
|
onClick={onCancel}
|
||||||
onFormChange({ dockerfile_template: e.target.value })
|
disabled={isSaving}
|
||||||
}
|
>
|
||||||
className="mobile-form-textarea mobile-form-code"
|
Discard Changes
|
||||||
placeholder="FROM ubuntu:22.04"
|
</button>
|
||||||
rows={10}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</MobileEditView>
|
</MobileEditView>
|
||||||
);
|
);
|
||||||
@@ -328,15 +449,16 @@ export const ToolWorkshopMobileView = ({
|
|||||||
items={toolTypes.map((t) => ({
|
items={toolTypes.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
title: t.display_name,
|
title: t.display_name,
|
||||||
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
subtitle: `${t.category || "Uncategorized"} · ${t.definition_type} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||||
}))}
|
}))}
|
||||||
onItemClick={(id) => {
|
onItemClick={(id) => {
|
||||||
const toolType = toolTypes.find((t) => t.id === id);
|
const toolType = toolTypes.find((t) => t.id === id);
|
||||||
if (toolType) {
|
if (toolType) {
|
||||||
onSelect(toolType);
|
onSelect(toolType);
|
||||||
onViewChange("detail");
|
onViewChange("edit");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onItemDelete={(id) => onDelete(id)}
|
||||||
emptyMessage="No tool types yet"
|
emptyMessage="No tool types yet"
|
||||||
/>
|
/>
|
||||||
<MobileFAB
|
<MobileFAB
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/hooks
|
dir: apps/web/src/hooks
|
||||||
|
|
||||||
## role
|
## role
|
||||||
A collection of custom React hooks providing reusable state management, API integration, and UI behavior logic 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,15 +4,15 @@ 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
|
||||||
A collection of custom React hooks providing reusable state management, API integration, and UI behavior logic 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 custom hook that manages Server-Sent Events connections with automatic reconnection, backoff strategies, and error handling. | dep: vitest, @testing-library/react, ./use-events, ../api/events, use-events hook, ../api/events module
|
- use-events.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 exponential backoff reconnection, event buffering, and auth/rate-limit handling. | exp: UseEventsReturn, func:useEvents() → UseEventsReturn, call:useState, call:useRef, call:useCallback, call:clearTimeout, call:createEventSource, call:setConnected, call:setError, call:setReconnectCount, call:JSON.parse, call:setEvents, call:es.close, call:Math.min, call:Math.pow, call:Math.random, call:setTimeout, call:probeEventStreamStatus, call:window.location.assign, call:connect, call:useEffect, call:esRef.current.close | dep: react, ../api/events, ../types/events
|
- use-events.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
|
||||||
- use-instance-actions.ts | A custom React hook that provides a centralized interface for managing instance/session actions including opening, starting, stopping, deleting, force-deleting, recreating tunnels, and renaming, with loading states and dirty delete handling for conflict resolution. | exp: func:useInstanceActions(options: UseInstanceActionsOptions) → UseInstanceActionsReturn, call:useSessions, call:useSessionOperations, call:useState, call:useRef, call:useCallback, call:tabRefs.current.get, call:existing.focus, call:session.tool_type_interfaces?.includes, call:window.open, call:tabRefs.current.set, call:setLoadingSessionId, call:startOperation, call:startInstance, call:onRefresh, call:completeOperation, call:stopInstance, call:deleteInstance, call:setDirtyDeleteSession, call:setDirtyDeleteFiles, call:removeSession, call:recreateInstanceTunnel, call:alert, call:newName.trim, call:renameInstance | dep: react, ../api/sessions, ../state/sessions, ../state/session-operations
|
- use-instance-actions.ts | A React custom hook that manages instance/session actions including opening, starting, stopping, deleting, force-deleting, recreating tunnels, and renaming with loading states and dirty delete handling. | exp: func:useInstanceActions(options: UseInstanceActionsOptions) → UseInstanceActionsReturn, call:useSessions, call:useState, call:useRef, call:useCallback, call:tabRefs.current.get, call:existing.focus, call:session.tool_type_interfaces?.includes, call:window.open, call:tabRefs.current.set, call:setLoadingSessionId, call:startInstance, call:onRefresh, call:stopInstance, call:deleteInstance, call:setDirtyDeleteSession, call:setDirtyDeleteFiles, call:removeSession, call:recreateInstanceTunnel, call:alert, call:newName.trim, call:renameInstance | dep: react, ../api/sessions, ../state/sessions
|
||||||
- use-mobile-viewport.ts | A React hook that tracks whether the viewport width is below a mobile breakpoint (768px) | exp: func:useMobileViewport(), call:useState, call:useEffect, call:setIsMobile, call:window.addEventListener, call:window.removeEventListener | dep: react
|
- use-mobile-viewport.ts | A React hook that tracks whether the viewport width is below a mobile breakpoint (768px) | exp: func:useMobileViewport(), call:useState, call:useEffect, call:setIsMobile, call:window.addEventListener, call:window.removeEventListener | dep: react
|
||||||
- use-notifications.test.tsx | Tests the useNotifications custom React hook with mocked API calls, covering optimistic updates, polling behavior, and error handling. | dep: vitest, @testing-library/react, ./use-notifications, ../state/notifications, ../api/notifications, use-notifications
|
- use-notifications.test.tsx | Tests the useNotifications custom React hook with mocked API calls, covering optimistic updates, polling behavior, and error handling. | dep: vitest, @testing-library/react, ./use-notifications, ../state/notifications, ../api/notifications, use-notifications
|
||||||
- use-notifications.ts | Custom React hook that provides access to notification state and ensures it's used within a NotificationProvider | exp: func:useNotifications(), call:useContext, raise:Error | dep: react, ../state/notifications
|
- use-notifications.ts | Custom React hook that provides access to notification state and ensures it's used within a NotificationProvider | exp: func:useNotifications(), call:useContext, raise:Error | dep: react, ../state/notifications
|
||||||
@@ -23,7 +23,7 @@ A collection of custom React hooks providing reusable state management, API inte
|
|||||||
- use-terminal-page.ts | Manages terminal page state including sessions, keyboard shortcuts, fullscreen mode, mobile viewport handling, and terminal lifecycle operations. | exp: useTerminalPage | dep: react, react-router-dom, ../components/features/terminal/terminal, ../components/features/terminal/terminal-session-tabs, ./use-mobile-viewport, ./use-auto-hide, ./use-virtual-keyboard, ./use-terminal-sessions, ../api/terminal, ./use-special-keys, use-mobile-viewport, use-auto-hide, use-virtual-keyboard, use-terminal-sessions, terminal, terminal-session-tabs, api/terminal, api/sessions, use-special-keys
|
- use-terminal-page.ts | Manages terminal page state including sessions, keyboard shortcuts, fullscreen mode, mobile viewport handling, and terminal lifecycle operations. | exp: useTerminalPage | dep: react, react-router-dom, ../components/features/terminal/terminal, ../components/features/terminal/terminal-session-tabs, ./use-mobile-viewport, ./use-auto-hide, ./use-virtual-keyboard, ./use-terminal-sessions, ../api/terminal, ./use-special-keys, use-mobile-viewport, use-auto-hide, use-virtual-keyboard, use-terminal-sessions, terminal, terminal-session-tabs, api/terminal, api/sessions, use-special-keys
|
||||||
- use-terminal-sessions.ts | React custom hook that manages terminal session state (CRUD operations, active session tracking) for a given instance | exp: UseTerminalSessionsResult, func:useTerminalSessions(instanceId: string) → UseTerminalSessionsResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listTerminalSessions, call:setSessions, call:setActiveSessionId, call:createTerminalSession, call:closeTerminalSession, call:prev.filter, call:renameTerminalSession, call:prev.map, call:resetTerminalSession, call:loadSessions, call:useEffect | dep: react, ../api/terminal
|
- use-terminal-sessions.ts | React custom hook that manages terminal session state (CRUD operations, active session tracking) for a given instance | exp: UseTerminalSessionsResult, func:useTerminalSessions(instanceId: string) → UseTerminalSessionsResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listTerminalSessions, call:setSessions, call:setActiveSessionId, call:createTerminalSession, call:closeTerminalSession, call:prev.filter, call:renameTerminalSession, call:prev.map, call:resetTerminalSession, call:loadSessions, call:useEffect | dep: react, ../api/terminal
|
||||||
- use-theme.ts | React hook that fetches user theme preference on mount and applies it to the document root element via data-theme attribute | exp: func:useTheme(), call:useEffect, call:getUserConfig, call:document.documentElement.removeAttribute, call:document.documentElement.setAttribute | dep: react, ../api/settings
|
- use-theme.ts | React hook that fetches user theme preference on mount and applies it to the document root element via data-theme attribute | exp: func:useTheme(), call:useEffect, call:getUserConfig, call:document.documentElement.removeAttribute, call:document.documentElement.setAttribute | dep: react, ../api/settings
|
||||||
- use-tool-workshop.ts | React custom hook that manages state and operations for a tool workshop UI, including CRUD operations for tool types and tool definitions with form handling and validation. | exp: useToolWorkshop | dep: react, ../utils/errors, ../api/tool-types, ../api/tool-definitions, ../components/features/tool-workshop/ToolTypeEditorPanel
|
- use-tool-workshop.ts | Custom React hook that manages state and operations for a tool workshop UI, including CRUD operations for tool types and tool definitions with form handling and validation. | exp: useToolWorkshop | dep: react, ../utils/errors, ../api/tool-types, ../api/tool-definitions, ../components/features/tool-workshop/ToolTypeEditorPanel
|
||||||
- use-virtual-keyboard.ts | React hook that detects virtual keyboard open/close state and measures its height on mobile devices | exp: func:useVirtualKeyboard(), call:useState, call:useCallback, call:setState, call:useEffect, call:visualViewport.addEventListener, call:window.addEventListener, call:updateKeyboardState, call:visualViewport.removeEventListener, call:window.removeEventListener | dep: react
|
- use-virtual-keyboard.ts | React hook that detects virtual keyboard open/close state and measures its height on mobile devices | exp: func:useVirtualKeyboard(), call:useState, call:useCallback, call:setState, call:useEffect, call:visualViewport.addEventListener, call:window.addEventListener, call:updateKeyboardState, call:visualViewport.removeEventListener, call:window.removeEventListener | dep: react
|
||||||
- use-workspace-actions.ts | Provides a React hook that encapsulates workspace CRUD operations with loading state management and user confirmation dialogs for destructive actions. | exp: UseWorkspaceActionsResult, func:useWorkspaceActions() → UseWorkspaceActionsResult, call:useState, call:useCallback, call:createWorkspace, call:setLoadingId, call:deleteWorkspace, call:onRefresh, call:window.confirm, call:instances.map((i) => `- ${i.name}`).join, call:syncWorkspace, call:updateWorkspace, raise:err | dep: react, ../api/workspaces, ../types/workspace
|
- use-workspace-actions.ts | Provides a React hook that encapsulates workspace CRUD operations with loading state management and user confirmation dialogs for destructive actions. | exp: UseWorkspaceActionsResult, func:useWorkspaceActions() → UseWorkspaceActionsResult, call:useState, call:useCallback, call:createWorkspace, call:setLoadingId, call:deleteWorkspace, call:onRefresh, call:window.confirm, call:instances.map((i) => `- ${i.name}`).join, call:syncWorkspace, call:updateWorkspace, raise:err | dep: react, ../api/workspaces, ../types/workspace
|
||||||
- use-workspace-files.ts | Custom React hook for managing workspace file operations including listing, loading, saving, and navigating files. | exp: UseWorkspaceFilesResult, func:useWorkspaceFiles(workspaceId: string) → UseWorkspaceFilesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceFiles, call:setEntries, call:setCurrentPath, call:setContent, call:getWorkspaceFileContent, call:saveWorkspaceFile, call:refresh, call:useEffect | dep: react, ../api/workspace-files
|
- use-workspace-files.ts | Custom React hook for managing workspace file operations including listing, loading, saving, and navigating files. | exp: UseWorkspaceFilesResult, func:useWorkspaceFiles(workspaceId: string) → UseWorkspaceFilesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceFiles, call:setEntries, call:setCurrentPath, call:setContent, call:getWorkspaceFileContent, call:saveWorkspaceFile, call:refresh, call:useEffect | dep: react, ../api/workspace-files
|
||||||
@@ -31,7 +31,7 @@ A collection of custom React hooks providing reusable state management, API inte
|
|||||||
- use-workspace-instances.ts | Custom React hook for managing workspace instances with CRUD operations, loading states, and error handling. | exp: UseWorkspaceInstancesResult, func:useWorkspaceInstances(workspaceId: string) → UseWorkspaceInstancesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceInstances, call:setInstances, call:createWorkspaceInstance, call:refresh, call:useEffect | dep: react, ../api/workspace-instances, ../api/sessions
|
- use-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 encapsulates a specific domain concern (data fetching, CRUD operations, terminal/session management, UI interactions), often combining React state with API calls, side effects, and provider context integration.
|
Follows a 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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -151,13 +151,13 @@ export const useToolWorkshop = () => {
|
|||||||
resetToolTypeForm();
|
resetToolTypeForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToolTypeSubmit = async (e?: React.FormEvent) => {
|
const handleToolTypeSubmit = async (e?: React.FormEvent): Promise<boolean> => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
setToolTypeError(null);
|
setToolTypeError(null);
|
||||||
|
|
||||||
if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) {
|
if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) {
|
||||||
setToolTypeError("Name and display name are required");
|
setToolTypeError("Name and display name are required");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -166,7 +166,7 @@ export const useToolWorkshop = () => {
|
|||||||
isNaN(Number(toolTypeForm.default_port)))
|
isNaN(Number(toolTypeForm.default_port)))
|
||||||
) {
|
) {
|
||||||
setToolTypeError("Default port is required and must be a number");
|
setToolTypeError("Default port is required and must be a number");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolTypeForm.definition_type !== "manifest") {
|
if (toolTypeForm.definition_type !== "manifest") {
|
||||||
@@ -179,13 +179,13 @@ export const useToolWorkshop = () => {
|
|||||||
setToolTypeError(
|
setToolTypeError(
|
||||||
`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`,
|
`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`,
|
||||||
);
|
);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (!manifestData) {
|
} else if (!manifestData) {
|
||||||
setToolTypeError(
|
setToolTypeError(
|
||||||
"Manifest data is required for manifest definition type",
|
"Manifest data is required for manifest definition type",
|
||||||
);
|
);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variables = toolTypeForm.required_variables
|
const variables = toolTypeForm.required_variables
|
||||||
@@ -307,8 +307,10 @@ export const useToolWorkshop = () => {
|
|||||||
setToolTypeDirty(false);
|
setToolTypeDirty(false);
|
||||||
}
|
}
|
||||||
await loadData();
|
await loadData();
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setToolTypeError(extractErrorMessage(err));
|
setToolTypeError(extractErrorMessage(err));
|
||||||
|
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's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations.
|
Contains top-level page components that render the main UI views for the web application, each corresponding to a distinct route and feature area.
|
||||||
## 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,30 +4,30 @@ 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's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations.
|
Contains top-level page components that render the main UI views for the web application, each corresponding to a distinct route and feature area.
|
||||||
## 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
|
||||||
- GitRepositoriesPage.tsx | Displays and manages a project's Git repositories with CRUD operations including listing, creating, navigating to history, and deleting with confirmation | exp: GitRepositoriesPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../components/features/project/repository-create-dialog, ../hooks/use-async-data
|
- GitRepositoriesPage.tsx | Displays and manages a project's Git repositories with CRUD operations including listing, creating, navigating to history, and deleting with confirmation | exp: GitRepositoriesPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../components/features/project/repository-create-dialog, ../hooks/use-async-data
|
||||||
- PlaceholderPage.tsx | Exports three simple React page components (PlaceholderPage, NotFoundPage, LoginRedirectPage) for a frontend scaffold. | exp: PlaceholderPage, NotFoundPage, LoginRedirectPage | dep: ../components/icon, React
|
- PlaceholderPage.tsx | Exports three simple React page components (PlaceholderPage, NotFoundPage, LoginRedirectPage) for a frontend scaffold. | exp: PlaceholderPage, NotFoundPage, LoginRedirectPage | dep: ../components/icon, React
|
||||||
- ProfilePage.tsx | A React component that displays and allows editing of a user profile, including name, email, and avatar upload with validation. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../state/auth, ../hooks/use-async-data
|
- ProfilePage.tsx | A React component that renders a responsive profile page with form editing, avatar upload, and save functionality for both mobile and desktop viewports. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../components/features/profile/ProfileMobileView, ../state/auth, ../hooks/use-async-data, ../hooks/use-mobile-viewport
|
||||||
- ProjectSettingsPage.tsx | Renders a project settings page with tabbed navigation for general settings, repositories, and members, including project data fetching, editing, and deletion capabilities. | exp: ProjectSettingsPage | dep: react, react-router-dom, ../components/features/settings/settings-tab-layout, ../components/features/project/repositories-settings-tab, ../api/client, ../types
|
- ProjectSettingsPage.tsx | Renders a project settings page with tabbed navigation for general settings, repositories, and members, including project data fetching, editing, and deletion capabilities. | exp: ProjectSettingsPage | dep: react, react-router-dom, ../components/features/settings/settings-tab-layout, ../components/features/project/repositories-settings-tab, ../api/client, ../types
|
||||||
- ProjectsPage.test.tsx | Unit tests for the ProjectsPage component covering loading, empty, error, create, edit, and delete states with API mocking. | dep: @testing-library/react, react-router-dom, vitest, ./ProjectsPage, ../api/projects, ProjectsPage
|
- ProjectsPage.test.tsx | Unit tests for the ProjectsPage component covering loading, empty, error, create, edit, and delete states with API mocking. | dep: @testing-library/react, react-router-dom, vitest, ./ProjectsPage, ../api/projects, ProjectsPage
|
||||||
- ProjectsPage.tsx | Renders a responsive projects management page with separate mobile and desktop layouts, supporting project CRUD operations, repository management, and workspace actions. | exp: ProjectsPage | dep: react, ../components/data-states, ../components/icon, ../hooks/use-mobile-viewport, ../components/features/project/ProjectCard, ../components/features/project/ProjectDialog, ../components/features/project/repository-create-dialog, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-fab, ../hooks/use-projects, ../types
|
- ProjectsPage.tsx | Renders a responsive projects management page with separate mobile and desktop layouts, supporting project CRUD operations, repository management, and workspace actions. | exp: ProjectsPage | dep: react, ../components/data-states, ../components/icon, ../hooks/use-mobile-viewport, ../components/features/project/ProjectCard, ../components/features/project/ProjectDialog, ../components/features/project/repository-create-dialog, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-fab, ../hooks/use-projects, ../types, use-mobile-viewport, use-projects, data-states, icon, ProjectCard, ProjectDialog, RepositoryCreateDialog, MobileListView, MobileFAB
|
||||||
- SessionsPage.tsx | Renders a sessions management page that displays, polls health for, and handles CRUD operations on development environment sessions with dirty delete confirmation. | exp: SessionsPage | dep: react, ../api/sessions, ../api/settings, ../components/data-states, ../components/features/session/session-list, ../components/features/session/session-card, ../hooks/use-instance-actions, ../state/sessions
|
- SessionsPage.tsx | Renders a sessions management page that displays, polls health for, and handles CRUD operations on development environment sessions with dirty delete confirmation. | exp: SessionsPage | dep: react, ../api/sessions, ../api/settings, ../components/data-states, ../components/features/session/session-list, ../components/features/session/session-card, ../hooks/use-instance-actions, ../state/sessions
|
||||||
- SettingsPage.tsx | A React settings page component that loads, displays, and manages user configuration with tabbed navigation and nested outlet for child routes. | exp: SettingsPage | dep: react, react-router-dom, ../api/settings, ../components/data-states, ../hooks/use-async-data, ../components/features/settings/GeneralSettingsTab
|
- SettingsPage.tsx | A React settings page component that loads, displays, and manages user configuration with tabbed navigation and nested outlet for child routes. | exp: SettingsPage | dep: react, react-router-dom, ../api/settings, ../components/data-states, ../hooks/use-async-data, ../components/features/settings/GeneralSettingsTab
|
||||||
- SshKeysPage.tsx | React page component for managing SSH keys including generation, listing, signing, verification, and deletion | exp: SSHKeysPage | dep: react-router-dom, ../components/data-states, ../hooks/use-ssh-keys, ../components/features/ssh-keys/SSHKeyCreateForm, ../components/features/ssh-keys/SSHKeyList
|
- SshKeysPage.tsx | React page component for managing SSH keys including generation, listing, signing, verification, and deletion | exp: SSHKeysPage | dep: react-router-dom, ../components/data-states, ../hooks/use-ssh-keys, ../components/features/ssh-keys/SSHKeyCreateForm, ../components/features/ssh-keys/SSHKeyList
|
||||||
- TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView
|
- TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView
|
||||||
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with sidebar/editor layout for desktop and tabbed mobile view for managing tool types | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
|
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with list and editor views for managing tool types, handling loading/error states and mobile/desktop layouts | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
|
||||||
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
|
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
|
||||||
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tab-based navigation for files, git, tools, and settings panels, with mobile-responsive layout. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../hooks/use-workspaces, ../hooks/use-mobile-viewport, ../components/features/workspace/workspace-detail-header, ../components/features/workspace/workspace-tab-bar, ../components/features/workspace/workspace-file-panel, ../components/features/workspace/workspace-git-panel, ../components/features/workspace/workspace-tools-panel, ../components/features/workspace/workspace-settings-panel, use-workspaces, use-mobile-viewport, workspace-detail-header, workspace-tab-bar, workspace-file-panel, workspace-git-panel, workspace-tools-panel, workspace-settings-panel
|
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tab-based navigation for files, git, tools, and settings panels, with mobile-responsive layout. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../hooks/use-workspaces, ../hooks/use-mobile-viewport, ../components/features/workspace/workspace-detail-header, ../components/features/workspace/workspace-tab-bar, ../components/features/workspace/workspace-file-panel, ../components/features/workspace/workspace-git-panel, ../components/features/workspace/workspace-tools-panel, ../components/features/workspace/workspace-settings-panel, use-workspaces, use-mobile-viewport, workspace-detail-header, workspace-tab-bar, workspace-file-panel, workspace-git-panel, workspace-tools-panel, workspace-settings-panel
|
||||||
- WorkspacesPage.tsx | Renders a responsive workspaces management page with separate mobile and desktop layouts, supporting workspace listing, creation, deletion, sync, and tool launching. | exp: func:WorkspacesPage(), call:useMobileViewport, call:useState, call:useWorkspaces, call:useWorkspaceActions, call:actions.delete, call:actions.sync, call:setMobileView, call:refresh, call:setStartWorkspace, call:handleDelete, call:setSelectedWorkspace, call:workspaces.map, call:e.stopPropagation, call:setShowCreate | dep: react, ../components/icon, ../hooks/use-mobile-viewport, ../hooks/use-workspaces, ../hooks/use-workspace-actions, ../components/features/workspace/workspace-card, ../components/features/workspace/workspace-create-form, ../components/features/mobile/mobile-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
|
- WorkspacesPage.tsx | Renders a responsive workspaces management page with mobile and desktop views supporting listing, creating, viewing details, syncing, deleting, and starting tools for workspaces. | exp: func:WorkspacesPage(), call:useMobileViewport, call:useState, call:useWorkspaces, call:useWorkspaceActions, call:actions.delete, call:actions.sync, call:setMobileView, call:refresh, call:setStartWorkspace, call:handleDelete, call:setSelectedWorkspace, call:workspaces.map, call:e.stopPropagation, call:setShowCreate | dep: react, ../components/icon, ../hooks/use-mobile-viewport, ../hooks/use-workspaces, ../hooks/use-workspace-actions, ../components/features/workspace/workspace-card, ../components/features/workspace/workspace-create-form, ../components/features/mobile/mobile-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
|
||||||
## arch
|
## arch
|
||||||
Follows a page-based routing architecture where each file maps to a URL route, using responsive design patterns with explicit mobile/desktop view branching, compound component layouts (sidebar/editor, tabbed panels), polling for real-time data, and direct API integration within page components rather than abstracted service layers.
|
Route-based page organization with responsive mobile/desktop layout patterns, tabbed navigation for complex pages, polling/health-check mechanisms for live data, and CRUD operations with loading/error/empty states throughout.
|
||||||
## tags
|
## tags
|
||||||
page, components, workspace, features, react, mobile, settings, hooks
|
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)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
|
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
|
||||||
import { ErrorState, LoadingState } from "../components/data-states";
|
import { ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
|
import { ProfileMobileView } from "../components/features/profile/ProfileMobileView";
|
||||||
import { useAuth } from "../state/auth";
|
import { useAuth } from "../state/auth";
|
||||||
import { useAsyncData } from "../hooks/use-async-data";
|
import { useAsyncData } from "../hooks/use-async-data";
|
||||||
|
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
||||||
import type { UserProfile } from "../api/profile";
|
import type { UserProfile } from "../api/profile";
|
||||||
|
|
||||||
type ProfileStatus = "loading" | "ready" | "error" | "saving";
|
type ProfileStatus = "loading" | "ready" | "error" | "saving";
|
||||||
|
|
||||||
export const ProfilePage = () => {
|
export const ProfilePage = () => {
|
||||||
|
const isMobile = useMobileViewport();
|
||||||
const { refreshSession } = useAuth();
|
const { refreshSession } = useAuth();
|
||||||
const { data: profile, status: loadStatus, reload } = useAsyncData<UserProfile>(getProfile, []);
|
const { data: profile, status: loadStatus, reload } = useAsyncData<UserProfile>(getProfile, []);
|
||||||
const [displayStatus, setDisplayStatus] = useState<ProfileStatus>("loading");
|
const [displayStatus, setDisplayStatus] = useState<ProfileStatus>("loading");
|
||||||
@@ -90,14 +93,30 @@ export const ProfilePage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="stack">
|
<section className="stack">
|
||||||
<h1>Profile</h1>
|
{!isMobile && <h1>Profile</h1>}
|
||||||
|
|
||||||
{displayStatus === "loading" && <LoadingState message="Loading profile..." />}
|
{displayStatus === "loading" && <LoadingState message="Loading profile..." />}
|
||||||
|
|
||||||
{displayStatus === "error" && <ErrorState message="Failed to load profile" onRetry={reload} />}
|
{displayStatus === "error" && <ErrorState message="Failed to load profile" onRetry={reload} />}
|
||||||
|
|
||||||
{(displayStatus === "ready" || displayStatus === "saving") && profile && (
|
{(displayStatus === "ready" || displayStatus === "saving") && profile && (
|
||||||
<div className="card stack">
|
isMobile ? (
|
||||||
|
<ProfileMobileView
|
||||||
|
profile={profile}
|
||||||
|
name={name}
|
||||||
|
email={email}
|
||||||
|
error={error}
|
||||||
|
isSaving={displayStatus === "saving"}
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
fileInputRef={fileInputRef}
|
||||||
|
onNameChange={setName}
|
||||||
|
onEmailChange={setEmail}
|
||||||
|
onAvatarButtonClick={() => fileInputRef.current?.click()}
|
||||||
|
onAvatarChange={handleAvatarChange}
|
||||||
|
onSave={() => void handleSave()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="card stack">
|
||||||
<div className="profile-avatar-section">
|
<div className="profile-avatar-section">
|
||||||
<div className="avatar-preview">
|
<div className="avatar-preview">
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
@@ -178,7 +197,7 @@ export const ProfilePage = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,12 +57,20 @@ export const ToolWorkshopPage = () => {
|
|||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
toolTypeForm={toolTypeForm}
|
toolTypeForm={toolTypeForm}
|
||||||
|
manifestData={manifestData}
|
||||||
|
manifestDefinitionId={manifestDefinitionId}
|
||||||
|
baseDefinitions={baseDefinitions}
|
||||||
toolTypeError={toolTypeError}
|
toolTypeError={toolTypeError}
|
||||||
|
toolTypeDirty={toolTypeDirty}
|
||||||
onViewChange={setMobileView}
|
onViewChange={setMobileView}
|
||||||
onSelect={handleSelectToolType}
|
onSelect={handleSelectToolType}
|
||||||
onCreate={handleCreateNew}
|
onCreate={handleCreateNew}
|
||||||
onDelete={handleDeleteToolType}
|
onDelete={handleDeleteToolType}
|
||||||
onFormChange={handleFormChange}
|
onFormChange={handleFormChange}
|
||||||
|
onManifestChange={(m) => {
|
||||||
|
setManifestData(m);
|
||||||
|
setToolTypeDirty(true);
|
||||||
|
}}
|
||||||
onSubmit={handleToolTypeSubmit}
|
onSubmit={handleToolTypeSubmit}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
if (toolTypeDirty) {
|
if (toolTypeDirty) {
|
||||||
@@ -70,7 +78,7 @@ export const ToolWorkshopPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setMobileView(isCreating ? "list" : "detail");
|
setMobileView("list");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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, design tokens, utility classes, and component-specific styles.
|
Provides the complete visual design system and styling infrastructure for the web application, including tokens, utilities, global layouts, and component-specific styles.
|
||||||
## 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, design tokens, utility classes, and component-specific styles.
|
Provides the complete visual design system and styling infrastructure for the web application, including tokens, utilities, global layouts, and component-specific styles.
|
||||||
## 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 with mobile-first breakpoints | dep: CSS custom properties (variables like --space-*, --border, --bg, --brand, --muted, --success, --danger, --text-xs, --text-sm), xterm.js (terminal library)
|
- utilities.css | Provides a comprehensive responsive CSS utility system with layout primitives, mobile-first adaptations, terminal-specific styling, and component patterns for a web application | dep: CSS custom properties (variables), xterm.js (terminal integration)
|
||||||
## arch
|
## arch
|
||||||
CSS custom properties (variables) based theming system with light/dark mode support, mobile-first responsive breakpoints, fluid typography scales, utility-first class patterns, and modular separation of concerns across tokens, utilities, global layouts, and component-specific styles.
|
CSS custom properties-based design token architecture with theme-aware variables (light/dark), mobile-first responsive utilities, and modular separation of concerns across tokens, utilities, global styles, and component-specific stylesheets.
|
||||||
## tags
|
## tags
|
||||||
space, global, css, web, application, syntax, defines, styles
|
space, global, css, web, application, syntax, defines, layout
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -2053,10 +2053,38 @@ a.nav-item,
|
|||||||
.mobile-list-item-actions {
|
.mobile-list-item-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-action:active {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-action-delete {
|
||||||
|
color: var(--danger);
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-action-delete:active {
|
||||||
|
background: var(--danger-light);
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-list-empty {
|
.mobile-list-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2109,6 +2137,198 @@ a.nav-item,
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Page */
|
||||||
|
.mobile-page {
|
||||||
|
padding: var(--space-2);
|
||||||
|
padding-bottom: calc(var(--space-4) + 64px);
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Edit View */
|
||||||
|
.mobile-edit-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: var(--panel);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-cancel,
|
||||||
|
.mobile-edit-save,
|
||||||
|
.mobile-edit-delete {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-radius: 10px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-cancel {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-save {
|
||||||
|
background: var(--brand);
|
||||||
|
border: 1px solid var(--brand);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-delete {
|
||||||
|
background: var(--danger-light);
|
||||||
|
border: 1px solid var(--danger);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-save:disabled,
|
||||||
|
.mobile-edit-cancel:disabled,
|
||||||
|
.mobile-edit-delete:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-form {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-3);
|
||||||
|
padding-bottom: calc(var(--space-3) + 148px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Edit Actions (sticky bottom bar) */
|
||||||
|
.mobile-edit-actions {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 64px;
|
||||||
|
z-index: 110;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3);
|
||||||
|
padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
|
||||||
|
background: var(--panel);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-actions .mobile-edit-save,
|
||||||
|
.mobile-edit-actions .mobile-edit-delete {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-field {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-field-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-input,
|
||||||
|
.mobile-edit-textarea,
|
||||||
|
.mobile-edit-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-code {
|
||||||
|
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||||
|
Consolas, monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-checkbox input {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
accent-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Profile */
|
||||||
|
.profile-mobile-avatar-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-6) var(--space-3);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-mobile-avatar {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-mobile-avatar-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-mobile-avatar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile Page Header */
|
/* Mobile Page Header */
|
||||||
.mobile-page-header {
|
.mobile-page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2179,13 +2399,348 @@ a.nav-item,
|
|||||||
|
|
||||||
/* Mobile form group (inline fields) */
|
/* Mobile form group (inline fields) */
|
||||||
.mobile-form-group {
|
.mobile-form-group {
|
||||||
padding: var(--space-2);
|
padding: var(--space-3);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-input,
|
||||||
|
.mobile-form-select,
|
||||||
|
.mobile-form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-code {
|
||||||
|
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||||
|
Consolas, monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-select {
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.75rem center;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-checkbox {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
min-width: 22px;
|
||||||
|
min-height: 22px;
|
||||||
|
accent-color: var(--brand);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-help {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--danger-light);
|
||||||
|
color: var(--danger);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-discard-button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Manifest editor responsive overrides inside mobile forms */
|
||||||
|
.mobile-manifest-editor-wrapper .card {
|
||||||
|
padding: var(--space-3);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-manifest-editor-wrapper .row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-manifest-editor-wrapper .form-group,
|
||||||
|
.mobile-manifest-editor-wrapper input,
|
||||||
|
.mobile-manifest-editor-wrapper select,
|
||||||
|
.mobile-manifest-editor-wrapper textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-manifest-editor-wrapper input[style*="width:"] {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Mobile Edit-as-Default with Sticky Bottom Actions
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
On mobile, tapping a config profile or tool type now opens the edit view directly instead of the read-only detail view. Save and Delete actions are anchored to a sticky bottom action bar so they are always reachable while scrolling long forms.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
The previous mobile flow required an extra tap (list → detail → edit) for every change. Power users on phones primarily want to edit, so the default view should be edit. The Save and Delete buttons were also at the top of long forms, forcing users to scroll back up to commit changes.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Config Profiles mobile view (`ConfigProfilesMobileView`).
|
||||||
|
- Tool Workshop mobile view (`ToolWorkshopMobileView`).
|
||||||
|
- Shared `MobileEditView` component.
|
||||||
|
- Mobile edit-view CSS (`utilities.css`).
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Tapping a config profile opens the edit form.
|
||||||
|
- [x] Tapping a tool type opens the edit form.
|
||||||
|
- [x] Create flows still open the edit form and return to the list after save.
|
||||||
|
- [x] Edit flows stay on the edit form after a successful save and show a success message.
|
||||||
|
- [x] Delete is available in the edit view for existing items.
|
||||||
|
- [x] Save and Delete buttons are sticky at the bottom of the viewport on mobile.
|
||||||
|
- [x] Detail and preview views remain reachable from edit where already wired.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Changing desktop behavior or layout.
|
||||||
|
- Removing detail/preview views entirely.
|
||||||
|
- Altering the user profile page flow (it already uses `MobileEditView` and benefits from the sticky bar automatically).
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- `openspec/changes/mobile-config-profiles-ui`
|
||||||
|
- `openspec/changes/mobile-tool-profile-ui`
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Mobile Edit-as-Default with Sticky Bottom Actions — Tasks
|
||||||
|
|
||||||
|
- [x] Update `MobileEditView` to accept an optional `onDelete` callback and render Save/Delete in a sticky bottom action bar.
|
||||||
|
- [x] Move the Save button out of the `MobileEditView` header; keep Cancel + title in the header.
|
||||||
|
- [x] Update `ConfigProfilesMobileView` so list item clicks open edit view.
|
||||||
|
- [x] Update `ConfigProfilesMobileView` to pass `onDelete` to `MobileEditView` when editing existing profiles.
|
||||||
|
- [x] Update `ConfigProfilesMobileView` cancel/save navigation to return to list on create and stay on edit on save.
|
||||||
|
- [x] Update `ToolWorkshopMobileView` so list item clicks open edit view.
|
||||||
|
- [x] Update `ToolWorkshopMobileView` to pass `onDelete` to `MobileEditView` when editing existing tool types.
|
||||||
|
- [x] Update `ToolWorkshopMobileView` cancel/save navigation to return to list on create and stay on edit on save.
|
||||||
|
- [x] Add CSS for `.mobile-edit-actions` sticky bottom bar and `.mobile-edit-delete` button.
|
||||||
|
- [x] Increase `.mobile-edit-form` bottom padding so content is not hidden behind the sticky bar.
|
||||||
|
- [x] Run `npm run typecheck`, `npm run lint`, and `npm test -- --run` in `apps/web`.
|
||||||
|
- [x] Update project maps for changed files.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Restore Delete Button in Mobile List Views and Fix Edit Action Bar
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The `MobileListView` component accepted `onItemDelete` and `onItemDuplicate` callbacks but never rendered the corresponding action buttons. This change restores the delete button (and adds duplicate support) in each mobile list row for any view that supplies the callback.
|
||||||
|
|
||||||
|
In addition, the sticky bottom action bar added to `MobileEditView` in the previous change was positioned behind the 64px mobile navigation bar, making the Save and Delete buttons impossible to see on mobile. The bar is now raised above the navigation and given a higher z-index.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
Users need a quick way to remove config profiles and tool types directly from the mobile list without first opening the edit view. The callback props already existed, so wiring them up restores intended behavior.
|
||||||
|
|
||||||
|
Users also reported that the Save/Delete buttons in the mobile edit view were not visible. Investigation showed the fixed-position action bar was rendering underneath the persistent mobile bottom navigation.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- `MobileListView` shared component.
|
||||||
|
- `ToolWorkshopMobileView` (passes `onItemDelete`).
|
||||||
|
- `ConfigProfilesMobileView` already passed `onItemDelete`; it now actually renders.
|
||||||
|
- `MobileEditView` sticky action bar CSS (`utilities.css`).
|
||||||
|
- Mobile list item action CSS (`utilities.css`).
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `MobileListView` renders a delete button when `onItemDelete` is provided.
|
||||||
|
- [x] `MobileListView` renders a duplicate button when `onItemDuplicate` is provided.
|
||||||
|
- [x] Tapping delete/duplicate does not also trigger the row click.
|
||||||
|
- [x] `ToolWorkshopMobileView` list rows show the delete button.
|
||||||
|
- [x] `ConfigProfilesMobileView` list rows show the delete button.
|
||||||
|
- [x] Rows without delete/duplicate still show the chevron.
|
||||||
|
- [x] The `MobileEditView` sticky bottom action bar sits above the mobile navigation bar.
|
||||||
|
- [x] Save and Delete buttons in the edit view are visible and reachable.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- `openspec/changes/mobile-edit-default-bottom-actions`
|
||||||
|
- `openspec/changes/mobile-config-profiles-ui`
|
||||||
|
- `openspec/changes/mobile-tool-profile-ui`
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Restore Delete Button in Mobile List Views and Fix Edit Action Bar — Tasks
|
||||||
|
|
||||||
|
- [x] Read `MobileListView` source and confirm `onItemDelete`/`onItemDuplicate` were unused.
|
||||||
|
- [x] Update `MobileListView` to render delete/duplicate action buttons when callbacks are provided.
|
||||||
|
- [x] Stop propagation on action buttons so row click does not fire.
|
||||||
|
- [x] Update `ToolWorkshopMobileView` to pass `onItemDelete` to both list view usages.
|
||||||
|
- [x] Verify `ConfigProfilesMobileView` still passes `onItemDelete`.
|
||||||
|
- [x] Add `.mobile-list-item-action` and variant CSS to `utilities.css`.
|
||||||
|
- [x] Identify that `MobileEditView` sticky action bar was hidden behind the 64px mobile nav.
|
||||||
|
- [x] Update `.mobile-edit-actions` to `bottom: 64px` and `z-index: 110`.
|
||||||
|
- [x] Run `npm run typecheck`, `npm run lint`, and `npm test -- --run` in `apps/web`.
|
||||||
|
- [x] Update project maps for changed files.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Mobile Tool Workshop & Profile UI Rework
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Completely rework the mobile experience for the **Tool Workshop** (tool configuration) and **Profile** (user profile configuration) pages so they are usable on small viewports while preserving all existing desktop functionality.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Rework `ToolWorkshopMobileView` to expose every field and action available on the desktop editor, including manifest definition support.
|
||||||
|
- Add a new `ProfileMobileView` and update `ProfilePage` to render it on small viewports.
|
||||||
|
- Keep desktop layouts and behavior unchanged.
|
||||||
|
- No backend API changes.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- On viewports narrower than 768 px, the Tool Workshop page renders a mobile-friendly list/detail/edit flow.
|
||||||
|
- Tool type creation and editing on mobile support: definition type selection (Compose/Dockerfile/Manifest), manifest editor, interface type, conditional port, startup command, readiness probe, required variables, and all existing validation.
|
||||||
|
- On viewports narrower than 768 px, the Profile page renders a mobile-friendly form with avatar upload, name/email editing, validation, and save feedback.
|
||||||
|
- All existing desktop functionality remains intact.
|
||||||
|
- `npm run typecheck` and `npm run lint` pass in `apps/web`.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Backend changes.
|
||||||
|
- Changes to other pages (sessions, settings, etc.).
|
||||||
|
- New features beyond the existing desktop capabilities.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Tasks: Mobile Tool Workshop & Profile UI Rework
|
||||||
|
|
||||||
|
- [x] Inspect existing mobile components, desktop editors, and hooks.
|
||||||
|
- [x] Rework `ToolWorkshopMobileView` to support full desktop functionality (definition type, manifest editor, conditional port, startup command, readiness probe, required variables, validation).
|
||||||
|
- [x] Add responsive CSS for manifest editor and tool-workshop mobile forms.
|
||||||
|
- [x] Create `ProfileMobileView` component with mobile-friendly avatar/name/email form.
|
||||||
|
- [x] Update `ProfilePage` to render `ProfileMobileView` on mobile viewports.
|
||||||
|
- [x] Run `npm run typecheck` and `npm run lint` in `apps/web` and fix issues.
|
||||||
|
- [x] Update project maps for changed files.
|
||||||
Reference in New Issue
Block a user