fix: merge nav sessions middle

This commit is contained in:
Developer
2026-06-13 19:44:47 +00:00
19 changed files with 321 additions and 293 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ dir: .
Trust boundary: index routes, map orients, source decides. Trust boundary: index routes, map orients, source decides.
## role ## role
Infrastructure and deployment configuration for a self-hosted project management and git repository platform with OAuth2 authentication. Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
## parent ## parent
- -
## children ## children
+2 -2
View File
@@ -18,7 +18,7 @@ index: ./.pi-map.index.md
Trust boundary: index routes, map orients, source decides. Trust boundary: index routes, map orients, source decides.
## role ## role
Infrastructure and deployment configuration for a self-hosted project management and git repository platform with OAuth2 authentication. Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
## files ## files
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment - .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git - .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
@@ -31,7 +31,7 @@ Infrastructure and deployment configuration for a self-hosted project management
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases - progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
- swap-pane | Empty file with no functionality - swap-pane | Empty file with no functionality
## arch ## arch
Containerized microservices architecture using Docker Compose with PostgreSQL/Redis persistence, Traefik reverse proxy for TLS termination, environment-driven configuration, and a decoupled frontend/API backend structure. Containerized microservices architecture using Docker Compose with PostgreSQL and Redis backing services, Traefik reverse proxy integration, and a frontend/API split pattern with environment-driven configuration management.
## tags ## tags
docker, redis, git, application, postgresql, compose, traefik, project docker, redis, git, application, postgresql, compose, traefik, project
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps dir: apps
## role ## role
Contains the main executable application modules that serve as entry points for the project. Container directory for independent application modules or deployable units within the project.
## parent ## parent
index: ./.pi-map.index.md index: ./.pi-map.index.md
map: ./.pi-map.md map: ./.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps
index: apps/.pi-map.index.md index: apps/.pi-map.index.md
## role ## role
Contains the main executable application modules that serve as entry points for the project. Container directory for independent application modules or deployable units within the project.
## files ## files
## arch ## arch
Modular application structure with separate deployable units, likely following microservices or layered architecture patterns with domain-driven design boundaries. Monorepo-style organization where each subdirectory represents a self-contained application sharing common infrastructure or libraries.
## tags ## tags
- -
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web dir: apps/web
## role ## role
Frontend web application for the headquarters system, providing a browser-based UI with code editing, terminal, and routing capabilities. Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
## 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
View File
@@ -4,7 +4,7 @@ dir: apps/web
index: apps/web/.pi-map.index.md index: apps/web/.pi-map.index.md
## role ## role
Frontend web application for the headquarters system, providing a browser-based UI with code editing, terminal, and routing capabilities. Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
## files ## files
- .env.example | Template file defining example environment variables for frontend API and application URL configuration - .env.example | Template file defining example environment variables for frontend API and application URL configuration
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint - .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint
@@ -16,7 +16,7 @@ Frontend web application for the headquarters system, providing a browser-based
- 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, containerized with multi-stage Docker/nginx deployment, using ES modules with Vitest for testing. Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker with nginx serving, featuring 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
+1 -1
View File
@@ -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 shared 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
+2 -2
View File
@@ -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 shared 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
Provider-composition pattern with React Router v6 nested route declarations, centralized type definitions, and layered authentication/session management wrapping a protected route hierarchy. Modular React SPA with provider pattern for auth/session state, declarative routing with nested layouts and route guards, and centralized TypeScript type definitions for domain models.
## tags ## tags
pages, styles, css, router, react, session, dom, project pages, styles, css, router, react, session, dom, project
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/components dir: apps/web/src/components
## role ## role
Provides foundational, reusable React UI components and utilities for rendering layouts, interactive elements, data states, icons, code editing, routing guards, and toast notifications across the web application. Provides reusable, accessible React UI components and utilities for the web application including layout shell, data states, icons, code display, authentication guards, and toast notifications.
## parent ## parent
index: apps/web/src/.pi-map.index.md index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md map: apps/web/src/.pi-map.md
+3 -3
View File
@@ -4,9 +4,9 @@ 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 foundational, reusable React UI components and utilities for rendering layouts, interactive elements, data states, icons, code editing, routing guards, and toast notifications across the web application. Provides reusable, accessible React UI components and utilities for the web application including layout shell, data states, icons, code display, authentication guards, and toast notifications.
## files ## files
- app-shell.tsx | Renders the main application shell layout with navigation, session management, and responsive mobile/desktop views for a React Router-based app. | 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, session management, and responsive mobile/desktop views for a React Router-based application. | 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
- data-states.tsx | Provides reusable React components for displaying loading, error, and empty data states in a UI. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, icon - data-states.tsx | Provides reusable React components for displaying loading, error, and empty data states in a UI. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, icon
- icon.tsx | Provides a centralized, type-safe React icon component that maps semantic names to Phosphor icons with configurable size, weight, color, and accessibility attributes. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react - icon.tsx | Provides a centralized, type-safe React icon component that maps semantic names to Phosphor icons with configurable size, weight, color, and accessibility attributes. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react
@@ -17,7 +17,7 @@ Provides foundational, reusable React UI components and utilities for rendering
- 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 architecture with functional React patterns, custom hooks for state management, centralized icon mapping with type safety, HOC-like route guards, and utility modules for cross-cutting concerns (toast rules, data states) with co-located test files. Component-based architecture with functional React components, custom hooks for state management, centralized icon mapping with type safety, test-driven development for business logic (toast rules, protected routes), and separation of concerns between presentation (UI components) and behavior (utility modules).
## tags ## tags
toast, icon, react, state, code, loading, event, editor toast, icon, react, state, code, loading, event, editor
## symbols ## symbols
+146 -146
View File
@@ -16,166 +16,166 @@ import { StartToolFAB } from "./features/tool/start-tool-fab";
import type { IconName } from "../utils/icons"; import type { IconName } from "../utils/icons";
const NAV_ITEMS: { const NAV_ITEMS: {
to: string; to: string;
label: string; label: string;
icon: IconName; icon: IconName;
badge?: "sessions"; badge?: "sessions";
}[] = [ }[] = [
{ to: "/", label: "Home", icon: "dashboard" }, { to: "/", label: "Home", icon: "dashboard" },
{ to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" }, { to: "/projects", label: "Projects", icon: "projects" },
{ to: "/workspaces", label: "Workspaces", icon: "folder" }, { to: "/workspaces", label: "Workspaces", icon: "folder" },
{ to: "/projects", label: "Projects", icon: "projects" }, { to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" },
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" }, { to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
{ to: "/config-profiles", label: "Config Profiles", icon: "folder" }, { to: "/config-profiles", label: "Config Profiles", icon: "folder" },
{ to: "/settings", label: "Settings", icon: "settings" }, { to: "/settings", label: "Settings", icon: "settings" },
]; ];
const SessionItem = ({ session }: { session: Session }) => { const SessionItem = ({ session }: { session: Session }) => {
const isRunning = session.status === "running"; const isRunning = session.status === "running";
// Determine the link target: // Determine the link target:
// - Web tools open their tunnel URL // - Web tools open their tunnel URL
// - Terminal tools open the terminal page // - Terminal tools open the terminal page
// - Everything else falls back to the project page // - Everything else falls back to the project page
const hasTerminal = session.tool_type_interfaces.includes("terminal"); const hasTerminal = session.tool_type_interfaces.includes("terminal");
const hasWeb = session.tool_type_interfaces.includes("web"); const hasWeb = session.tool_type_interfaces.includes("web");
const href = const href =
session.url && hasWeb session.url && hasWeb
? session.url ? session.url
: hasTerminal : hasTerminal
? `/instances/${session.id}/terminal` ? `/instances/${session.id}/terminal`
: `/projects/${session.project_id}`; : `/projects/${session.project_id}`;
const tooltipParts = [session.display_name, session.project_name]; const tooltipParts = [session.display_name, session.project_name];
if (session.workspace_name) tooltipParts.push(session.workspace_name); if (session.workspace_name) tooltipParts.push(session.workspace_name);
else if (session.repository_name) tooltipParts.push(session.repository_name); else if (session.repository_name) tooltipParts.push(session.repository_name);
tooltipParts.push(`(${session.status})`); tooltipParts.push(`(${session.status})`);
return ( return (
<a <a
href={href} href={href}
target={`session-${session.id}`} target={`session-${session.id}`}
rel="noreferrer" rel="noreferrer"
className="nav-item session-item" className="nav-item session-item"
title={tooltipParts.join(" ")} title={tooltipParts.join(" ")}
> >
<span className={`session-status ${isRunning ? "running" : ""}`} /> <span className={`session-status ${isRunning ? "running" : ""}`} />
<Icon name={session.tool_icon as IconName} size="sm" /> <Icon name={session.tool_icon as IconName} size="sm" />
<span className="session-name"> <span className="session-name">
{session.display_name} {session.display_name}
<span className="session-tool">{session.tool_type_name}</span> <span className="session-tool">{session.tool_type_name}</span>
</span> </span>
</a> </a>
); );
}; };
export const AppShell = () => { export const AppShell = () => {
useTheme(); useTheme();
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { sessions } = useSessions(); const { sessions } = useSessions();
const location = useLocation(); const location = useLocation();
const isMobile = useMobileViewport(); const isMobile = useMobileViewport();
const isMobileTerminal = const isMobileTerminal =
isMobile && isMobile &&
location.pathname.includes("/instances/") && location.pathname.includes("/instances/") &&
location.pathname.includes("/terminal"); location.pathname.includes("/terminal");
if (isMobileTerminal) { if (isMobileTerminal) {
return ( return (
<EventProvider> <EventProvider>
<ToastProvider> <ToastProvider>
<NotificationProvider> <NotificationProvider>
<EventToastBridge /> <EventToastBridge />
<div className="shell mobile-terminal-shell"> <div className="shell mobile-terminal-shell">
<Outlet /> <Outlet />
</div> </div>
</NotificationProvider> </NotificationProvider>
</ToastProvider> </ToastProvider>
</EventProvider> </EventProvider>
); );
} }
return ( return (
<EventProvider> <EventProvider>
<ToastProvider> <ToastProvider>
<NotificationProvider> <NotificationProvider>
<EventToastBridge /> <EventToastBridge />
<div className="shell"> <div className="shell">
<header className="shell-header"> <header className="shell-header">
<Link className="brand" to="/"> <Link className="brand" to="/">
Headquarter Headquarter
</Link> </Link>
<div className="header-actions"> <div className="header-actions">
<NotificationCenter isMobileTerminal={isMobileTerminal} /> <NotificationCenter isMobileTerminal={isMobileTerminal} />
<Link className="user-chip" to="/profile"> <Link className="user-chip" to="/profile">
{user?.name ?? "User"} {user?.name ?? "User"}
</Link> </Link>
<button <button
className="ghost-button" className="ghost-button"
onClick={() => { onClick={() => {
void logout(); void logout();
}} }}
type="button" type="button"
> >
<Icon name="logout" size="sm" /> <Icon name="logout" size="sm" />
Logout Logout
</button> </button>
</div> </div>
</header> </header>
<div className="shell-body"> <div className="shell-body">
{!isMobile && ( {!isMobile && (
<aside className="shell-nav" aria-label="Primary navigation"> <aside className="shell-nav" aria-label="Primary navigation">
{NAV_ITEMS.map((item) => { {NAV_ITEMS.map((item) => {
const activeCount = sessions.filter( const activeCount = sessions.filter(
(s) => s.status === "running", (s) => s.status === "running",
).length; ).length;
return ( return (
<NavLink <NavLink
key={item.to} key={item.to}
to={item.to} to={item.to}
className={({ isActive }) => className={({ isActive }) =>
isActive ? "nav-item nav-item-active" : "nav-item" isActive ? "nav-item nav-item-active" : "nav-item"
} }
end={item.to === "/"} end={item.to === "/"}
> >
<Icon name={item.icon} size="sm" /> <Icon name={item.icon} size="sm" />
{item.label} {item.label}
{item.badge === "sessions" && activeCount > 0 && ( {item.badge === "sessions" && activeCount > 0 && (
<span className="nav-badge">{activeCount}</span> <span className="nav-badge">{activeCount}</span>
)} )}
</NavLink> </NavLink>
); );
})} })}
{sessions.length > 0 && ( {sessions.length > 0 && (
<> <>
<div className="nav-divider" /> <div className="nav-divider" />
<div className="nav-section-title">Live sessions</div> <div className="nav-section-title">Live sessions</div>
{sessions.map((session) => ( {sessions.map((session) => (
<SessionItem key={session.id} session={session} /> <SessionItem key={session.id} session={session} />
))} ))}
</> </>
)} )}
</aside> </aside>
)} )}
<main className={`shell-content ${isMobile ? "mobile" : ""}`}> <main className={`shell-content ${isMobile ? "mobile" : ""}`}>
<Outlet /> <Outlet />
</main> </main>
</div> </div>
{isMobile && ( {isMobile && (
<MobileNav <MobileNav
sessionCount={ sessionCount={
sessions.filter((s) => s.status === "running").length sessions.filter((s) => s.status === "running").length
} }
/> />
)} )}
<StartToolFAB /> <StartToolFAB />
</div> </div>
</NotificationProvider> </NotificationProvider>
</ToastProvider> </ToastProvider>
</EventProvider> </EventProvider>
); );
}; };
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features dir: apps/web/src/components/features
## role ## role
Contains reusable UI components organized by feature domain for the web application. Contains reusable React components organized by feature domains for the web application UI.
## 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
+2 -2
View File
@@ -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 UI components organized by feature domain for the web application. Contains reusable React components organized by feature domains for the web application UI.
## files ## files
## arch ## arch
Feature-based colocation pattern with domain-specific subdirectories grouping related components, likely using composition patterns with shared UI primitives. Feature-based component colocation with domain-driven folder structure, separating UI concerns by business capability rather than technical layer.
## tags ## tags
- -
## symbols ## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/mobile dir: apps/web/src/components/features/mobile
## role ## role
Provides a complete set of mobile-optimized UI components for core application patterns including navigation, lists, detail views, editing, modals, and specialized terminal interfaces. Provides mobile-optimized UI components for core application features including navigation, data views, forms, modals, and specialized terminal interfaces.
## 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 a complete set of mobile-optimized UI components for core application patterns including navigation, lists, detail views, editing, modals, and specialized terminal interfaces. Provides mobile-optimized UI components for core application features including navigation, data views, forms, modals, and specialized terminal interfaces.
## 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 button, title, edit/delete actions) and a field list supporting multiple value types (text, code, JSON, boolean). | exp: MobileDetailView | dep: ../../icon, react
- mobile-edit-view.tsx | A React component that renders a mobile-optimized form for editing data with configurable field types and save/cancel actions. | exp: MobileEditView | dep: react - mobile-edit-view.tsx | A React component that renders a mobile-optimized form for editing data with configurable field types and save/cancel actions. | exp: MobileEditView | dep: react
- mobile-fab.tsx | Renders a floating action button component for mobile with an add icon and configurable click handler and label. | exp: MobileFAB | dep: ../../icon, react, icon - mobile-fab.tsx | Renders a floating action button component for mobile with an add icon and configurable click handler and label. | exp: MobileFAB | dep: ../../icon, react, icon
- mobile-list-view.tsx | Renders a mobile-optimized list view with optional search, custom item rendering, and empty state handling | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon - mobile-list-view.tsx | Renders a mobile-optimized list view with optional search, empty state, and customizable item rendering | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon
- mobile-nav.tsx | Renders a mobile navigation bar with route links and expandable bottom sheets for grouped navigation items. | 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 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-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 - 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
Composable mobile-first component library using bottom sheets, action sheets, and floating action buttons as primary mobile interaction patterns, with feature-specific wrappers that handle mobile concerns like virtual keyboards and auto-hiding headers. 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.
## tags ## tags
mobile, terminal, sheet, view, react, icon, header, renders mobile, terminal, sheet, react, icon, view, header, renders
## symbols ## symbols
- MobileActionSheet - MobileActionSheet
- MobilePageHeader - MobilePageHeader
@@ -3,81 +3,86 @@ import { Icon } from "../../icon";
import type { IconName } from "../../../utils/icons"; import type { IconName } from "../../../utils/icons";
interface MobileListItem { interface MobileListItem {
id: string; id: string;
title: string; title: string;
subtitle?: string; subtitle?: string;
icon?: string; icon?: string;
status?: string; status?: string;
} }
interface MobileListViewProps { interface MobileListViewProps {
items: MobileListItem[]; items: MobileListItem[];
onItemClick: (id: string) => void; onItemClick: (id: string) => void;
onItemDelete?: (id: string) => void; onItemDelete?: (id: string) => void;
onItemDuplicate?: (id: string) => void; onItemDuplicate?: (id: string) => void;
emptyMessage?: string; emptyMessage?: string;
searchPlaceholder?: string; searchPlaceholder?: string;
onSearch?: (query: string) => void; onSearch?: (query: string) => void;
renderItem?: (item: MobileListItem) => ReactNode; renderItem?: (item: MobileListItem) => ReactNode;
} }
export const MobileListView: React.FC<MobileListViewProps> = ({ export const MobileListView: React.FC<MobileListViewProps> = ({
items, items,
onItemClick, onItemClick,
emptyMessage = "No items found", emptyMessage = "No items found",
searchPlaceholder = "Search...", searchPlaceholder = "Search...",
onSearch, onSearch,
renderItem, renderItem,
}) => { }) => {
return ( return (
<div className="mobile-list-view"> <div className="mobile-list-view">
{onSearch && ( {onSearch && (
<div className="mobile-list-search"> <div className="mobile-list-search">
<input <input
type="search" type="search"
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
onChange={(e) => onSearch(e.target.value)} onChange={(e) => onSearch(e.target.value)}
className="mobile-list-search-input" className="mobile-list-search-input"
/> />
</div> </div>
)} )}
{items.length === 0 ? ( {items.length === 0 ? (
<div className="mobile-list-empty"> <div className="mobile-list-empty">
<Icon name="folder" size="lg" /> <Icon name="folder" size="lg" />
<p>{emptyMessage}</p> <p>{emptyMessage}</p>
</div> </div>
) : ( ) : (
<div className="mobile-list-items"> <div className="mobile-list-items">
{items.map((item) => ( {items.map((item) => (
<button <button
key={item.id} key={item.id}
className="mobile-list-item" className="mobile-list-item"
onClick={() => onItemClick(item.id)} onClick={() => onItemClick(item.id)}
type="button" type="button"
> >
{item.icon && ( {item.icon && (
<div className="mobile-list-item-icon"> <div className="mobile-list-item-icon">
<Icon name={item.icon as IconName} size="md" /> <Icon name={item.icon as IconName} size="md" />
</div> </div>
)} )}
{renderItem ? ( {renderItem ? (
renderItem(item) renderItem(item)
) : ( ) : (
<div className="mobile-list-item-content"> <div className="mobile-list-item-content">
<div className="mobile-list-item-title">{item.title}</div> <div className="mobile-list-item-title">{item.title}</div>
{item.subtitle && ( {item.subtitle && (
<div className="mobile-list-item-subtitle">{item.subtitle}</div> <div className="mobile-list-item-subtitle">
)} {item.subtitle}
</div> </div>
)} )}
<div className="mobile-list-item-actions" style={{ transform: "rotate(180deg)" }}> </div>
<Icon name="arrow-left" size="sm" /> )}
</div> <div
</button> className="mobile-list-item-actions"
))} style={{ transform: "rotate(180deg)" }}
</div> >
)} <Icon name="arrow-left" size="sm" />
</div> </div>
); </button>
))}
</div>
)}
</div>
);
}; };
@@ -19,9 +19,21 @@ interface NavItem {
const MOBILE_NAV_ITEMS: NavItem[] = [ const MOBILE_NAV_ITEMS: NavItem[] = [
{ to: "/", label: "Home", icon: "dashboard" }, { to: "/", label: "Home", icon: "dashboard" },
{
to: "/spaces",
label: "Spaces",
icon: "projects",
isGroup: true,
group: "spaces",
},
{ to: "/sessions", label: "Sessions", icon: "terminal" }, { to: "/sessions", label: "Sessions", icon: "terminal" },
{ to: "/spaces", label: "Spaces", icon: "projects", isGroup: true, group: "spaces" }, {
{ to: "/tools", label: "Tools", icon: "settings", isGroup: true, group: "tools" }, to: "/tools",
label: "Tools",
icon: "settings",
isGroup: true,
group: "tools",
},
{ to: "/settings", label: "Settings", icon: "settings" }, { to: "/settings", label: "Settings", icon: "settings" },
]; ];
@@ -49,10 +61,15 @@ export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
return ( return (
<> <>
<nav className="mobile-nav" role="navigation" aria-label="Mobile navigation"> <nav
className="mobile-nav"
role="navigation"
aria-label="Mobile navigation"
>
{MOBILE_NAV_ITEMS.map((item) => { {MOBILE_NAV_ITEMS.map((item) => {
if (item.isGroup) { if (item.isGroup) {
const isActive = item.group === "spaces" ? isSpacesActive : isToolsActive; const isActive =
item.group === "spaces" ? isSpacesActive : isToolsActive;
return ( return (
<button <button
key={item.to} key={item.to}
@@ -2,61 +2,65 @@ import { useLocation, useNavigate } from "react-router-dom";
import { Icon } from "../../icon"; import { Icon } from "../../icon";
interface SpacesBottomSheetProps { interface SpacesBottomSheetProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
} }
const SPACES_ITEMS = [ const SPACES_ITEMS = [
{ to: "/projects", label: "Projects" }, { to: "/projects", label: "Projects" },
{ to: "/workspaces", label: "Workspaces" }, { to: "/workspaces", label: "Workspaces" },
]; ];
export const SpacesBottomSheet: React.FC<SpacesBottomSheetProps> = ({ export const SpacesBottomSheet: React.FC<SpacesBottomSheetProps> = ({
isOpen, isOpen,
onClose, onClose,
}) => { }) => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
if (!isOpen) return null; if (!isOpen) return null;
const handleSelect = (to: string) => { const handleSelect = (to: string) => {
onClose(); onClose();
navigate(to); navigate(to);
}; };
return ( return (
<div <div
className="mobile-bottom-sheet-overlay" className="mobile-bottom-sheet-overlay"
onClick={onClose} onClick={onClose}
role="presentation" role="presentation"
> >
<div <div
className="mobile-bottom-sheet" className="mobile-bottom-sheet"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
role="dialog" role="dialog"
aria-label="Spaces menu" aria-label="Spaces menu"
> >
<div className="mobile-bottom-sheet-header"> <div className="mobile-bottom-sheet-header">
<div className="mobile-bottom-sheet-handle" /> <div className="mobile-bottom-sheet-handle" />
<h3 className="mobile-bottom-sheet-title">Spaces</h3> <h3 className="mobile-bottom-sheet-title">Spaces</h3>
</div> </div>
<div className="mobile-bottom-sheet-content"> <div className="mobile-bottom-sheet-content">
{SPACES_ITEMS.map((item) => ( {SPACES_ITEMS.map((item) => (
<button <button
key={item.to} key={item.to}
className={`mobile-bottom-sheet-item ${ className={`mobile-bottom-sheet-item ${
location.pathname === item.to ? "active" : "" location.pathname === item.to ? "active" : ""
}`} }`}
onClick={() => handleSelect(item.to)} onClick={() => handleSelect(item.to)}
type="button" type="button"
> >
<span className="mobile-bottom-sheet-item-label">{item.label}</span> <span className="mobile-bottom-sheet-item-label">
{location.pathname === item.to && <Icon name="success" size="sm" />} {item.label}
</button> </span>
))} {location.pathname === item.to && (
</div> <Icon name="success" size="sm" />
</div> )}
</div> </button>
); ))}
</div>
</div>
</div>
);
}; };
+3 -1
View File
@@ -182,7 +182,9 @@ export const ProjectsPage = () => {
)} )}
<div className="mobile-list-item-chips"> <div className="mobile-list-item-chips">
{(project.repositories?.length ?? 0) === 0 ? ( {(project.repositories?.length ?? 0) === 0 ? (
<span className="mobile-list-item-chip muted">No repositories</span> <span className="mobile-list-item-chip muted">
No repositories
</span>
) : ( ) : (
project.repositories.map((repo) => ( project.repositories.map((repo) => (
<span key={repo.id} className="mobile-list-item-chip"> <span key={repo.id} className="mobile-list-item-chip">