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:
Developer
2026-06-13 19:44:47 +00:00
parent 4ced6141ae
commit 1d09193652
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.
## 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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+146 -146
View File
@@ -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
+2 -2
View File
@@ -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>
);
};
+3 -1
View File
@@ -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">