a12d6a8169
- Wire MobileListView onItemDelete/onItemDuplicate callbacks to render action buttons in each list row. - Pass onItemDelete in ToolWorkshopMobileView list view. - Add CSS for mobile-list-item-action buttons. - Fix MobileEditView sticky bottom action bar that was hidden behind the 64px mobile navigation bar; raise to bottom: 64px and z-index 110. Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed) Refs: openspec/changes/mobile-list-delete-button
118 lines
2.9 KiB
TypeScript
118 lines
2.9 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Icon } from "../../icon";
|
|
import type { IconName } from "../../../utils/icons";
|
|
|
|
interface MobileListItem {
|
|
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;
|
|
}
|
|
|
|
export const MobileListView: React.FC<MobileListViewProps> = ({
|
|
items,
|
|
onItemClick,
|
|
onItemDelete,
|
|
onItemDuplicate,
|
|
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>
|
|
)}
|
|
|
|
{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">
|
|
{onItemDuplicate && (
|
|
<button
|
|
type="button"
|
|
className="mobile-list-item-action mobile-list-item-action-duplicate"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onItemDuplicate(item.id);
|
|
}}
|
|
aria-label="Duplicate"
|
|
>
|
|
<Icon name="copy" size="sm" />
|
|
</button>
|
|
)}
|
|
{onItemDelete && (
|
|
<button
|
|
type="button"
|
|
className="mobile-list-item-action mobile-list-item-action-delete"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onItemDelete(item.id);
|
|
}}
|
|
aria-label="Delete"
|
|
>
|
|
<Icon name="delete" size="sm" />
|
|
</button>
|
|
)}
|
|
{!onItemDelete && !onItemDuplicate && (
|
|
<span style={{ transform: "rotate(180deg)" }}>
|
|
<Icon name="arrow-left" size="sm" />
|
|
</span>
|
|
)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|