fix: put Sessions back in the middle of nav on desktop and mobile
- Reorder desktop sidebar so Sessions sits between spaces/tools groups: Home, Projects, Workspaces, Sessions, Tool Workshop, Config Profiles, Settings. - Reorder mobile bottom nav so Sessions is the center item: Home, Spaces, Sessions, Tools, Settings. - Workspace cards already display the owning project name; no extra change needed. Quality gates: npm run typecheck, npm run lint clean, npm test -- --run 87 passed.
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ dir: .
|
||||
Trust boundary: index routes, map orients, source decides.
|
||||
|
||||
## role
|
||||
Infrastructure and deployment configuration for a self-hosted project management and git repository platform with OAuth2 authentication.
|
||||
Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
|
||||
## parent
|
||||
-
|
||||
## children
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ index: ./.pi-map.index.md
|
||||
Trust boundary: index routes, map orients, source decides.
|
||||
|
||||
## role
|
||||
Infrastructure and deployment configuration for a self-hosted project management and git repository platform with OAuth2 authentication.
|
||||
Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
|
||||
## files
|
||||
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
|
||||
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
|
||||
@@ -31,7 +31,7 @@ Infrastructure and deployment configuration for a self-hosted project management
|
||||
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
|
||||
- swap-pane | Empty file with no functionality
|
||||
## arch
|
||||
Containerized microservices architecture using Docker Compose with PostgreSQL/Redis 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
|
||||
docker, redis, git, application, postgresql, compose, traefik, project
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps
|
||||
|
||||
## 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
|
||||
index: ./.pi-map.index.md
|
||||
map: ./.pi-map.md
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@ dir: apps
|
||||
index: apps/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains the main executable application modules that serve as entry points for the project.
|
||||
Container directory for independent application modules or deployable units within the project.
|
||||
## files
|
||||
## 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
|
||||
-
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web
|
||||
|
||||
## 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
|
||||
index: apps/.pi-map.index.md
|
||||
map: apps/.pi-map.md
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ dir: apps/web
|
||||
index: apps/web/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Frontend web application for 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
|
||||
- .env.example | Template file defining example environment variables for frontend API and application URL configuration
|
||||
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint
|
||||
@@ -16,7 +16,7 @@ Frontend web application for 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
|
||||
- vite.config.ts | Configures Vite build tool for a React project with custom dev server port and Vitest test settings. | dep: vite, @vitejs/plugin-react
|
||||
## arch
|
||||
Modern React SPA built with Vite and TypeScript, 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
|
||||
react, eslint, vite, typescript, dom, application, nginx, web
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src
|
||||
|
||||
## role
|
||||
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||
Entry point and core infrastructure for a React web application handling routing, authentication, and shared type definitions.
|
||||
## parent
|
||||
index: apps/web/.pi-map.index.md
|
||||
map: apps/web/.pi-map.md
|
||||
|
||||
@@ -4,13 +4,13 @@ dir: apps/web/src
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||
Entry point and core infrastructure for a React web application handling routing, authentication, and shared type definitions.
|
||||
## files
|
||||
- main.tsx | Bootstraps a React application with routing, authentication, and session management providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom
|
||||
- router.tsx | Defines the React Router configuration for a web application with protected routes, nested layouts, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
|
||||
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
||||
## arch
|
||||
Provider-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
|
||||
pages, styles, css, router, react, session, dom, project
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components
|
||||
|
||||
## 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
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,9 +4,9 @@ dir: apps/web/src/components
|
||||
index: apps/web/src/components/.pi-map.index.md
|
||||
|
||||
## 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
|
||||
- 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
|
||||
- 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
|
||||
@@ -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.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type
|
||||
## arch
|
||||
Component-based 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
|
||||
toast, icon, react, state, code, loading, event, editor
|
||||
## symbols
|
||||
|
||||
@@ -16,166 +16,166 @@ import { StartToolFAB } from "./features/tool/start-tool-fab";
|
||||
import type { IconName } from "../utils/icons";
|
||||
|
||||
const NAV_ITEMS: {
|
||||
to: string;
|
||||
label: string;
|
||||
icon: IconName;
|
||||
badge?: "sessions";
|
||||
to: string;
|
||||
label: string;
|
||||
icon: IconName;
|
||||
badge?: "sessions";
|
||||
}[] = [
|
||||
{ to: "/", label: "Home", icon: "dashboard" },
|
||||
{ to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" },
|
||||
{ to: "/workspaces", label: "Workspaces", icon: "folder" },
|
||||
{ to: "/projects", label: "Projects", icon: "projects" },
|
||||
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
|
||||
{ to: "/config-profiles", label: "Config Profiles", icon: "folder" },
|
||||
{ to: "/settings", label: "Settings", icon: "settings" },
|
||||
{ to: "/", label: "Home", icon: "dashboard" },
|
||||
{ to: "/projects", label: "Projects", icon: "projects" },
|
||||
{ to: "/workspaces", label: "Workspaces", icon: "folder" },
|
||||
{ to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" },
|
||||
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
|
||||
{ to: "/config-profiles", label: "Config Profiles", icon: "folder" },
|
||||
{ to: "/settings", label: "Settings", icon: "settings" },
|
||||
];
|
||||
|
||||
const SessionItem = ({ session }: { session: Session }) => {
|
||||
const isRunning = session.status === "running";
|
||||
const isRunning = session.status === "running";
|
||||
|
||||
// Determine the link target:
|
||||
// - Web tools open their tunnel URL
|
||||
// - Terminal tools open the terminal page
|
||||
// - Everything else falls back to the project page
|
||||
const hasTerminal = session.tool_type_interfaces.includes("terminal");
|
||||
const hasWeb = session.tool_type_interfaces.includes("web");
|
||||
const href =
|
||||
session.url && hasWeb
|
||||
? session.url
|
||||
: hasTerminal
|
||||
? `/instances/${session.id}/terminal`
|
||||
: `/projects/${session.project_id}`;
|
||||
// Determine the link target:
|
||||
// - Web tools open their tunnel URL
|
||||
// - Terminal tools open the terminal page
|
||||
// - Everything else falls back to the project page
|
||||
const hasTerminal = session.tool_type_interfaces.includes("terminal");
|
||||
const hasWeb = session.tool_type_interfaces.includes("web");
|
||||
const href =
|
||||
session.url && hasWeb
|
||||
? session.url
|
||||
: hasTerminal
|
||||
? `/instances/${session.id}/terminal`
|
||||
: `/projects/${session.project_id}`;
|
||||
|
||||
const tooltipParts = [session.display_name, session.project_name];
|
||||
if (session.workspace_name) tooltipParts.push(session.workspace_name);
|
||||
else if (session.repository_name) tooltipParts.push(session.repository_name);
|
||||
tooltipParts.push(`(${session.status})`);
|
||||
const tooltipParts = [session.display_name, session.project_name];
|
||||
if (session.workspace_name) tooltipParts.push(session.workspace_name);
|
||||
else if (session.repository_name) tooltipParts.push(session.repository_name);
|
||||
tooltipParts.push(`(${session.status})`);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={`session-${session.id}`}
|
||||
rel="noreferrer"
|
||||
className="nav-item session-item"
|
||||
title={tooltipParts.join(" ")}
|
||||
>
|
||||
<span className={`session-status ${isRunning ? "running" : ""}`} />
|
||||
<Icon name={session.tool_icon as IconName} size="sm" />
|
||||
<span className="session-name">
|
||||
{session.display_name}
|
||||
<span className="session-tool">{session.tool_type_name}</span>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={`session-${session.id}`}
|
||||
rel="noreferrer"
|
||||
className="nav-item session-item"
|
||||
title={tooltipParts.join(" ")}
|
||||
>
|
||||
<span className={`session-status ${isRunning ? "running" : ""}`} />
|
||||
<Icon name={session.tool_icon as IconName} size="sm" />
|
||||
<span className="session-name">
|
||||
{session.display_name}
|
||||
<span className="session-tool">{session.tool_type_name}</span>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppShell = () => {
|
||||
useTheme();
|
||||
const { user, logout } = useAuth();
|
||||
const { sessions } = useSessions();
|
||||
const location = useLocation();
|
||||
const isMobile = useMobileViewport();
|
||||
const isMobileTerminal =
|
||||
isMobile &&
|
||||
location.pathname.includes("/instances/") &&
|
||||
location.pathname.includes("/terminal");
|
||||
useTheme();
|
||||
const { user, logout } = useAuth();
|
||||
const { sessions } = useSessions();
|
||||
const location = useLocation();
|
||||
const isMobile = useMobileViewport();
|
||||
const isMobileTerminal =
|
||||
isMobile &&
|
||||
location.pathname.includes("/instances/") &&
|
||||
location.pathname.includes("/terminal");
|
||||
|
||||
if (isMobileTerminal) {
|
||||
return (
|
||||
<EventProvider>
|
||||
<ToastProvider>
|
||||
<NotificationProvider>
|
||||
<EventToastBridge />
|
||||
<div className="shell mobile-terminal-shell">
|
||||
<Outlet />
|
||||
</div>
|
||||
</NotificationProvider>
|
||||
</ToastProvider>
|
||||
</EventProvider>
|
||||
);
|
||||
}
|
||||
if (isMobileTerminal) {
|
||||
return (
|
||||
<EventProvider>
|
||||
<ToastProvider>
|
||||
<NotificationProvider>
|
||||
<EventToastBridge />
|
||||
<div className="shell mobile-terminal-shell">
|
||||
<Outlet />
|
||||
</div>
|
||||
</NotificationProvider>
|
||||
</ToastProvider>
|
||||
</EventProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EventProvider>
|
||||
<ToastProvider>
|
||||
<NotificationProvider>
|
||||
<EventToastBridge />
|
||||
<div className="shell">
|
||||
<header className="shell-header">
|
||||
<Link className="brand" to="/">
|
||||
Headquarter
|
||||
</Link>
|
||||
<div className="header-actions">
|
||||
<NotificationCenter isMobileTerminal={isMobileTerminal} />
|
||||
<Link className="user-chip" to="/profile">
|
||||
{user?.name ?? "User"}
|
||||
</Link>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={() => {
|
||||
void logout();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="logout" size="sm" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
return (
|
||||
<EventProvider>
|
||||
<ToastProvider>
|
||||
<NotificationProvider>
|
||||
<EventToastBridge />
|
||||
<div className="shell">
|
||||
<header className="shell-header">
|
||||
<Link className="brand" to="/">
|
||||
Headquarter
|
||||
</Link>
|
||||
<div className="header-actions">
|
||||
<NotificationCenter isMobileTerminal={isMobileTerminal} />
|
||||
<Link className="user-chip" to="/profile">
|
||||
{user?.name ?? "User"}
|
||||
</Link>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={() => {
|
||||
void logout();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="logout" size="sm" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="shell-body">
|
||||
{!isMobile && (
|
||||
<aside className="shell-nav" aria-label="Primary navigation">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const activeCount = sessions.filter(
|
||||
(s) => s.status === "running",
|
||||
).length;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
isActive ? "nav-item nav-item-active" : "nav-item"
|
||||
}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<Icon name={item.icon} size="sm" />
|
||||
{item.label}
|
||||
{item.badge === "sessions" && activeCount > 0 && (
|
||||
<span className="nav-badge">{activeCount}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
<div className="shell-body">
|
||||
{!isMobile && (
|
||||
<aside className="shell-nav" aria-label="Primary navigation">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const activeCount = sessions.filter(
|
||||
(s) => s.status === "running",
|
||||
).length;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
isActive ? "nav-item nav-item-active" : "nav-item"
|
||||
}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<Icon name={item.icon} size="sm" />
|
||||
{item.label}
|
||||
{item.badge === "sessions" && activeCount > 0 && (
|
||||
<span className="nav-badge">{activeCount}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
|
||||
{sessions.length > 0 && (
|
||||
<>
|
||||
<div className="nav-divider" />
|
||||
<div className="nav-section-title">Live sessions</div>
|
||||
{sessions.map((session) => (
|
||||
<SessionItem key={session.id} session={session} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
{sessions.length > 0 && (
|
||||
<>
|
||||
<div className="nav-divider" />
|
||||
<div className="nav-section-title">Live sessions</div>
|
||||
{sessions.map((session) => (
|
||||
<SessionItem key={session.id} session={session} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<main className={`shell-content ${isMobile ? "mobile" : ""}`}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
<main className={`shell-content ${isMobile ? "mobile" : ""}`}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<MobileNav
|
||||
sessionCount={
|
||||
sessions.filter((s) => s.status === "running").length
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<StartToolFAB />
|
||||
</div>
|
||||
</NotificationProvider>
|
||||
</ToastProvider>
|
||||
</EventProvider>
|
||||
);
|
||||
{isMobile && (
|
||||
<MobileNav
|
||||
sessionCount={
|
||||
sessions.filter((s) => s.status === "running").length
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<StartToolFAB />
|
||||
</div>
|
||||
</NotificationProvider>
|
||||
</ToastProvider>
|
||||
</EventProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features
|
||||
|
||||
## 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
|
||||
index: apps/web/src/components/.pi-map.index.md
|
||||
map: apps/web/src/components/.pi-map.md
|
||||
|
||||
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
|
||||
index: apps/web/src/components/features/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains reusable UI components organized by feature domain for the web application.
|
||||
Contains reusable React components organized by feature domains for the web application UI.
|
||||
## files
|
||||
## 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
|
||||
-
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features/mobile
|
||||
|
||||
## 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
|
||||
index: apps/web/src/components/features/.pi-map.index.md
|
||||
map: apps/web/src/components/features/.pi-map.md
|
||||
|
||||
@@ -4,22 +4,22 @@ dir: apps/web/src/components/features/mobile
|
||||
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides 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
|
||||
- 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-edit-view.tsx | A React component that renders a mobile-optimized form for editing data with configurable field types and save/cancel actions. | exp: MobileEditView | dep: react
|
||||
- mobile-fab.tsx | Renders a floating action button component for mobile with an add icon and configurable click handler and label. | exp: MobileFAB | dep: ../../icon, react, icon
|
||||
- mobile-list-view.tsx | Renders a mobile-optimized list view with optional search, custom item rendering, and empty state handling | 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-list-view.tsx | Renders a mobile-optimized list view with optional search, empty state, and customizable item rendering | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon
|
||||
- mobile-nav.tsx | Renders a mobile navigation bar with grouped items that open bottom sheets and standard items that use React Router links, including active state indicators and session count badges. | exp: MobileNav | dep: react, react-router-dom, ../../icon, ../tool/tools-bottom-sheet, ./spaces-bottom-sheet, ../../../utils/icons
|
||||
- mobile-page-header.tsx | Renders a mobile-only page header with optional back navigation and custom actions. | exp: func:MobilePageHeader({ title, showBack = true, actions }: MobilePageHeaderProps), call:useNavigate, call:useMobileViewport, call:navigate | dep: react-router-dom, ../../../hooks/use-mobile-viewport, ../../icon, use-mobile-viewport hook, Icon component
|
||||
- mobile-terminal-header.tsx | Renders a mobile-responsive header for a terminal interface with navigation, title, connection status, font size controls, and close actions. | exp: MobileTerminalHeader | dep: react, ../../icon, icon
|
||||
- mobile-terminal-wrapper.tsx | Wraps a terminal component with mobile-specific UI including auto-hiding header, virtual keyboard handling, and special keys interface. | exp: MobileTerminalWrapper | dep: react, ../terminal/terminal, ./mobile-terminal-header, ../terminal/special-keys-strip, ../terminal/special-keys-panel, ../../../hooks/use-mobile-viewport, ../../../hooks/use-virtual-keyboard, ../../../hooks/use-auto-hide, ../../../hooks/use-special-keys
|
||||
- spaces-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for switching between "Projects" and "Workspaces" spaces with active state highlighting | exp: SpacesBottomSheet | dep: react-router-dom, ../../icon
|
||||
- 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
|
||||
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
|
||||
mobile, terminal, sheet, view, react, icon, header, renders
|
||||
mobile, terminal, sheet, react, icon, view, header, renders
|
||||
## symbols
|
||||
- MobileActionSheet
|
||||
- MobilePageHeader
|
||||
|
||||
@@ -3,81 +3,86 @@ import { Icon } from "../../icon";
|
||||
import type { IconName } from "../../../utils/icons";
|
||||
|
||||
interface MobileListItem {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
icon?: string;
|
||||
status?: string;
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
icon?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface MobileListViewProps {
|
||||
items: MobileListItem[];
|
||||
onItemClick: (id: string) => void;
|
||||
onItemDelete?: (id: string) => void;
|
||||
onItemDuplicate?: (id: string) => void;
|
||||
emptyMessage?: string;
|
||||
searchPlaceholder?: string;
|
||||
onSearch?: (query: string) => void;
|
||||
renderItem?: (item: MobileListItem) => ReactNode;
|
||||
items: MobileListItem[];
|
||||
onItemClick: (id: string) => void;
|
||||
onItemDelete?: (id: string) => void;
|
||||
onItemDuplicate?: (id: string) => void;
|
||||
emptyMessage?: string;
|
||||
searchPlaceholder?: string;
|
||||
onSearch?: (query: string) => void;
|
||||
renderItem?: (item: MobileListItem) => ReactNode;
|
||||
}
|
||||
|
||||
export const MobileListView: React.FC<MobileListViewProps> = ({
|
||||
items,
|
||||
onItemClick,
|
||||
emptyMessage = "No items found",
|
||||
searchPlaceholder = "Search...",
|
||||
onSearch,
|
||||
renderItem,
|
||||
items,
|
||||
onItemClick,
|
||||
emptyMessage = "No items found",
|
||||
searchPlaceholder = "Search...",
|
||||
onSearch,
|
||||
renderItem,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mobile-list-view">
|
||||
{onSearch && (
|
||||
<div className="mobile-list-search">
|
||||
<input
|
||||
type="search"
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
className="mobile-list-search-input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div className="mobile-list-view">
|
||||
{onSearch && (
|
||||
<div className="mobile-list-search">
|
||||
<input
|
||||
type="search"
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
className="mobile-list-search-input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="mobile-list-empty">
|
||||
<Icon name="folder" size="lg" />
|
||||
<p>{emptyMessage}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mobile-list-items">
|
||||
{items.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
className="mobile-list-item"
|
||||
onClick={() => onItemClick(item.id)}
|
||||
type="button"
|
||||
>
|
||||
{item.icon && (
|
||||
<div className="mobile-list-item-icon">
|
||||
<Icon name={item.icon as IconName} size="md" />
|
||||
</div>
|
||||
)}
|
||||
{renderItem ? (
|
||||
renderItem(item)
|
||||
) : (
|
||||
<div className="mobile-list-item-content">
|
||||
<div className="mobile-list-item-title">{item.title}</div>
|
||||
{item.subtitle && (
|
||||
<div className="mobile-list-item-subtitle">{item.subtitle}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mobile-list-item-actions" style={{ transform: "rotate(180deg)" }}>
|
||||
<Icon name="arrow-left" size="sm" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{items.length === 0 ? (
|
||||
<div className="mobile-list-empty">
|
||||
<Icon name="folder" size="lg" />
|
||||
<p>{emptyMessage}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mobile-list-items">
|
||||
{items.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
className="mobile-list-item"
|
||||
onClick={() => onItemClick(item.id)}
|
||||
type="button"
|
||||
>
|
||||
{item.icon && (
|
||||
<div className="mobile-list-item-icon">
|
||||
<Icon name={item.icon as IconName} size="md" />
|
||||
</div>
|
||||
)}
|
||||
{renderItem ? (
|
||||
renderItem(item)
|
||||
) : (
|
||||
<div className="mobile-list-item-content">
|
||||
<div className="mobile-list-item-title">{item.title}</div>
|
||||
{item.subtitle && (
|
||||
<div className="mobile-list-item-subtitle">
|
||||
{item.subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="mobile-list-item-actions"
|
||||
style={{ transform: "rotate(180deg)" }}
|
||||
>
|
||||
<Icon name="arrow-left" size="sm" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,9 +19,21 @@ interface NavItem {
|
||||
|
||||
const MOBILE_NAV_ITEMS: NavItem[] = [
|
||||
{ to: "/", label: "Home", icon: "dashboard" },
|
||||
{
|
||||
to: "/spaces",
|
||||
label: "Spaces",
|
||||
icon: "projects",
|
||||
isGroup: true,
|
||||
group: "spaces",
|
||||
},
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
@@ -49,10 +61,15 @@ export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
|
||||
|
||||
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) => {
|
||||
if (item.isGroup) {
|
||||
const isActive = item.group === "spaces" ? isSpacesActive : isToolsActive;
|
||||
const isActive =
|
||||
item.group === "spaces" ? isSpacesActive : isToolsActive;
|
||||
return (
|
||||
<button
|
||||
key={item.to}
|
||||
|
||||
@@ -2,61 +2,65 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Icon } from "../../icon";
|
||||
|
||||
interface SpacesBottomSheetProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SPACES_ITEMS = [
|
||||
{ to: "/projects", label: "Projects" },
|
||||
{ to: "/workspaces", label: "Workspaces" },
|
||||
{ to: "/projects", label: "Projects" },
|
||||
{ to: "/workspaces", label: "Workspaces" },
|
||||
];
|
||||
|
||||
export const SpacesBottomSheet: React.FC<SpacesBottomSheetProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!isOpen) return null;
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSelect = (to: string) => {
|
||||
onClose();
|
||||
navigate(to);
|
||||
};
|
||||
const handleSelect = (to: string) => {
|
||||
onClose();
|
||||
navigate(to);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mobile-bottom-sheet-overlay"
|
||||
onClick={onClose}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
className="mobile-bottom-sheet"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-label="Spaces menu"
|
||||
>
|
||||
<div className="mobile-bottom-sheet-header">
|
||||
<div className="mobile-bottom-sheet-handle" />
|
||||
<h3 className="mobile-bottom-sheet-title">Spaces</h3>
|
||||
</div>
|
||||
<div className="mobile-bottom-sheet-content">
|
||||
{SPACES_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.to}
|
||||
className={`mobile-bottom-sheet-item ${
|
||||
location.pathname === item.to ? "active" : ""
|
||||
}`}
|
||||
onClick={() => handleSelect(item.to)}
|
||||
type="button"
|
||||
>
|
||||
<span className="mobile-bottom-sheet-item-label">{item.label}</span>
|
||||
{location.pathname === item.to && <Icon name="success" size="sm" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="mobile-bottom-sheet-overlay"
|
||||
onClick={onClose}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
className="mobile-bottom-sheet"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-label="Spaces menu"
|
||||
>
|
||||
<div className="mobile-bottom-sheet-header">
|
||||
<div className="mobile-bottom-sheet-handle" />
|
||||
<h3 className="mobile-bottom-sheet-title">Spaces</h3>
|
||||
</div>
|
||||
<div className="mobile-bottom-sheet-content">
|
||||
{SPACES_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.to}
|
||||
className={`mobile-bottom-sheet-item ${
|
||||
location.pathname === item.to ? "active" : ""
|
||||
}`}
|
||||
onClick={() => handleSelect(item.to)}
|
||||
type="button"
|
||||
>
|
||||
<span className="mobile-bottom-sheet-item-label">
|
||||
{item.label}
|
||||
</span>
|
||||
{location.pathname === item.to && (
|
||||
<Icon name="success" size="sm" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -182,7 +182,9 @@ export const ProjectsPage = () => {
|
||||
)}
|
||||
<div className="mobile-list-item-chips">
|
||||
{(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) => (
|
||||
<span key={repo.id} className="mobile-list-item-chip">
|
||||
|
||||
Reference in New Issue
Block a user