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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user