feat: mobile Spaces nav and list-based project/workspace views

- Combine Projects and Workspaces into a single 'Spaces' grouped mobile
  nav item that opens a bottom-sheet menu.
- Add SpacesBottomSheet component with Projects/Workspaces options.
- Extend MobileListView with optional renderItem prop for rich rows.
- Redesign mobile ProjectsPage rows to show project description and
  repository chips.
- Replace mobile WorkspacesPage list with compact WorkspaceCard grid,
  matching desktop card content.
- Add mobile-list-* CSS and mobile-workspaces-list spacing.

Quality gates: npm run typecheck, npm run lint clean,
npm test -- --run 87 passed.
This commit is contained in:
Developer
2026-06-13 19:32:16 +00:00
parent e6ab77d123
commit 530225d37c
27 changed files with 346 additions and 113 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/components
## role
Provides reusable, accessible React UI components and utilities for the web application including layout shells, data display states, icon system, code editing, routing guards, and toast notification infrastructure.
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.
## parent
index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/web/src/components
index: apps/web/src/components/.pi-map.index.md
## role
Provides reusable, accessible React UI components and utilities for the web application including layout shells, data display states, icon system, code editing, routing guards, and toast notification infrastructure.
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.
## 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
- code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
@@ -17,7 +17,7 @@ Provides reusable, accessible React UI components and utilities for the web appl
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
- toast-rules.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, utility modules for cross-cutting concerns (icons, toasts), and test co-location with implementation files.
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.
## tags
toast, icon, react, state, code, loading, event, editor
## symbols
@@ -1,52 +1,52 @@
# features (index)
dir: features
# apps/web/src/components/features (index)
dir: apps/web/src/components/features
## role
Contains modular, feature-based components that encapsulate specific domain functionality or user-facing capabilities within the project.
Contains reusable UI components organized by feature domain for the web application.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
index: apps/web/src/components/.pi-map.index.md
map: apps/web/src/components/.pi-map.md
## children
- features/config-profiles
index: features/config-profiles/.pi-map.index.md
map: features/config-profiles/.pi-map.md
- features/git
index: features/git/.pi-map.index.md
map: features/git/.pi-map.md
- features/mobile
index: features/mobile/.pi-map.index.md
map: features/mobile/.pi-map.md
- features/notification
index: features/notification/.pi-map.index.md
map: features/notification/.pi-map.md
- features/project
index: features/project/.pi-map.index.md
map: features/project/.pi-map.md
- features/session
index: features/session/.pi-map.index.md
map: features/session/.pi-map.md
- features/settings
index: features/settings/.pi-map.index.md
map: features/settings/.pi-map.md
- features/ssh-keys
index: features/ssh-keys/.pi-map.index.md
map: features/ssh-keys/.pi-map.md
- features/terminal
index: features/terminal/.pi-map.index.md
map: features/terminal/.pi-map.md
- features/tool
index: features/tool/.pi-map.index.md
map: features/tool/.pi-map.md
- features/tool-workshop
index: features/tool-workshop/.pi-map.index.md
map: features/tool-workshop/.pi-map.md
- features/workspace
index: features/workspace/.pi-map.index.md
map: features/workspace/.pi-map.md
- apps/web/src/components/features/config-profiles
index: apps/web/src/components/features/config-profiles/.pi-map.index.md
map: apps/web/src/components/features/config-profiles/.pi-map.md
- apps/web/src/components/features/git
index: apps/web/src/components/features/git/.pi-map.index.md
map: apps/web/src/components/features/git/.pi-map.md
- apps/web/src/components/features/mobile
index: apps/web/src/components/features/mobile/.pi-map.index.md
map: apps/web/src/components/features/mobile/.pi-map.md
- apps/web/src/components/features/notification
index: apps/web/src/components/features/notification/.pi-map.index.md
map: apps/web/src/components/features/notification/.pi-map.md
- apps/web/src/components/features/project
index: apps/web/src/components/features/project/.pi-map.index.md
map: apps/web/src/components/features/project/.pi-map.md
- apps/web/src/components/features/session
index: apps/web/src/components/features/session/.pi-map.index.md
map: apps/web/src/components/features/session/.pi-map.md
- apps/web/src/components/features/settings
index: apps/web/src/components/features/settings/.pi-map.index.md
map: apps/web/src/components/features/settings/.pi-map.md
- apps/web/src/components/features/ssh-keys
index: apps/web/src/components/features/ssh-keys/.pi-map.index.md
map: apps/web/src/components/features/ssh-keys/.pi-map.md
- apps/web/src/components/features/terminal
index: apps/web/src/components/features/terminal/.pi-map.index.md
map: apps/web/src/components/features/terminal/.pi-map.md
- apps/web/src/components/features/tool
index: apps/web/src/components/features/tool/.pi-map.index.md
map: apps/web/src/components/features/tool/.pi-map.md
- apps/web/src/components/features/tool-workshop
index: apps/web/src/components/features/tool-workshop/.pi-map.index.md
map: apps/web/src/components/features/tool-workshop/.pi-map.md
- apps/web/src/components/features/workspace
index: apps/web/src/components/features/workspace/.pi-map.index.md
map: apps/web/src/components/features/workspace/.pi-map.md
## files
## links
index: features/.pi-map.index.md
map: features/.pi-map.md
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
## workflows
-
## dirty
+5 -5
View File
@@ -1,13 +1,13 @@
# features
dir: features
# apps/web/src/components/features
dir: apps/web/src/components/features
index: features/.pi-map.index.md
index: apps/web/src/components/features/.pi-map.index.md
## role
Contains modular, feature-based components that encapsulate specific domain functionality or user-facing capabilities within the project.
Contains reusable UI components organized by feature domain for the web application.
## files
## arch
Feature-based modular architecture with self-contained, cohesive units that likely combine related components, logic, and assets to support vertical slicing of application functionality.
Feature-based colocation pattern with domain-specific subdirectories grouping related components, likely using composition patterns with shared UI primitives.
## tags
-
## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/mobile
## role
Provides mobile-optimized UI components for a responsive web application, covering navigation, lists, detail views, forms, modals, and specialized terminal interfaces.
Provides a complete set of mobile-optimized UI components for core application patterns including navigation, lists, detail views, editing, 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
@@ -18,6 +18,7 @@ map: apps/web/src/components/features/.pi-map.md
- mobile-page-header.tsx
- mobile-terminal-header.tsx
- mobile-terminal-wrapper.tsx
- spaces-bottom-sheet.tsx
## links
index: apps/web/src/components/features/mobile/.pi-map.index.md
map: apps/web/src/components/features/mobile/.pi-map.md
@@ -4,21 +4,22 @@ dir: apps/web/src/components/features/mobile
index: apps/web/src/components/features/mobile/.pi-map.index.md
## role
Provides mobile-optimized UI components for a responsive web application, covering navigation, lists, detail views, forms, modals, and specialized terminal interfaces.
Provides a complete set of mobile-optimized UI components for core application patterns including navigation, lists, detail views, editing, 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 searchable mobile-friendly list view with optional icons, subtitles, and click handling | exp: MobileListView | dep: ../../icon, ../../../utils/icons, react, icon, icons
- mobile-nav.tsx | Renders a mobile navigation bar with routing links, a tools group button that opens a bottom sheet, and session count badges | exp: MobileNav | dep: react, react-router-dom, ../../icon, ../tool/tools-bottom-sheet, ../../../utils/icons, icon, tools-bottom-sheet, icons utils
- 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-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
## arch
Feature-based component organization with compound mobile UI patterns (action sheets, FABs, bottom sheets, auto-hiding headers) that mirror native mobile app paradigms, using configurable props for customization and likely React Context or hooks for keyboard/virtual terminal state management.
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.
## tags
mobile, terminal, view, icon, header, react, sheet, action
mobile, terminal, sheet, view, react, icon, header, renders
## symbols
- MobileActionSheet
- MobilePageHeader
@@ -1,3 +1,4 @@
import type { ReactNode } from "react";
import { Icon } from "../../icon";
import type { IconName } from "../../../utils/icons";
@@ -17,6 +18,7 @@ interface MobileListViewProps {
emptyMessage?: string;
searchPlaceholder?: string;
onSearch?: (query: string) => void;
renderItem?: (item: MobileListItem) => ReactNode;
}
export const MobileListView: React.FC<MobileListViewProps> = ({
@@ -25,6 +27,7 @@ export const MobileListView: React.FC<MobileListViewProps> = ({
emptyMessage = "No items found",
searchPlaceholder = "Search...",
onSearch,
renderItem,
}) => {
return (
<div className="mobile-list-view">
@@ -58,12 +61,16 @@ export const MobileListView: React.FC<MobileListViewProps> = ({
<Icon name={item.icon as IconName} size="md" />
</div>
)}
<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>
{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>
@@ -2,6 +2,7 @@ import { useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { Icon } from "../../icon";
import { ToolsBottomSheet } from "../tool/tools-bottom-sheet";
import { SpacesBottomSheet } from "./spaces-bottom-sheet";
import type { IconName } from "../../../utils/icons";
interface MobileNavProps {
@@ -13,26 +14,35 @@ interface NavItem {
label: string;
icon: IconName;
isGroup?: boolean;
group?: "tools" | "spaces";
}
const MOBILE_NAV_ITEMS: NavItem[] = [
{ to: "/", label: "Home", icon: "dashboard" },
{ to: "/projects", label: "Projects", icon: "projects" },
{ to: "/sessions", label: "Sessions", icon: "terminal" },
{ to: "/tools", label: "Tools", icon: "settings", isGroup: true },
{ to: "/spaces", label: "Spaces", icon: "projects", isGroup: true, group: "spaces" },
{ to: "/tools", label: "Tools", icon: "settings", isGroup: true, group: "tools" },
{ to: "/settings", label: "Settings", icon: "settings" },
];
export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
const location = useLocation();
const [toolsSheetOpen, setToolsSheetOpen] = useState(false);
const [spacesSheetOpen, setSpacesSheetOpen] = useState(false);
const isToolsActive =
location.pathname === "/tool-workshop" ||
location.pathname === "/config-profiles";
const isSpacesActive =
location.pathname.startsWith("/projects") ||
location.pathname.startsWith("/workspaces");
const handleNavClick = (item: NavItem) => {
if (item.isGroup) {
if (!item.isGroup) return;
if (item.group === "spaces") {
setSpacesSheetOpen(true);
} else {
setToolsSheetOpen(true);
}
};
@@ -42,10 +52,11 @@ export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
<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;
return (
<button
key={item.to}
className={`mobile-nav-item ${isToolsActive ? "active" : ""}`}
className={`mobile-nav-item ${isActive ? "active" : ""}`}
onClick={() => handleNavClick(item)}
type="button"
>
@@ -82,6 +93,10 @@ export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
isOpen={toolsSheetOpen}
onClose={() => setToolsSheetOpen(false)}
/>
<SpacesBottomSheet
isOpen={spacesSheetOpen}
onClose={() => setSpacesSheetOpen(false)}
/>
</>
);
};
@@ -0,0 +1,62 @@
import { useLocation, useNavigate } from "react-router-dom";
import { Icon } from "../../icon";
interface SpacesBottomSheetProps {
isOpen: boolean;
onClose: () => void;
}
const SPACES_ITEMS = [
{ to: "/projects", label: "Projects" },
{ to: "/workspaces", label: "Workspaces" },
];
export const SpacesBottomSheet: React.FC<SpacesBottomSheetProps> = ({
isOpen,
onClose,
}) => {
const location = useLocation();
const navigate = useNavigate();
if (!isOpen) return null;
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>
);
};