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,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>
);
};